Compare commits

..

No commits in common. "main" and "gh-pages" have entirely different histories.

16 changed files with 38548 additions and 11197 deletions

View File

@ -1,32 +0,0 @@
name: Workflow push to Dappbundle
on: [push]
jobs:
build:
name: Build
runs-on: self-hosted
steps:
- name: Executing remote command
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.R_HOST }}
username: ${{ secrets.P_USERNAME }}
password: ${{ secrets.P_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
script: |
if [ -d "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle" ]; then
echo "Folder exists. Skipping Git clone."
else
echo "Folder does not exist. Cloning repository..."
cd ${{ secrets.DEPLOYMENT_LOCATION}}/ && git clone https://github.com/ranchimall/dappbundle.git
fi
if [ -d "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}" ]; then
echo "Repository exists. Remove folder "
rm -r "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}"
fi
echo "Cloning repository..."
cd ${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle && git clone https://github.com/ranchimall/${{ github.event.repository.name }}
cd "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}" && rm -rf .gitattributes .git .github .gitignore
cd ${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/ && git add . && git commit -m "Workflow updating files of ${{ github.event.repository.name }}" && git push "https://saketongit:${{ secrets.RM_ACCESS_TOKEN }}@github.com/ranchimall/dappbundle.git"

402
README.md
View File

@ -1,402 +0,0 @@
# Cardano Web Wallet - Technical Documentation
## Overview
The Cardano Multi-Chain Wallet is a web-based cryptocurrency wallet that supports multiple blockchain networks including Cardano (ADA), FLO, and Bitcoin (BTC). The wallet provides comprehensive functionality for address generation, transaction management, balance checking, and transaction history viewing with a focus on Cardano blockchain integration.
### Key Features
- **Multi-Chain Support**: ADA, FLO, and BTC address generation from a single private key
- **Transaction History**: Paginated transaction viewing with smart caching and filtering
- **Address Search**: Persistent search history with IndexedDB storage
- **URL Sharing**: Direct link sharing for addresses and transaction hashes
- **Real-Time Data**: Live balance updates and transaction status checking
- **Responsive Design**: Mobile-first responsive interface with dark/light theme support
- **CIP-1852 Compliance**: Standard Cardano derivation path for wallet compatibility
## Architecture
### System Architecture
```
┌────────────────────────────────────────────────────────────┐
│ Frontend Layer │
├────────────────────────────────────────────────────────────┤
│ index.html │ style.css │ JavaScript Modules │
├──────────────┼─────────────┼───────────────────────────────┤
│ │ │ • cardanoCrypto.js │
│ │ │ • cardanoBlockchainAPI.js │
│ │ │ • cardanoSearchDB.js │
│ │ │ • main.js │
│ │ │ • lib.cardano.js │
└──────────────┴─────────────┴───────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Storage Layer │
├─────────────────────────────────────────────────────────────┤
│ IndexedDB │ LocalStorage │ Session Storage │
│ • Address History │ • Theme Prefs │ • Temp Data │
│ • Search Cache │ • User Settings │ • Form State │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Blockchain Layer │
├─────────────────────────────────────────────────────────────┤
│ Cardano Mainnet │ FLO Network │ Bitcoin Network │
│ • Ogmios RPC │ • Address Gen │ • Address Gen │
│ • CardanoScan API│ • Key Derivation│ • Key Derivation │
│ • UTXO Query │ │ │
│ • Transaction │ │ │
└─────────────────────────────────────────────────────────────┘
```
## Core Components
### 1. Cryptographic Engine (`cardanoCrypto.js`)
The cryptographic engine handles multi-chain address generation and key management using CIP-1852 derivation standards.
#### Key Functions
```javascript
// Generate multi-chain addresses from private key
async generateWallet()
// Import from BTC/FLO private key or ADA Root Key
async importFromKey(input)
// Import from BTC/FLO and generate ADA Root Key
async importFromBtcFlo(key)
// Import from ADA Root Key (128-byte hex)
async importFromRootKey(rootKeyHex)
// Get spend private key for transaction signing
async getSpendPrivateKey(rootKeyHex)
```
#### Supported Private Key Formats
- **ADA Root Key**: 256-character hexadecimal (128 bytes)
- **BTC/FLO**: WIF format (51-52 characters, Base58 encoded)
- **Hex Private Key**: 64-character hexadecimal (32 bytes)
#### Derivation Path
The wallet uses the **CIP-1852** standard derivation path:
- **Spending Key**: `m/1852'/1815'/0'/0/0`
- **Staking Key**: `m/1852'/1815'/0'/2/0`
This ensures compatibility with other Cardano wallets like Daedalus, Yoroi, and Nami.
#### Address Generation Process
```
BTC/FLO Private Key (32 bytes)
Extended Key (64 bytes) - Duplicate private key
SHA-512 Hash → Second Half (64 bytes)
ADA Root Key (128 bytes) - Combine extended key + hash
CIP-1852 Derivation → Spend Key + Stake Key
Blake2b-256 Hashing → Payment Hash + Stake Hash
Bech32 Encoding → Cardano Address (addr1...)
```
### 2. Blockchain API Layer (`cardanoBlockchainAPI.js`)
Handles all blockchain interactions using dual API architecture for optimal performance and reliability.
#### API Architecture
- **Ogmios RPC**: UTXO queries, protocol parameters, transaction submission
- **CardanoScan API**: Balance queries, transaction history, transaction details
#### Core Functions
```javascript
// Balance retrieval (CardanoScan)
async getBalance(address)
// Transaction history with pagination (CardanoScan)
async getHistory(address, pageNo, limit, order)
// Transaction details by hash (CardanoScan)
async getTransaction(hash)
// UTXO retrieval (Ogmios)
async getUtxos(address)
// Protocol parameters (Ogmios)
async getProtocolParameters()
// Current blockchain slot (Ogmios)
async getCurrentSlot()
// Fee estimation
async estimateFee(senderAddress, recipientAddress, amountLovelace)
// Send ADA transaction
async sendAda(senderPrivKeyHex, senderAddress, recipientAddress, amountLovelace)
```
#### RPC Configuration
```javascript
// Ogmios JSON-RPC endpoint (GetBlock)
const rpcUrl = "https://go.getblock.io/.../ext/bc/C/rpc";
// CardanoScan API
const cardanoscanBaseUrl = "https://api.cardanoscan.io/api/v1";
```
#### Transaction Building Process
```
1. Fetch UTXOs (Ogmios)
2. Fetch Protocol Parameters (Ogmios)
3. Get Current Slot for TTL (Ogmios)
4. Select Inputs (Coin Selection)
5. Build Transaction Body
6. Calculate Fees (Linear Fee Formula)
7. Add Change Output
8. Sign Transaction (Ed25519)
9. Submit to Network (Ogmios)
```
#### Fee Calculation
Cardano uses a linear fee formula:
```
fee = a × size + b
Where:
- a = minFeeCoefficient (typically 44)
- b = minFeeConstant (typically 155,381 lovelace)
- size = transaction size in bytes
```
### 3. Data Persistence (`cardanoSearchDB.js`)
IndexedDB wrapper for persistent storage of searched addresses and metadata.
#### Database Schema
```javascript
// Database: CardanoWalletDB
// Object Store: searchedAddresses
{
address: string (Primary Key), // Cardano address
balance: number, // Balance in ADA
timestamp: number (Indexed), // Last updated timestamp
formattedBalance: string, // "X.XXXXXX ADA"
sourceInfo: string | null, // 'address', 'btc-key', 'flo-key', etc.
addresses: { // Multi-chain addresses
BTC: { address, privateKey },
FLO: { address, privateKey },
Cardano: { address, rootKey, ... }
} | null
}
```
#### API Methods
```javascript
class CardanoSearchDB {
async init()
async saveSearchedAddress(cardanoAddress, balance, timestamp, sourceInfo, addresses)
async updateBalance(cardanoAddress, balance, timestamp)
async getSearchedAddresses()
async deleteSearchedAddress(cardanoAddress)
async clearAllSearchedAddresses()
}
```
### 4. Main Application (`main.js`)
Central controller managing UI interactions, wallet operations, and transaction flows.
#### Key Features
- **Wallet Generation**: Create new multi-chain wallets
- **Wallet Recovery**: Restore from private keys
- **Transaction Management**: Send ADA with fee estimation
- **Search Functionality**: Address and transaction hash lookup
- **URL Parameter Support**: Direct linking to addresses/transactions
- **Theme Management**: Dark/light mode with persistence
- **Input Validation**: Comprehensive validation for all inputs
## API Reference
### Wallet Generation
#### `generateWallet()`
Generates a new multi-chain wallet with random private keys.
**Returns:** Promise resolving to wallet object
```javascript
{
BTC: { address: string, privateKey: string },
FLO: { address: string, privateKey: string },
Cardano: {
address: string, // Bech32 address (addr1...)
rootKey: string, // 128-byte hex
spendKeyBech32: string, // Bech32 encoded spend key
stakeKeyBech32: string // Bech32 encoded stake key
},
cardanoRootKey: string, // 128-byte hex (for backup)
originalKey: string // 32-byte hex (original BTC/FLO key)
}
```
### Address Recovery
#### `importFromKey(input)`
Recovers wallet addresses from an existing private key.
**Parameters:**
- `input` (string): Valid ADA Root Key (256 hex), BTC/FLO WIF, or hex private key
**Returns:** Promise resolving to wallet object (same structure as generateWallet)
**Auto-Detection:**
- 256 hex chars → ADA Root Key import
- 51-52 chars Base58 → BTC/FLO WIF import
- 64 hex chars → Hex private key import
### Transaction Management
#### `searchTransactions(page)`
Loads transaction history for a given address with smart pagination and filtering.
**Process Flow:**
1. Input validation (address/private key/transaction hash)
2. Address derivation (if private key provided)
3. Balance retrieval (CardanoScan API)
4. Transaction history fetching (paginated, 10 per page)
5. Filter application (All/Received/Sent)
6. Pagination setup
7. UI updates and data persistence
**Supported Filters:**
- **All**: Show all transactions
- **Received**: Only incoming transactions (positive netAmount)
- **Sent**: Only outgoing transactions (negative netAmount)
#### `sendAda()`
Prepares and broadcasts a transaction to the Cardano network.
**Parameters:**
- `senderPrivKeyHex` (string): Sender's private key (64-byte hex)
- `senderAddress` (string): Sender's Cardano address
- `recipientAddress` (string): Recipient's Cardano address
- `amountLovelace` (string): Amount in Lovelace (1 ADA = 1,000,000 Lovelace)
**Process:**
```
Input Validation → Fee Estimation → User Confirmation →
UTXO Selection → Transaction Building → Signing → Broadcast
```
**Validations:**
- Minimum UTXO value check (~1 ADA)
- Sufficient balance verification
- Address format validation
- TTL (Time-To-Live) calculation (2 hours from current slot)
### Search Functionality
#### `handleSearch()`
Unified search handler supporting both address and transaction hash lookup.
**Search Types:**
- `address`: Loads balance and transaction history
- `hash`: Retrieves transaction details from blockchain
**Input Auto-Detection:**
- Cardano address: `addr1...` (50+ characters)
- Private key: 64/256 hex, or WIF format
- Transaction hash: 64 hex characters
#### URL Parameter Support
- `?address=addr1...` - Direct address loading
- `?hash=0x...` - Direct transaction hash loading
## Security Features
### Private Key Handling
- **No Storage**: Private keys are never stored in any form (localStorage, IndexedDB, etc.)
- **Memory Clearing**: Variables containing keys are nullified after use
- **Input Validation**: Strict format validation before processing
- **Error Handling**: Secure error messages without key exposure
- **Local Processing**: All cryptographic operations happen client-side
### URL Security
- **Address-Only URLs**: Only public addresses included in shareable URLs
- **No Private Data**: Private keys never included in URL parameters
- **State Management**: Secure browser history handling
### Transaction Security
- **Confirmation Modal**: User must confirm all transaction details
- **Fee Display**: Clear display of network fees before sending
- **Irreversibility Warning**: Users are warned that transactions cannot be reversed
- **Balance Verification**: Ensures sufficient funds before broadcasting
## Performance Optimizations
### Smart Pagination
```javascript
// Initial load: Fetch 10 transactions per page
// Navigate between pages with cached data
// Filter transactions client-side for instant response
const PAGE_LIMIT = 10;
const transactions = await getHistory(address, currentPage, PAGE_LIMIT);
```
### Caching Strategy
- **Transaction Cache**: Store transaction data for fast pagination
- **Balance Cache**: Cache balance data in IndexedDB with timestamps
- **Address History**: Persistent search history with last updated time
- **Protocol Parameters**: Cache protocol params to reduce RPC calls
### UI Optimizations
- **Lazy Loading**: Progressive content loading
- **Debounced Inputs**: Prevent excessive API calls
- **Responsive Images**: Optimized for mobile devices
- **CSS Grid/Flexbox**: Efficient layout rendering
- **Loading States**: Clear feedback during async operations
## File Structure
```
cardano-wallet/
├── index.html # Main application HTML
├── lib.cardano.js # External libraries (Bitcoin.js, Cardano libs)
├── vite.config.js # Vite build configuration
├── package.json # Dependencies
├── src/
│ ├── main.js # Main application controller
│ ├── style.css # Application styles
│ └── lib/
│ ├── cardanoCrypto.js # Cryptographic functions
│ ├── cardanoBlockchainAPI.js # Blockchain integration
│ └── cardanoSearchDB.js # Data persistence
├── public/ # Static assets
└── dist/ # Production build output
```
## Dependencies
### Core Libraries
- **@emurgo/cardano-serialization-lib-browser**: Transaction building and signing
- **cardano-crypto.js**: Key derivation and cryptographic operations
- **bech32**: Address encoding/decoding
- **buffer**: Node.js Buffer polyfill for browser
### Build Tools
- **Vite**: Fast build tool and dev server
- **@vitejs/plugin-node-polyfills**: Node.js polyfills for browser

Binary file not shown.

123
assets/index-BsM49tGo.js Normal file
View File

@ -0,0 +1,123 @@
function N(d, w) {
for (var i = 0; i < w.length; i++) {
const g = w[i];
if (typeof g != "string" && !Array.isArray(g)) {
for (const h in g) if (h !== "default" && !(h in d)) {
const x = Object.getOwnPropertyDescriptor(g, h);
x && Object.defineProperty(d, h, x.get ? x : { enumerable: true, get: () => g[h] });
}
}
}
return Object.freeze(Object.defineProperty(d, Symbol.toStringTag, { value: "Module" }));
}
var y = {}, O;
function P() {
if (O) return y;
O = 1, Object.defineProperty(y, "__esModule", { value: true }), y.bech32m = y.bech32 = void 0;
const d = "qpzry9x8gf2tvdw0s3jn54khce6mua7l", w = {};
for (let t = 0; t < d.length; t++) {
const e = d.charAt(t);
w[e] = t;
}
function i(t) {
const e = t >> 25;
return (t & 33554431) << 5 ^ -(e >> 0 & 1) & 996825010 ^ -(e >> 1 & 1) & 642813549 ^ -(e >> 2 & 1) & 513874426 ^ -(e >> 3 & 1) & 1027748829 ^ -(e >> 4 & 1) & 705979059;
}
function g(t) {
let e = 1;
for (let o = 0; o < t.length; ++o) {
const s = t.charCodeAt(o);
if (s < 33 || s > 126) return "Invalid prefix (" + t + ")";
e = i(e) ^ s >> 5;
}
e = i(e);
for (let o = 0; o < t.length; ++o) {
const s = t.charCodeAt(o);
e = i(e) ^ s & 31;
}
return e;
}
function h(t, e, o, s) {
let p = 0, l = 0;
const r = (1 << o) - 1, c = [];
for (let n = 0; n < t.length; ++n) for (p = p << e | t[n], l += e; l >= o; ) l -= o, c.push(p >> l & r);
if (s) l > 0 && c.push(p << o - l & r);
else {
if (l >= e) return "Excess padding";
if (p << o - l & r) return "Non-zero padding";
}
return c;
}
function x(t) {
return h(t, 8, 5, true);
}
function j(t) {
const e = h(t, 5, 8, false);
if (Array.isArray(e)) return e;
}
function k(t) {
const e = h(t, 5, 8, false);
if (Array.isArray(e)) return e;
throw new Error(e);
}
function E(t) {
let e;
t === "bech32" ? e = 1 : e = 734539939;
function o(r, c, n) {
if (n = n || 90, r.length + 7 + c.length > n) throw new TypeError("Exceeds length limit");
r = r.toLowerCase();
let u = g(r);
if (typeof u == "string") throw new Error(u);
let b = r + "1";
for (let f = 0; f < c.length; ++f) {
const a = c[f];
if (a >> 5 !== 0) throw new Error("Non 5-bit word");
u = i(u) ^ a, b += d.charAt(a);
}
for (let f = 0; f < 6; ++f) u = i(u);
u ^= e;
for (let f = 0; f < 6; ++f) {
const a = u >> (5 - f) * 5 & 31;
b += d.charAt(a);
}
return b;
}
function s(r, c) {
if (c = c || 90, r.length < 8) return r + " too short";
if (r.length > c) return "Exceeds length limit";
const n = r.toLowerCase(), u = r.toUpperCase();
if (r !== n && r !== u) return "Mixed-case string " + r;
r = n;
const b = r.lastIndexOf("1");
if (b === -1) return "No separator character for " + r;
if (b === 0) return "Missing prefix for " + r;
const f = r.slice(0, b), a = r.slice(b + 1);
if (a.length < 6) return "Data too short";
let A = g(f);
if (typeof A == "string") return A;
const _ = [];
for (let m = 0; m < a.length; ++m) {
const C = a.charAt(m), v = w[C];
if (v === void 0) return "Unknown character " + C;
A = i(A) ^ v, !(m + 6 >= a.length) && _.push(v);
}
return A !== e ? "Invalid checksum for " + r : { prefix: f, words: _ };
}
function p(r, c) {
const n = s(r, c);
if (typeof n == "object") return n;
}
function l(r, c) {
const n = s(r, c);
if (typeof n == "object") return n;
throw new Error(n);
}
return { decodeUnsafe: p, decode: l, encode: o, toWords: x, fromWordsUnsafe: j, fromWords: k };
}
return y.bech32 = E("bech32"), y.bech32m = E("bech32m"), y;
}
var D = P();
const z = N({ __proto__: null }, [D]);
export {
z as i
};

File diff suppressed because one or more lines are too long

38422
assets/index-LR75N_xu.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,8 @@
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<link rel="stylesheet" href="./src/style.css" />
<script type="module" crossorigin src="./assets/index-LR75N_xu.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-C4oHu9vm.css">
</head>
<body>
<!-- Loading Screen -->
@ -515,8 +516,5 @@
</div>
</div>
<script src="./lib.cardano.js"></script>
<script type="module" src="./src/lib/cardanoCrypto.js"></script>
<script type="module" src="./src/lib/cardanoBlockchainAPI.js"></script>
<script type="module" src="./src/main.js"></script>
</body>
</html>

4168
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
{
"name": "cardano-wallet",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist"
},
"devDependencies": {
"gh-pages": "^6.3.0",
"vite": "^7.2.2",
"vite-plugin-node-polyfills": "^0.24.0",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0"
},
"dependencies": {
"@emurgo/cardano-serialization-lib-browser": "^15.0.3",
"bech32": "^2.0.0",
"bip32": "^5.0.0",
"bip39": "^3.1.0",
"bitcoinjs-lib": "^7.0.0",
"blakejs": "^1.2.1",
"buffer": "^6.0.3",
"cardano-crypto.js": "^6.1.2",
"tiny-secp256k1": "^2.2.4"
}
}

View File

@ -1,869 +0,0 @@
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-browser';
import * as CardanoLib from 'cardano-crypto.js';
class CardanoAPI {
constructor(cardanoscanApiKey = null) {
// GetBlock Ogmios JSON-RPC endpoint (for UTXOs and transactions only)
this.rpcUrl = "https://go.getblock.io/9260a43da7164b6fa58ac6a54fee2d21";
// CardanoScan API configuration
this.cardanoscanApiKey = cardanoscanApiKey;
this.cardanoscanBaseUrl = "https://api.cardanoscan.io/api/v1";
this.useCardanoScan = !!cardanoscanApiKey;
}
/**
* Convert Bech32 address to full hex bytes
* CardanoScan expects the FULL address bytes (including network tag),
* not just the payment credential hash
* @param {string} address - Bech32 address (addr1...)
* @returns {string} Full address in hex format (e.g., 0193a4ef...)
*/
addressToHex(address) {
try {
const CSL = CardanoWasm;
const addr = CSL.Address.from_bech32(address);
// Return the full address bytes as hex
// This includes the network tag (01 for mainnet base address)
// plus payment credential and staking credential
const addressBytes = addr.to_bytes();
const hexAddress = Buffer.from(addressBytes).toString('hex');
return hexAddress;
} catch (error) {
console.error('[CardanoAPI] Error converting address to hex:', error);
// If conversion fails, return the address as-is
return address;
}
}
/**
* Convert Hex address to Bech32
* @param {string} hexAddress
* @returns {string} Bech32 address
*/
hexToAddress(hexAddress) {
try {
if (hexAddress.startsWith('addr')) return hexAddress;
const CSL = CardanoWasm;
const bytes = Buffer.from(hexAddress, 'hex');
const addr = CSL.Address.from_bytes(bytes);
return addr.to_bech32();
} catch (error) {
return hexAddress;
}
}
/**
* Call CardanoScan API
* @param {string} endpoint - API endpoint (e.g., '/address/balance')
* @param {object} params - Query parameters
*/
async callCardanoScan(endpoint, params = {}) {
if (!this.cardanoscanApiKey) {
throw new Error("CardanoScan API key not configured");
}
const url = new URL(this.cardanoscanBaseUrl + endpoint);
Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, params[key]);
}
});
try {
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"apiKey": this.cardanoscanApiKey,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP Error: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`CardanoScan API Error: ${data.error}`);
}
return data;
} catch (error) {
console.error(`[CardanoScan] Error calling ${endpoint}:`, error);
throw error;
}
}
/**
* Generic JSON-RPC call to Ogmios
* @param {string} method - RPC method name
* @param {object} params - RPC parameters
*/
async callRpc(method, params = null) {
const payload = {
jsonrpc: "2.0",
method: method,
id: Date.now(),
};
if (params !== null && params !== undefined && Object.keys(params).length > 0) {
payload.params = params;
}
try {
const response = await fetch(this.rpcUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[CardanoAPI] HTTP ${response.status} response:`, errorText);
throw new Error(`HTTP Error: ${response.status} ${response.statusText} - ${errorText}`);
}
const data = await response.json();
if (data.error) {
console.error(`[CardanoAPI] RPC Error:`, data.error);
throw new Error(`RPC Error: ${JSON.stringify(data.error)}`);
}
return data.result;
} catch (error) {
console.error(`[CardanoAPI] Error calling ${method}:`, error);
throw error;
}
}
/**
* Get balance for an address (in Lovelace)
* Uses CardanoScan API exclusively
*/
async getBalance(address) {
if (!this.useCardanoScan) {
throw new Error("CardanoScan API key not configured");
}
// Convert to hex to support all address types (Base, Enterprise, etc.)
const hexAddress = this.addressToHex(address);
const result = await this.callCardanoScan('/address/balance', { address: hexAddress });
return result.balance || "0";
}
/**
* Get transaction history for an address
* Uses CardanoScan API exclusively
* @param {string} address - Cardano address (Bech32 format)
* @param {number} pageNo - Page number (default: 1)
* @param {number} limit - Results per page (default: 20, max: 50)
* @param {string} order - Sort order: 'asc' or 'desc' (default: 'desc')
*/
async getHistory(address, pageNo = 1, limit = 20, order = 'desc') {
if (!this.useCardanoScan) {
throw new Error("CardanoScan API key not configured");
}
// Convert address to hex for transaction list endpoint
const hexAddress = this.addressToHex(address);
const result = await this.callCardanoScan('/transaction/list', {
address: hexAddress,
pageNo,
limit,
order
});
// Transform CardanoScan response to our format
const transactions = result.transactions || [];
return transactions.map(tx => ({
txHash: tx.hash,
blockHash: tx.blockHash,
fees: tx.fees,
slot: tx.slot,
epoch: tx.epoch,
blockHeight: tx.blockHeight,
absSlot: tx.absSlot,
timestamp: tx.timestamp,
index: tx.index,
inputs: tx.inputs || [],
outputs: tx.outputs || [],
netAmount: this.calculateNetAmount(tx, address)
}));
}
/**
* Get transaction details by hash with status (confirmed/pending/failed)
* Uses CardanoScan API exclusively with fallback for pending detection
* @param {string} hash - Transaction hash
* @returns {object} Transaction details with status
*/
async getTransaction(hash) {
if (!this.useCardanoScan) {
throw new Error("CardanoScan API key not configured");
}
try {
// Try to get transaction from CardanoScan (confirmed transactions)
const result = await this.callCardanoScan('/transaction', { hash });
// If we get here, transaction is confirmed on-chain
return {
...result,
status: 'confirmed',
statusLabel: 'Confirmed',
statusColor: '#28a745'
};
} catch (error) {
console.log(`[CardanoAPI] Transaction ${hash} not found in CardanoScan, checking status...`);
return {
hash: hash,
status: 'pending',
statusLabel: 'Pending or Failed',
statusColor: '#ffc107', // warning
message: 'Transaction not yet confirmed on-chain. It may still be pending in the mempool, or it may have failed/expired. Please check again in a few minutes.',
timestamp: null,
blockHeight: null,
inputs: [],
outputs: [],
fees: null,
error: error.message
};
}
}
/**
* Calculate net amount for an address in a transaction
* @param {object} tx - Transaction object from CardanoScan
* @param {string} address - Address to calculate for (Bech32 format)
* @returns {string} Net amount (positive for incoming, negative for outgoing)
*/
calculateNetAmount(tx, address) {
let totalIn = 0n;
let totalOut = 0n;
// Convert address to hex for comparison
const hexAddress = this.addressToHex(address);
// Sum inputs from this address
if (tx.inputs) {
for (const input of tx.inputs) {
// Compare both hex and bech32 formats
if (input.address === address || input.address === hexAddress) {
totalIn += BigInt(input.value || 0);
}
}
}
// Sum outputs to this address
if (tx.outputs) {
for (const output of tx.outputs) {
// Compare both hex and bech32 formats
if (output.address === address || output.address === hexAddress) {
totalOut += BigInt(output.value || 0);
}
}
}
const net = totalOut - totalIn;
return net.toString();
}
/**
* Fetch UTXOs for an address (using Ogmios)
*/
async getUtxos(address) {
try {
const result = await this.callRpc("queryLedgerState/utxo", {
addresses: [address]
});
return this.parseUtxoResult(result);
} catch (e) {
console.warn("queryLedgerState/utxo failed, trying 'query' method...", e);
const result = await this.callRpc("query", {
query: "utxo",
arg: { addresses: [address] }
});
return this.parseUtxoResult(result);
}
}
parseUtxoResult(result) {
console.log("Parsing UTXO result:", JSON.stringify(result, null, 2));
const utxos = [];
if (Array.isArray(result)) {
for (const item of result) {
// GetBlock/Ogmios direct format
if (item.transaction && item.transaction.id !== undefined && item.index !== undefined) {
utxos.push({
txHash: item.transaction.id,
index: item.index,
value: item.value,
address: item.address
});
}
// Standard Ogmios array of pairs
else if (Array.isArray(item) && item.length === 2) {
const [input, output] = item;
utxos.push({
txHash: input.txId,
index: input.index,
value: output.value,
address: output.address
});
}
}
} else if (typeof result === 'object' && result !== null) {
//Object with "txId#index" keys
console.log("Received object result for UTXOs, parsing keys...");
for (const [key, output] of Object.entries(result)) {
const parts = key.split('#');
if (parts.length === 2) {
const [txHash, indexStr] = parts;
const index = parseInt(indexStr, 10);
utxos.push({
txHash: txHash,
index: index,
value: output.value,
address: output.address
});
}
}
console.log(`Parsed ${utxos.length} UTXOs from object format`);
}
console.log("Final parsed UTXOs:", utxos);
return utxos;
}
/**
* Get protocol parameters from Ogmios
*/
async getProtocolParameters() {
try {
// Try queryLedgerState/protocolParameters first
try {
const result = await this.callRpc("queryLedgerState/protocolParameters");
return result;
} catch (e) {
console.warn("queryLedgerState/protocolParameters failed, trying 'query' method...", e);
// Fallback to 'query' method
const result = await this.callRpc("query", {
query: "protocolParameters"
});
return result;
}
} catch (e) {
console.error("Failed to fetch protocol parameters:", e);
throw new Error("Could not fetch protocol parameters needed for transaction.");
}
}
/**
* Get current tip (latest block slot) from Ogmios
* Used for calculating transaction TTL
*/
async getCurrentSlot() {
try {
const tip = await this.callRpc("queryNetwork/tip");
// Handle different response formats
if (tip && tip.slot !== undefined) {
return tip.slot;
} else if (Array.isArray(tip) && tip.length > 0 && tip[0].slot !== undefined) {
return tip[0].slot;
}
throw new Error("Unable to parse current slot from tip response");
} catch (error) {
console.error("[CardanoAPI] Error fetching current slot:", error);
throw new Error("Failed to fetch current slot for TTL calculation");
}
}
/**
* Estimate transaction fee by building the transaction without submitting
* @param {string} senderAddress - Bech32 address
* @param {string} recipientAddress - Bech32 address
* @param {string|BigInt} amountLovelace - Amount in Lovelace
* @returns {object} Fee estimation details
*/
async estimateFee(senderAddress, recipientAddress, amountLovelace) {
try {
console.log("[CardanoAPI] Estimating transaction fee...");
// Fetch required data
const [protocolParams, utxos] = await Promise.all([
this.getProtocolParameters(),
this.getUtxos(senderAddress)
]);
if (utxos.length === 0) {
throw new Error("No UTXOs found. Balance is 0.");
}
// Calculate total balance
let totalBalance = 0n;
for (const utxo of utxos) {
if (utxo.value?.ada?.lovelace) {
totalBalance += BigInt(utxo.value.ada.lovelace);
}
}
// Parse protocol parameters
const minFeeA = protocolParams.minFeeCoefficient || 44;
const minFeeB = protocolParams.minFeeConstant?.ada?.lovelace || protocolParams.minFeeConstant || 155381;
// Calculate how many inputs we'll need
let inputsNeeded = 0;
let accumulatedInput = 0n;
const estimatedFeePerInput = 200000n; // Conservative estimate
const targetAmount = BigInt(amountLovelace) + estimatedFeePerInput;
for (const utxo of utxos) {
let utxoValue = 0n;
if (utxo.value?.ada?.lovelace) {
utxoValue = BigInt(utxo.value.ada.lovelace);
}
accumulatedInput += utxoValue;
inputsNeeded++;
if (accumulatedInput >= targetAmount) {
break;
}
}
// Estimate transaction size based on inputs/outputs
// Formula: ~150 bytes base + ~150 bytes per input + ~50 bytes per output
const willHaveChange = (accumulatedInput - BigInt(amountLovelace)) >= 1000000n;
const outputCount = willHaveChange ? 2 : 1; // recipient + change (if any)
const estimatedSize = 150 + (inputsNeeded * 150) + (outputCount * 50);
// Calculate fee using Cardano formula: fee = a * size + b
const estimatedFee = BigInt(minFeeA) * BigInt(estimatedSize) + BigInt(minFeeB);
// Calculate change
const change = accumulatedInput - BigInt(amountLovelace) - estimatedFee;
return {
success: true,
fee: estimatedFee.toString(),
feeAda: (Number(estimatedFee) / 1000000).toFixed(6),
amount: amountLovelace.toString(),
amountAda: (Number(amountLovelace) / 1000000).toFixed(6),
totalCost: (BigInt(amountLovelace) + estimatedFee).toString(),
totalCostAda: (Number(BigInt(amountLovelace) + estimatedFee) / 1000000).toFixed(6),
change: change.toString(),
changeAda: (Number(change) / 1000000).toFixed(6),
balance: totalBalance.toString(),
balanceAda: (Number(totalBalance) / 1000000).toFixed(6),
inputsNeeded,
outputCount,
estimatedSize,
sufficientBalance: totalBalance >= (BigInt(amountLovelace) + estimatedFee)
};
} catch (error) {
console.error("[CardanoAPI] Fee estimation failed:", error);
return {
success: false,
error: error.message
};
}
}
/**
* Send ADA
* Uses TransactionBuilder for proper fee calculation and change handling
* @param {string} senderPrivKeyHex - Hex private key (extended or normal)
* @param {string} senderAddress - Bech32 address
* @param {string} recipientAddress - Bech32 address
* @param {string|BigInt} amountLovelace - Amount in Lovelace
* @returns {object} Transaction submission result with txId
*/
async sendAda(senderPrivKeyHex, senderAddress, recipientAddress, amountLovelace) {
console.log("[CardanoAPI] Initiating Send ADA transaction...");
console.log(` From: ${senderAddress}`);
console.log(` To: ${recipientAddress}`);
console.log(` Amount: ${amountLovelace} lovelace`);
const CSL = CardanoWasm;
if (!CSL) {
throw new Error("Cardano Serialization Library not loaded.");
}
try {
// Fetch required data in parallel
console.log("[CardanoAPI] Fetching protocol parameters, UTXOs, and current slot...");
const [protocolParams, utxos, currentSlot] = await Promise.all([
this.getProtocolParameters(),
this.getUtxos(senderAddress),
this.getCurrentSlot()
]);
if (utxos.length === 0) {
throw new Error("No UTXOs found. Your balance is 0 or address has no funds.");
}
console.log(`[CardanoAPI] Found ${utxos.length} UTXOs, current slot: ${currentSlot}`);
// Calculate total available balance
let totalBalance = 0n;
for (const utxo of utxos) {
if (utxo.value?.ada?.lovelace) {
totalBalance += BigInt(utxo.value.ada.lovelace);
} else if (utxo.value?.lovelace) {
totalBalance += BigInt(utxo.value.lovelace);
} else if (utxo.value?.coins) {
totalBalance += BigInt(utxo.value.coins);
}
}
console.log(`[CardanoAPI] Total balance: ${totalBalance} lovelace (${(Number(totalBalance) / 1000000).toFixed(2)} ADA)`);
// Estimate fee (conservative estimate: ~0.2 ADA)
const estimatedFee = 200000n;
const totalNeeded = BigInt(amountLovelace) + estimatedFee;
console.log(`[CardanoAPI] Amount to send: ${amountLovelace} lovelace (${(Number(amountLovelace) / 1000000).toFixed(2)} ADA)`);
console.log(`[CardanoAPI] Estimated fee: ${estimatedFee} lovelace (~0.2 ADA)`);
console.log(`[CardanoAPI] Total needed: ${totalNeeded} lovelace (${(Number(totalNeeded) / 1000000).toFixed(2)} ADA)`);
// Check if balance is sufficient
if (totalBalance < totalNeeded) {
const shortfall = totalNeeded - totalBalance;
throw new Error(
`Insufficient balance! ` +
`You have ${totalBalance} lovelace (${(Number(totalBalance) / 1000000).toFixed(2)} ADA), ` +
`but need ${totalNeeded} lovelace (${(Number(totalNeeded) / 1000000).toFixed(2)} ADA) ` +
`for ${amountLovelace} lovelace + ~${estimatedFee} lovelace fee. ` +
`You're short by ${shortfall} lovelace (${(Number(shortfall) / 1000000).toFixed(2)} ADA).`
);
}
console.log(`[CardanoAPI] ✅ Balance check passed`);
//Parse protocol parameters (handle nested Ogmios structure)
const minFeeA = protocolParams.minFeeCoefficient || 44;
const minFeeB = protocolParams.minFeeConstant?.ada?.lovelace ||
protocolParams.minFeeConstant || 155381;
const maxTxSize = protocolParams.maxTransactionSize?.bytes ||
protocolParams.maxTxSize || 16384;
const utxoCostPerByte = protocolParams.utxoCostPerByte?.ada?.lovelace ||
protocolParams.coinsPerUtxoByte || 4310;
const poolDeposit = protocolParams.stakePoolDeposit?.ada?.lovelace ||
protocolParams.poolDeposit || 500000000;
const keyDeposit = protocolParams.stakeCredentialDeposit?.ada?.lovelace ||
protocolParams.keyDeposit || 2000000;
console.log("[CardanoAPI] Protocol parameters:", {
minFeeA,
minFeeB,
maxTxSize,
utxoCostPerByte,
poolDeposit,
keyDeposit
});
// Configure TransactionBuilder
const txBuilderConfig = CSL.TransactionBuilderConfigBuilder.new()
.fee_algo(
CSL.LinearFee.new(
CSL.BigNum.from_str(minFeeA.toString()),
CSL.BigNum.from_str(minFeeB.toString())
)
)
.coins_per_utxo_byte(CSL.BigNum.from_str(utxoCostPerByte.toString()))
.pool_deposit(CSL.BigNum.from_str(poolDeposit.toString()))
.key_deposit(CSL.BigNum.from_str(keyDeposit.toString()))
.max_tx_size(maxTxSize)
.max_value_size(5000)
.build();
const txBuilder = CSL.TransactionBuilder.new(txBuilderConfig);
// Validate minimum UTXO value
const recipientAddr = CSL.Address.from_bech32(recipientAddress);
const outputValue = CSL.Value.new(CSL.BigNum.from_str(amountLovelace.toString()));
// Calculate approximate minimum ADA required for a standard output
// Formula: utxoCostPerByte × estimatedOutputSize
// Standard output (address + ADA value) ≈ 225 bytes
const estimatedOutputSize = 225;
const minAdaRequired = BigInt(utxoCostPerByte) * BigInt(estimatedOutputSize);
console.log(`[CardanoAPI] Minimum ADA required for output: ${minAdaRequired} lovelace (~${(Number(minAdaRequired) / 1000000).toFixed(2)} ADA)`);
// Validate amount meets minimum
if (BigInt(amountLovelace) < minAdaRequired) {
const minAdaInAda = (Number(minAdaRequired) / 1000000).toFixed(2);
const requestedInAda = (Number(amountLovelace) / 1000000).toFixed(2);
throw new Error(
`Amount too small! Cardano requires a minimum of ${minAdaRequired} lovelace (~${minAdaInAda} ADA) per UTXO. ` +
`You tried to send ${amountLovelace} lovelace (~${requestedInAda} ADA). ` +
`Please send at least ${minAdaInAda} ADA.`
);
}
console.log(`[CardanoAPI] ✅ Amount ${amountLovelace} lovelace meets minimum requirement`);
// Use TransactionOutputBuilder for better compatibility
let output;
try {
output = CSL.TransactionOutputBuilder.new()
.with_address(recipientAddr)
.next()
.with_value(outputValue)
.build();
console.log(`[CardanoAPI] Output created using TransactionOutputBuilder`);
} catch (e) {
console.log(`[CardanoAPI] Falling back to TransactionOutput.new()`);
output = CSL.TransactionOutput.new(recipientAddr, outputValue);
}
txBuilder.add_output(output);
console.log(`[CardanoAPI] Added output: ${amountLovelace} lovelace to ${recipientAddress}`);
// Add inputs (UTXOs) using TransactionUnspentOutputs
const txUnspentOutputs = CSL.TransactionUnspentOutputs.new();
for (const utxo of utxos) {
try {
// Parse UTXO amount
let lovelaceAmount = 0n;
if (utxo.value?.ada?.lovelace) {
lovelaceAmount = BigInt(utxo.value.ada.lovelace);
} else if (utxo.value?.lovelace) {
lovelaceAmount = BigInt(utxo.value.lovelace);
} else if (utxo.value?.coins) {
lovelaceAmount = BigInt(utxo.value.coins);
} else {
console.warn("[CardanoAPI] Skipping UTXO with unknown value format:", utxo);
continue;
}
// Create TransactionInput
const txHash = CSL.TransactionHash.from_bytes(
Buffer.from(utxo.txHash, "hex")
);
const txInput = CSL.TransactionInput.new(txHash, utxo.index);
// Create TransactionOutput for this UTXO
const utxoAddr = CSL.Address.from_bech32(utxo.address || senderAddress);
const utxoValue = CSL.Value.new(CSL.BigNum.from_str(lovelaceAmount.toString()));
// Use TransactionOutputBuilder for compatibility
let utxoOutput;
try {
utxoOutput = CSL.TransactionOutputBuilder.new()
.with_address(utxoAddr)
.next()
.with_value(utxoValue)
.build();
} catch (e) {
utxoOutput = CSL.TransactionOutput.new(utxoAddr, utxoValue);
}
// Create TransactionUnspentOutput
const txUnspentOutput = CSL.TransactionUnspentOutput.new(txInput, utxoOutput);
txUnspentOutputs.add(txUnspentOutput);
} catch (error) {
console.warn("[CardanoAPI] Error processing UTXO, skipping:", error, utxo);
}
}
if (txUnspentOutputs.len() === 0) {
throw new Error("No valid UTXOs could be processed");
}
console.log(`[CardanoAPI] Prepared ${txUnspentOutputs.len()} UTXOs for input selection`);
// Manual input selection for better control and correct fee calculation
const senderAddr = CSL.Address.from_bech32(senderAddress);
console.log(`[CardanoAPI] Using manual coin selection for accurate fee calculation...`);
// Derive public key and key hash once (reused for all inputs)
const privKey = CSL.PrivateKey.from_hex(senderPrivKeyHex);
const pubKey = privKey.to_public();
const keyHash = pubKey.hash();
// Add inputs manually until we have enough
let accumulatedValue = 0n;
const targetValue = BigInt(amountLovelace) + 300000n; // amount + estimated fee buffer
for (let i = 0; i < txUnspentOutputs.len(); i++) {
const utxo = txUnspentOutputs.get(i);
const utxoValue = BigInt(utxo.output().amount().coin().to_str());
// Use add_key_input with Ed25519KeyHash
txBuilder.add_key_input(
keyHash,
utxo.input(),
utxo.output().amount()
);
accumulatedValue += utxoValue;
console.log(`[CardanoAPI] Added input ${i + 1}: ${utxoValue} lovelace (total: ${accumulatedValue} lovelace)`);
// Stop when we have enough (don't add all UTXOs unnecessarily)
if (accumulatedValue >= targetValue) {
console.log(`[CardanoAPI] ✅ Sufficient inputs added (${i + 1} UTXO${i > 0 ? 's' : ''})`);
break;
}
}
// Add change output automatically
console.log(`[CardanoAPI] Adding change output...`);
// const senderAddr = CSL.Address.from_bech32(senderAddress); // Already declared above
// Ensure currentSlot is a number
const slotNumber = Number(currentSlot);
if (isNaN(slotNumber) || slotNumber <= 0) {
throw new Error(`Invalid current slot: ${currentSlot}`);
}
// Calculate TTL (time-to-live) - 2 hours from current slot
const ttl = slotNumber + 7200; // 2 hours = 7200 slots (1 slot = 1 second)
console.log(`[CardanoAPI] Calculated TTL: ${ttl} (current slot: ${slotNumber} + 7200)`);
// Set TTL on transaction builder
// In cardano-serialization-lib v15, use set_ttl_bignum for reliability
try {
// Try set_ttl_bignum first (v15+ recommended method)
if (typeof txBuilder.set_ttl_bignum === 'function') {
txBuilder.set_ttl_bignum(CSL.BigNum.from_str(ttl.toString()));
console.log(`[CardanoAPI] ✅ TTL set using set_ttl_bignum: ${ttl}`);
} else {
// Fallback to set_ttl (older versions)
txBuilder.set_ttl(CSL.BigNum.from_str(ttl.toString()));
console.log(`[CardanoAPI] ✅ TTL set using set_ttl: ${ttl}`);
}
} catch (e) {
console.error(`[CardanoAPI] Failed to set TTL:`, e);
throw new Error(`Cannot set TTL on TransactionBuilder: ${e.message}`);
}
// This calculates the fee and adds a change output if the remainder is sufficient
const changeAdded = txBuilder.add_change_if_needed(senderAddr);
console.log(`[CardanoAPI] Change added: ${changeAdded}`);
// Build the transaction body
// Note: In v15, TransactionBody may not have a ttl() getter method,
// but the TTL is properly set internally and will be included in the serialized transaction
const txBody = txBuilder.build();
console.log(`[CardanoAPI] ✅ Transaction body built successfully`);
console.log(` Fee: ${txBody.fee().to_str()} lovelace`);
//Sign the transaction
// Create the transaction hash for signing
console.log("[CardanoAPI] Creating transaction hash for signing...");
let txHash;
try {
// Try different methods to get the transaction hash
if (typeof txBody.to_hash === 'function') {
txHash = txBody.to_hash();
console.log("[CardanoAPI] Hash created using txBody.to_hash()");
} else if (typeof CSL.hash_transaction === 'function') {
// Use hash_transaction if available
txHash = CSL.hash_transaction(txBody);
console.log("[CardanoAPI] Hash created using hash_transaction()");
} else {
// Manually create hash from transaction body bytes
console.log("[CardanoAPI] Using manual hash creation from body bytes");
const bodyBytes = txBody.to_bytes();
console.log(`[CardanoAPI] Transaction body size: ${bodyBytes.length} bytes`);
console.log("[CardanoAPI] Computing Blake2b-256 hash using CardanoLib...");
const bodyBuffer = Buffer.from(bodyBytes);
const hashBytes = CardanoLib.blake2b(bodyBuffer, 32);
txHash = CSL.TransactionHash.from_bytes(hashBytes);
console.log("[CardanoAPI] ✅ Transaction hash created successfully using Blake2b-256");
}
} catch (e) {
console.error("[CardanoAPI] Failed to create transaction hash:", e);
throw new Error(`Cannot create transaction hash: ${e.message}`);
}
console.log("[CardanoAPI] Signing transaction with private key...");
const vkeyWitnesses = CSL.Vkeywitnesses.new();
let vkeyWitness;
try {
vkeyWitness = CSL.make_vkey_witness(txHash, privKey);
console.log("[CardanoAPI] ✅ Transaction signed successfully");
} catch (e) {
console.error("[CardanoAPI] Signing failed:", e);
throw new Error(`Failed to sign transaction: ${e.message}`);
}
vkeyWitnesses.add(vkeyWitness);
const witnessSet = CSL.TransactionWitnessSet.new();
witnessSet.set_vkeys(vkeyWitnesses);
console.log("[CardanoAPI] Transaction signed successfully");
// Construct final transaction
const transaction = CSL.Transaction.new(
txBody,
witnessSet,
undefined // No auxiliary data (metadata)
);
// Serialize to CBOR hex
const signedTxBytes = transaction.to_bytes();
const signedTxHex = Buffer.from(signedTxBytes).toString("hex");
console.log(`[CardanoAPI] Transaction serialized (${signedTxBytes.length} bytes)`);
console.log(`[CardanoAPI] Transaction CBOR (first 100 chars): ${signedTxHex.substring(0, 100)}...`);
// Submit transaction to network via Ogmios
console.log("[CardanoAPI] Submitting transaction to network...");
const submitResult = await this.callRpc("submitTransaction", {
transaction: { cbor: signedTxHex }
});
console.log("[CardanoAPI] Transaction submitted successfully!");
console.log(" Result:", submitResult);
// Return transaction details
return {
success: true,
txId: submitResult?.transaction?.id || "unknown",
txHash: Buffer.from(txHash.to_bytes()).toString("hex"),
fee: txBody.fee().to_str(),
submitResult: submitResult
};
} catch (error) {
console.error("[CardanoAPI] Error in sendAda:", error);
throw new Error(`Failed to send ADA: ${error.message}`);
}
}
}
export default CardanoAPI;

View File

@ -1,314 +0,0 @@
import * as CardanoLib from "cardano-crypto.js";
import { Buffer } from "buffer";
window.cardanoCrypto = window.cardanoCrypto || {};
const cardanoCrypto = window.cardanoCrypto;
/**
* Generate a new random BTC/FLO/ADA wallet
* @returns {Promise<Object>} Wallet data with BTC, FLO, ADA addresses and Root Key
*/
cardanoCrypto.generateWallet = async function () {
function generateNewID() {
var key = new Bitcoin.ECKey(false);
key.setCompressed(true);
return {
floID: key.getBitcoinAddress(),
pubKey: key.getPubKeyHex(),
privKey: key.getBitcoinWalletImportFormat(),
};
}
const newID = generateNewID();
return await cardanoCrypto.importFromKey(newID.privKey);
};
/**
* Import from BTC/FLO private key or ADA Root Key
* @param {string} input - BTC/FLO WIF, Hex, or ADA Root Key (128-byte hex)
* @returns {Promise<Object>} Wallet data
*/
cardanoCrypto.importFromKey = async function (input) {
const trimmedInput = input.trim();
const hexOnly = /^[0-9a-fA-F]+$/.test(trimmedInput);
// Check if it's a ADA Root Key (128 bytes = 256 hex chars)
if (hexOnly && trimmedInput.length === 256) {
return await cardanoCrypto.importFromRootKey(trimmedInput);
} else {
return await cardanoCrypto.importFromBtcFlo(trimmedInput);
}
};
/**
* Import from BTC/FLO private key and generate ADA Root Key
* @param {string} key - BTC/FLO private key (WIF or Hex)
* @returns {Promise<Object>} Wallet data
*/
cardanoCrypto.importFromBtcFlo = async function (key) {
try {
// Decode private key to hex
const privKeyHex = await decodePrivateKey(key);
console.log("BTC/FLO key (32 bytes):", privKeyHex);
// Convert to bytes
const privateKeyBytes = hexToBytes(privKeyHex);
// Duplicate to create 64-byte extended key
const extendedKeyBytes = new Uint8Array(64);
extendedKeyBytes.set(privateKeyBytes, 0);
extendedKeyBytes.set(privateKeyBytes, 32);
console.log("Extended key (64 bytes):", bytesToHex(extendedKeyBytes));
// Expand to 128-byte ADA Root Key using SHA-512
const hashBuffer = await window.crypto.subtle.digest('SHA-512', extendedKeyBytes);
const secondHalf = new Uint8Array(hashBuffer);
const rootKey128 = new Uint8Array(128);
rootKey128.set(extendedKeyBytes, 0);
rootKey128.set(secondHalf, 64);
console.log("ADA Root Key (128 bytes):", bytesToHex(rootKey128));
// Derive ADA address from Root Key
const cardanoData = await deriveCardanoFromRoot(rootKey128);
// Derive BTC/FLO addresses from original key
const btcFloData = await deriveBtcFloFromKey(privKeyHex);
return {
BTC: btcFloData.BTC,
FLO: btcFloData.FLO,
Cardano: cardanoData,
cardanoRootKey: bytesToHex(rootKey128),
originalKey: privKeyHex
};
} catch (error) {
console.error("Import from BTC/FLO failed:", error);
throw new Error(`Failed to import: ${error.message}`);
}
};
/**
* Import from ADA Root Key and extract BTC/FLO keys
* @param {string} rootKeyHex - 128-byte ADA Root Key (256 hex chars)
* @returns {Promise<Object>} Wallet data
*/
cardanoCrypto.importFromRootKey = async function (rootKeyHex) {
try {
const rootKey128 = hexToBytes(rootKeyHex);
if (rootKey128.length !== 128) {
throw new Error(`Invalid Root Key length: ${rootKey128.length} bytes. Expected 128.`);
}
console.log("Cardano Root Key (128 bytes):", rootKeyHex);
// Extract original BTC/FLO key from first 32 bytes
const privateKeyBytes = rootKey128.slice(0, 32);
const privKeyHex = bytesToHex(privateKeyBytes);
console.log("Extracted BTC/FLO key (32 bytes):", privKeyHex);
// Derive ADA address from Root Key
const cardanoData = await deriveCardanoFromRoot(rootKey128);
// Derive BTC/FLO addresses from extracted key
const btcFloData = await deriveBtcFloFromKey(privKeyHex);
return {
BTC: btcFloData.BTC,
FLO: btcFloData.FLO,
Cardano: cardanoData,
cardanoRootKey: rootKeyHex,
extractedKey: privKeyHex
};
} catch (error) {
console.error("Import from ADA Root failed:", error);
throw new Error(`Failed to import: ${error.message}`);
}
};
/**
* Get the Spend Private Key (hex) from the Root Key
* Needed for signing transactions
* @param {string} rootKeyHex - 128-byte ADA Root Key
* @returns {Promise<string>} Private Key Hex (64 bytes extended or 32 bytes)
*/
cardanoCrypto.getSpendPrivateKey = async function(rootKeyHex) {
try {
const rootKey = hexToBytes(rootKeyHex);
const rootKeyBuffer = Buffer.from(rootKey);
// Derivation path: m/1852'/1815'/0'/0/0
const accountKey = CardanoLib.derivePrivate(rootKeyBuffer, 0x80000000 + 1852, 2);
const coinKey = CardanoLib.derivePrivate(accountKey, 0x80000000 + 1815, 2);
const accountIndex = CardanoLib.derivePrivate(coinKey, 0x80000000 + 0, 2);
const chainKey = CardanoLib.derivePrivate(accountIndex, 0, 2);
const spendKey = CardanoLib.derivePrivate(chainKey, 0, 2);
return bytesToHex(spendKey.slice(0, 64));
} catch (error) {
console.error("Failed to derive spend private key:", error);
throw error;
}
};
/**
* Derive ADA address from 128-byte Root Key
* @private
*/
async function deriveCardanoFromRoot(rootKey) {
// Ensure rootKey is a Buffer
const rootKeyBuffer = Buffer.from(rootKey);
// Use CIP-1852 derivation path: m/1852'/1815'/0'/0/0 (spend) and m/1852'/1815'/0'/2/0 (stake)
const accountKey = CardanoLib.derivePrivate(rootKeyBuffer, 0x80000000 + 1852, 2);
const coinKey = CardanoLib.derivePrivate(accountKey, 0x80000000 + 1815, 2);
const accountIndex = CardanoLib.derivePrivate(coinKey, 0x80000000 + 0, 2);
// Spending key
const chainKey = CardanoLib.derivePrivate(accountIndex, 0, 2);
const spendKey = CardanoLib.derivePrivate(chainKey, 0, 2);
// Staking key
const stakingChainKey = CardanoLib.derivePrivate(accountIndex, 2, 2);
const stakeKey = CardanoLib.derivePrivate(stakingChainKey, 0, 2);
// Extract public keys (32 bytes each, from offset 64-96)
const spendPubKey = spendKey.slice(64, 96);
const stakePubKey = stakeKey.slice(64, 96);
// Create payment and stake credentials
const paymentKeyHash = CardanoLib.blake2b(spendPubKey, 28);
const stakeKeyHash = CardanoLib.blake2b(stakePubKey, 28);
// Build base address (mainnet)
const networkTag = 0x01;
const header = (0b0000 << 4) | networkTag;
const addressBytes = new Uint8Array(1 + 28 + 28);
addressBytes[0] = header;
addressBytes.set(paymentKeyHash, 1);
addressBytes.set(stakeKeyHash, 29);
// Re-import bech32 here to be safe
const { bech32 } = await import("bech32");
const words = bech32.toWords(addressBytes);
const address = bech32.encode("addr", words, 1000);
// Encode keys to bech32
const spendKeyBech32 = bech32.encode("ed25519e_sk", bech32.toWords(spendKey.slice(0, 64)), 1000);
const stakeKeyBech32 = bech32.encode("ed25519e_sk", bech32.toWords(stakeKey.slice(0, 64)), 1000);
return {
address: address,
rootKey: bytesToHex(rootKey),
spendKeyBech32: spendKeyBech32,
stakeKeyBech32: stakeKeyBech32
};
}
/**
* Derive BTC and FLO addresses from private key hex
* @private
*/
async function deriveBtcFloFromKey(privKeyHex) {
const versions = {
BTC: { pub: 0x00, priv: 0x80 },
FLO: { pub: 0x23, priv: 0xa3 }
};
const origBitjsPub = bitjs.pub;
const origBitjsPriv = bitjs.priv;
const origBitjsCompressed = bitjs.compressed;
const origCoinJsCompressed = coinjs.compressed;
// Enforce compressed keys
bitjs.compressed = true;
coinjs.compressed = true;
const result = { BTC: {}, FLO: {} };
// BTC
bitjs.pub = versions.BTC.pub;
bitjs.priv = versions.BTC.priv;
const pubKeyBTC = bitjs.newPubkey(privKeyHex);
result.BTC.address = coinjs.bech32Address(pubKeyBTC).address;
result.BTC.privateKey = bitjs.privkey2wif(privKeyHex);
// FLO
bitjs.pub = versions.FLO.pub;
bitjs.priv = versions.FLO.priv;
const pubKeyFLO = bitjs.newPubkey(privKeyHex);
result.FLO.address = bitjs.pubkey2address(pubKeyFLO);
result.FLO.privateKey = bitjs.privkey2wif(privKeyHex);
// Restore original values
bitjs.pub = origBitjsPub;
bitjs.priv = origBitjsPriv;
bitjs.compressed = origBitjsCompressed;
coinjs.compressed = origCoinJsCompressed;
return result;
}
/**
* Decode private key from WIF or Hex to hex string
* @private
*/
async function decodePrivateKey(key) {
const trimmed = key.trim();
const hexOnly = /^[0-9a-fA-F]+$/.test(trimmed);
// If it's already hex (64 chars = 32 bytes)
if (hexOnly && trimmed.length === 64) {
return trimmed;
}
// Decode WIF
try {
const decode = Bitcoin.Base58.decode(trimmed);
const keyWithVersion = decode.slice(0, decode.length - 4);
let keyBytes = keyWithVersion.slice(1);
// Remove compression flag if present
if (keyBytes.length >= 33 && keyBytes[keyBytes.length - 1] === 0x01) {
keyBytes = keyBytes.slice(0, keyBytes.length - 1);
}
return Crypto.util.bytesToHex(keyBytes);
} catch (e) {
throw new Error(`Invalid private key format: ${e.message}`);
}
}
/**
* Helper: Convert hex string to Uint8Array
* Uses built-in Crypto.util.hexToBytes from lib.cardano.js
* @private
*/
function hexToBytes(hex) {
return new Uint8Array(Crypto.util.hexToBytes(hex));
}
/**
* Helper: Convert Uint8Array to hex string
* Uses built-in Crypto.util.bytesToHex from lib.cardano.js
* @private
*/
function bytesToHex(bytes) {
return Crypto.util.bytesToHex(Array.from(bytes));
}

View File

@ -1,161 +0,0 @@
/**
* IndexedDB for storing searched Cardano addresses
*/
class CardanoSearchDB {
constructor() {
this.dbName = "CardanoWalletDB";
this.version = 1;
this.storeName = "searchedAddresses";
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, {
keyPath: "address",
});
store.createIndex("timestamp", "timestamp", { unique: false });
}
};
});
}
async saveSearchedAddress(
cardanoAddress,
balance,
timestamp = Date.now(),
sourceInfo = null,
addresses = null
) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(cardanoAddress);
getRequest.onsuccess = () => {
const existingRecord = getRequest.result;
let finalSourceInfo = sourceInfo;
let finalAddresses = addresses;
if (existingRecord && existingRecord.sourceInfo && !sourceInfo) {
finalSourceInfo = existingRecord.sourceInfo;
} else if (
existingRecord &&
existingRecord.sourceInfo &&
sourceInfo === null
) {
finalSourceInfo = existingRecord.sourceInfo;
}
// Preserve existing addresses if not provided
if (existingRecord && existingRecord.addresses && !addresses) {
finalAddresses = existingRecord.addresses;
}
const data = {
address: cardanoAddress,
balance,
timestamp,
formattedBalance: `${balance} ADA`,
sourceInfo: finalSourceInfo,
addresses: finalAddresses, // Store BTC, FLO, ADA addresses
};
const putRequest = store.put(data);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
};
getRequest.onerror = () => reject(getRequest.error);
});
}
async updateBalance(
cardanoAddress,
balance,
timestamp = Date.now()
) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(cardanoAddress);
getRequest.onsuccess = () => {
const existingRecord = getRequest.result;
if (!existingRecord) {
// If doesn't exist, save as new with basic info
const data = {
address: cardanoAddress,
balance,
timestamp,
formattedBalance: `${balance} ADA`,
sourceInfo: 'address',
addresses: null,
};
const putRequest = store.put(data);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
} else {
// Update only balance and timestamp, preserve everything else
existingRecord.balance = balance;
existingRecord.timestamp = timestamp;
existingRecord.formattedBalance = `${balance} ADA`;
const putRequest = store.put(existingRecord);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
}
};
getRequest.onerror = () => reject(getRequest.error);
});
}
async getSearchedAddresses() {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
const index = store.index("timestamp");
const request = index.getAll();
request.onsuccess = () => {
const results = request.result.sort(
(a, b) => b.timestamp - a.timestamp
);
resolve(results);
};
request.onerror = () => reject(request.error);
});
}
async deleteSearchedAddress(cardanoAddress) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const request = store.delete(cardanoAddress);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async clearAllSearchedAddresses() {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
export default CardanoSearchDB;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
import { defineConfig } from "vite";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig(({ command }) => {
const isProduction = command === "build";
return {
base: "./",
define: {
global: "window",
"process.env.NODE_ENV": JSON.stringify(
isProduction ? "production" : "development"
),
"process.version": JSON.stringify("v16.0.0"),
},
plugins: [
nodePolyfills({
exclude: ["fs"],
}),
wasm(),
topLevelAwait(),
],
};
});