push code after first round of testing

This commit is contained in:
void-57 2025-12-02 13:40:54 +05:30
commit 28fd131174
11 changed files with 22762 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

522
index.html Normal file
View File

@ -0,0 +1,522 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cardano Wallet - RanchiMall</title>
<link
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" />
</head>
<body>
<!-- Loading Screen -->
<div id="loadingScreen" class="loading-screen">
<div class="loading-content">
<div class="loading-spinner"></div>
<h2>Loading Cardano Wallet...</h2>
</div>
</div>
<!-- Notification Drawer -->
<div id="notificationDrawer" class="notification-drawer"></div>
<header class="header">
<div class="header-content">
<div id="logo" class="app-brand">
<svg id="main_logo" class="icon" viewBox="0 0 27.25 32">
<title>RanchiMall</title>
<path
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.80,2.33q-.18.34-.39.69Z"
/>
</svg>
<div class="app-name">
<div class="app-name__company">RanchiMall</div>
<h4 class="app-name__title">Cardano Wallet</h4>
</div>
</div>
<div class="header-actions">
<button class="theme-toggle" id="themeToggle" title="Toggle theme">
<i class="fas fa-moon" id="themeIcon"></i>
</button>
</div>
</div>
</header>
<div id="sidebarOverlay" class="sidebar-overlay"></div>
<aside id="sidebar" class="sidebar">
<ul class="sidebar-menu">
<li>
<a href="#" class="nav-link active" data-page="generate">
<i class="fas fa-plus-circle"></i>
<span>Generate</span>
</a>
</li>
<li>
<a href="#" class="nav-link" data-page="send">
<i class="fas fa-paper-plane"></i>
<span>Send</span>
</a>
</li>
<li>
<a href="#" class="nav-link" data-page="transactions">
<i class="fas fa-history"></i>
<span>Transactions</span>
</a>
</li>
<li>
<a href="#" class="nav-link" data-page="recover">
<i class="fas fa-key"></i>
<span>Recover</span>
</a>
</li>
</ul>
</aside>
<div class="container">
<main class="main-content">
<!-- Generate Page -->
<div id="generatePage" class="page">
<div class="page-header">
<h2>
<i class="fas fa-wallet"></i> Generate Multi-Blockchain Addresses
</h2>
<p>
Generate addresses for ADA, FLO, BTC from a single private key
</p>
</div>
<div class="generate-wallet-intro">
<div class="intro-icon">
<i class="fas fa-wallet"></i>
</div>
<div class="intro-content">
<h3>One Key, Multiple Blockchains</h3>
<p>
Generate a single private key that works across ADA, FLO, BTC networks. This creates a unified experience across multiple blockchains.
</p>
</div>
</div>
<div class="card generate-actions">
<button
id="generateBtn"
class="btn btn-primary btn-block"
>
<i class="fas fa-wallet"></i>
Generate
</button>
</div>
<div id="walletOutput" class="output"></div>
<div class="wallet-security-notice">
<div class="notice-icon">
<i class="fas fa-shield-alt"></i>
</div>
<div class="notice-content">
<h4>Security Notice</h4>
<p>
Your private keys are generated locally and never leave your
device. Make sure to backup your keys securely before using.
</p>
</div>
</div>
</div>
<!-- Send ADA Page -->
<div id="sendPage" class="page hidden">
<div class="page-header">
<h2><i class="fas fa-paper-plane"></i> Send ADA</h2>
<p>Send ADA to another Cardano address</p>
</div>
<div class="card">
<form id="sendForm">
<div class="form-group">
<label for="sendPrivateKey">
<i class="fas fa-key"></i> Private Key
</label>
<div class="input-with-actions">
<input
type="password"
id="sendPrivateKey"
class="form-input"
placeholder="Enter ADA/FLO/BTC private key"
required
/>
<button
type="button"
class="input-action-btn password-toggle"
onclick="togglePasswordVisibility('sendPrivateKey')"
title="Show/Hide Password"
>
<i class="fas fa-eye"></i>
</button>
<button
type="button"
class="input-action-btn clear-btn"
onclick="clearInput('sendPrivateKey')"
title="Clear"
>
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div
id="balanceDisplay"
class="balance-info card"
style="display: none; margin-bottom: 1.5rem"
>
<div class="balance-header">
<h3><i class="fas fa-wallet"></i> Sender Balance</h3>
</div>
<div class="balance-display">
<div class="balance-amount" id="availableBalance">
0 <span class="currency">ADA</span>
</div>
</div>
<div
class="address-display"
style="display: block; margin-top: 0.5rem"
>
<span class="address-label">Address:</span>
<span class="address-value" id="senderAddress">-</span>
</div>
</div>
<div class="form-group">
<label for="recipientAddress">
<i class="fas fa-user"></i> To Address
</label>
<div class="input-with-actions">
<input
type="text"
id="recipientAddress"
class="form-input"
placeholder="Enter recipient address (addr1...)"
required
/>
<button
type="button"
class="input-action-btn clear-btn"
onclick="clearInput('recipientAddress')"
title="Clear"
>
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="sendAmount">
<i class="fas fa-coins"></i> Amount (ADA)
</label>
<div class="input-with-actions">
<input
type="number"
id="sendAmount"
class="form-input"
placeholder="Enter the amount to send"
min="0.000001"
step="0.000001"
required
/>
<button
type="button"
class="input-action-btn clear-btn"
onclick="clearInput('sendAmount')"
title="Clear"
>
<i class="fas fa-times"></i>
</button>
</div>
</div>
<button type="submit" id="sendBtn" class="btn btn-primary btn-block">
<i class="fas fa-paper-plane"></i>
Send Transaction
</button>
</form>
</div>
<div id="sendOutput" class="output"></div>
</div>
<!-- Transactions Page -->
<div id="transactionsPage" class="page hidden">
<div class="page-header">
<h2><i class="fas fa-exchange-alt"></i> Cardano Transactions</h2>
<p>Check balance and transaction history for any Cardano address</p>
</div>
<div class="card">
<!-- Search Type Selector -->
<div class="search-type-selector">
<div class="search-type-label">Search Type</div>
<div class="search-type-options">
<label class="radio-button-container active" id="addressSearchType">
<input type="radio" name="searchType" value="address" checked />
<div class="radio-icon"><i class="fas fa-wallet"></i></div>
<span>Cardano Address</span>
</label>
<label class="radio-button-container" id="hashSearchType">
<input type="radio" name="searchType" value="hash" />
<div class="radio-icon"><i class="fas fa-fingerprint"></i></div>
<span>Transaction Hash</span>
</label>
</div>
</div>
<div class="form-group">
<label for="searchInput">ADA Address or ADA/FLO/BTC Private Key</label>
<div class="input-with-actions">
<input
type="text"
id="searchInput"
class="form-input"
placeholder="Enter ADA address or ADA/FLO/BTC private key"
/>
<button
type="button"
class="input-action-btn clear-btn"
onclick="clearInput('searchInput')"
title="Clear"
>
<i class="fas fa-times"></i>
</button>
</div>
<div class="form-text">
Enter ADA address or ADA/FLO/BTC private key to view transactions
</div>
</div>
<button
class="btn btn-primary btn-block"
onclick="searchTransactions()"
id="searchBtn"
>
<i class="fas fa-search"></i>
Search
</button>
</div>
<div
id="transactionBalance"
class="balance-info card"
style="display: none"
>
<div class="balance-header">
<h3><i class="fas fa-wallet"></i> Balance</h3>
</div>
<div class="balance-display">
<div class="balance-amount" id="adaBalance">
0 <span class="currency">ADA</span>
</div>
</div>
<div
class="address-display"
id="transactionAddressDisplay"
style="display: none"
>
<span class="address-label">Address:</span>
<span class="address-value" id="displayedAddress"></span>
</div>
</div>
<div
id="transactionFilterSection"
class="transaction-section"
style="display: none"
>
<div class="transaction-header">
<h3><i class="fas fa-history"></i> Transactions</h3>
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="received">Received</button>
<button class="filter-btn" data-filter="sent">Sent</button>
</div>
</div>
</div>
<div id="transactionList" class="transaction-list"></div>
<div
id="paginationSection"
class="pagination-section"
style="display: none"
>
<div class="pagination-info">
<span id="paginationInfo">Showing 1-10 of 0 transactions</span>
</div>
<div class="pagination-controls">
<button
class="pagination-btn"
id="prevPageBtn"
disabled
>
<i class="fas fa-chevron-left"></i>
<span class="btn-text">Prev</span>
</button>
<div class="page-numbers" id="pageNumbers"></div>
<button class="pagination-btn" id="nextPageBtn">
<span class="btn-text">Next</span>
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<!-- Searched Addresses History -->
<div id="searchedAddressesSection" class="card" style="margin-top: 1.5rem; display: none;">
<div class="search-history-header">
<h3><i class="fas fa-sync-alt"></i> Recent Addresses</h3>
<button class="btn btn-secondary btn-sm" onclick="clearSearchHistory()" title="Clear all history">
<i class="fas fa-trash-alt"></i> Clear All
</button>
</div>
<div id="searchedAddressesList" class="searched-addresses-list"></div>
</div>
</div>
<!-- Recover Page -->
<div id="recoverPage" class="page hidden">
<div class="page-header">
<h2>
<i class="fas fa-sync-alt"></i> Recover Multi-Blockchain Addresses
</h2>
<p>
Recover all blockchain addresses (ADA, FLO, BTC) from a single private key
</p>
</div>
<div class="card">
<div class="form-group">
<label for="recoverPrivateKey"
><i class="fas fa-key"></i> Private Key (ADA/FLO/BTC)</label
>
<div class="input-with-actions">
<input
type="password"
id="recoverPrivateKey"
class="form-input"
placeholder="Enter ADA/FLO/BTC private key"
/>
<button
type="button"
class="input-action-btn password-toggle"
onclick="togglePasswordVisibility('recoverPrivateKey')"
title="Show/Hide Password"
>
<i class="fas fa-eye"></i>
</button>
<button
type="button"
class="input-action-btn clear-btn"
onclick="clearInput('recoverPrivateKey')"
title="Clear"
>
<i class="fas fa-times"></i>
</button>
</div>
<div class="form-text">
Only private keys accepted
</div>
</div>
<button id="recoverBtn" class="btn btn-primary btn-block">
<i class="fas fa-sync-alt"></i>
Recover
</button>
<div id="recoverOutput" class="output"></div>
</div>
<div class="wallet-security-notice">
<div class="notice-icon">
<i class="fas fa-shield-alt"></i>
</div>
<div class="notice-content">
<h4>Privacy Notice</h4>
<p>
Your private key is processed locally and never transmitted to
any server. All recovery operations happen in your browser.
</p>
</div>
</div>
</div>
</main>
</div>
<!-- Bottom Navigation (Mobile) -->
<nav class="nav-box">
<button class="nav-btn active" data-page="generate">
<i class="fas fa-plus-circle"></i>
<span>Generate</span>
</button>
<button class="nav-btn" data-page="send">
<i class="fas fa-paper-plane"></i>
<span>Send</span>
</button>
<button class="nav-btn" data-page="transactions">
<i class="fas fa-history"></i>
<span>Transactions</span>
</button>
<button class="nav-btn" data-page="recover">
<i class="fas fa-key"></i>
<span>Recover</span>
</button>
</nav>
<!-- Transaction Confirmation Modal -->
<div id="confirmModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-shield-alt"></i> Confirm Transaction</h3>
<span class="modal-close" id="closeModal">&times;</span>
</div>
<div class="modal-body">
<div class="confirm-details">
<div class="detail-group">
<label><i class="fas fa-coins"></i> Amount</label>
<div class="confirm-value" id="confirmAmount">0 ADA</div>
</div>
<div class="detail-group">
<label><i class="fas fa-gas-pump"></i> Network Fee</label>
<div class="confirm-value" id="confirmFee">0 ADA</div>
</div>
<div class="detail-group">
<label><i class="fas fa-arrow-right"></i> Total Cost</label>
<div class="confirm-value" id="confirmTotal">0 ADA</div>
</div>
<div class="detail-group">
<label><i class="fas fa-user"></i> Recipient</label>
<div class="confirm-value address-value" id="confirmRecipient">addr1...</div>
</div>
<div class="detail-group warning">
<i class="fas fa-exclamation-triangle"></i>
<span>Transactions are irreversible. Please verify the recipient address carefully.</span>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelTxBtn">Cancel</button>
<button class="btn btn-primary" id="confirmTxBtn">
<i class="fas fa-check"></i> Confirm & Send
</button>
</div>
</div>
</div>
<script src="./src/lib/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 Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"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

@ -0,0 +1,869 @@
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;

314
src/lib/cardanoCrypto.js Normal file
View File

@ -0,0 +1,314 @@
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));
}

161
src/lib/cardanoSearchDB.js Normal file
View File

@ -0,0 +1,161 @@
/**
* 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;

11473
src/lib/lib.cardano.js Normal file

File diff suppressed because it is too large Load Diff

1626
src/main.js Normal file

File diff suppressed because it is too large Load Diff

3547
src/style.css Normal file

File diff suppressed because it is too large Load Diff

26
vite.config.js Normal file
View File

@ -0,0 +1,26 @@
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(),
],
};
});