push code after first round of testing
This commit is contained in:
commit
28fd131174
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal 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
522
index.html
Normal 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">×</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
4168
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
869
src/lib/cardanoBlockchainAPI.js
Normal file
869
src/lib/cardanoBlockchainAPI.js
Normal 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
314
src/lib/cardanoCrypto.js
Normal 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
161
src/lib/cardanoSearchDB.js
Normal 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
11473
src/lib/lib.cardano.js
Normal file
File diff suppressed because it is too large
Load Diff
1626
src/main.js
Normal file
1626
src/main.js
Normal file
File diff suppressed because it is too large
Load Diff
3547
src/style.css
Normal file
3547
src/style.css
Normal file
File diff suppressed because it is too large
Load Diff
26
vite.config.js
Normal file
26
vite.config.js
Normal 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(),
|
||||
],
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user