hd-wallet-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # HD Wallet UI
2
+
3
+ Standalone HD wallet interface with glass morphism design. Supports BIP-32/39/44 key derivation across multiple blockchain networks.
4
+
5
+ ## Features
6
+
7
+ - **Multi-chain support** -- BTC, ETH, SOL, SUI, Monad, Cardano (and more via HD derivation)
8
+ - **Three login methods** -- Password, BIP39 seed phrase, or stored wallet (PIN/Passkey)
9
+ - **HD key derivation** -- BIP44 paths with configurable network, account, and index
10
+ - **Secure storage** -- PIN (PBKDF2 + AES-256-GCM) or Passkey (WebAuthn PRF)
11
+ - **vCard generation** -- Export identity with cryptographic public keys
12
+ - **Live balance checking** -- Fetches balances from public blockchain APIs
13
+ - **Glass morphism UI** -- Frosted glass aesthetic with blurred background
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ npm install
19
+ npm run dev
20
+ ```
21
+
22
+ Opens on `http://localhost:3000`.
23
+
24
+ ## Build
25
+
26
+ ```bash
27
+ npm run build # Output in dist/
28
+ npm run preview # Preview the build
29
+ ```
30
+
31
+ ## Project Structure
32
+
33
+ ```
34
+ wallet-ui/
35
+ ├── index.html # Main HTML
36
+ ├── src/
37
+ │ ├── app.js # Entry point, login/logout, UI handlers
38
+ │ ├── wallet-storage.js # Encrypted wallet storage (PIN/Passkey)
39
+ │ ├── address-derivation.js # Multi-chain address generation
40
+ │ └── constants.js # Coin configs, explorer URLs, path helpers
41
+ ├── styles/
42
+ │ └── main.css # Glass morphism styles
43
+ ├── package.json
44
+ └── vite.config.js
45
+ ```
46
+
47
+ ## Usage Examples
48
+
49
+ ### Address Derivation
50
+
51
+ ```js
52
+ import {
53
+ generateBtcAddress,
54
+ generateEthAddress,
55
+ generateSolAddress,
56
+ deriveSuiAddress,
57
+ deriveCardanoAddress,
58
+ } from './src/address-derivation.js';
59
+
60
+ // Bitcoin P2PKH from compressed secp256k1 pubkey
61
+ const btcAddr = generateBtcAddress(compressedPubKey); // "1A1zP1..."
62
+
63
+ // Ethereum from secp256k1 (handles 33, 64, or 65 byte keys)
64
+ const ethAddr = generateEthAddress(compressedPubKey); // "0x..."
65
+
66
+ // Solana from Ed25519 pubkey
67
+ const solAddr = generateSolAddress(ed25519PubKey); // Base58 string
68
+
69
+ // SUI from Ed25519 with BLAKE2b
70
+ const suiAddr = deriveSuiAddress(ed25519PubKey, 'ed25519'); // "0x..."
71
+
72
+ // Cardano enterprise address (Bech32)
73
+ const adaAddr = deriveCardanoAddress(ed25519PubKey); // "addr1..."
74
+ ```
75
+
76
+ ### Derivation Paths
77
+
78
+ ```js
79
+ import { buildSigningPath, buildEncryptionPath } from './src/constants.js';
80
+
81
+ buildSigningPath(0, 0, 0); // "m/44'/0'/0'/0/0" (Bitcoin)
82
+ buildSigningPath(60, 0, 0); // "m/44'/60'/0'/0/0" (Ethereum)
83
+ buildEncryptionPath(0, 0, 0); // "m/44'/0'/0'/1/0" (encryption key)
84
+ ```
85
+
86
+ ### Coin Configuration
87
+
88
+ ```js
89
+ import { cryptoConfig, coinTypeToConfig } from './src/constants.js';
90
+
91
+ cryptoConfig.btc.explorer; // "https://blockstream.info/address/"
92
+ coinTypeToConfig[60].name; // "Ethereum"
93
+ cryptoConfig.eth.formatBalance(1e18); // "1.000000 ETH"
94
+ ```
95
+
96
+ ### Wallet Storage
97
+
98
+ ```js
99
+ import WalletStorage, { StorageMethod } from './src/wallet-storage.js';
100
+
101
+ // Store with PIN
102
+ await WalletStorage.storeWithPIN('123456', { type: 'seed', seedPhrase: '...' });
103
+
104
+ // Retrieve with PIN
105
+ const data = await WalletStorage.retrieveWithPIN('123456');
106
+
107
+ // Store with Passkey (WebAuthn PRF)
108
+ await WalletStorage.storeWithPasskey(walletData, {
109
+ rpName: 'My Wallet',
110
+ userName: 'user',
111
+ });
112
+
113
+ // Check storage status
114
+ const meta = WalletStorage.getStorageMetadata();
115
+ // { method: 'passkey', storedAt: 1706000000000, version: 2 }
116
+ ```
117
+
118
+ ### Balance Fetching
119
+
120
+ ```js
121
+ import {
122
+ fetchBtcBalance,
123
+ fetchEthBalance,
124
+ fetchSolBalance,
125
+ } from './src/address-derivation.js';
126
+
127
+ const { balance } = await fetchBtcBalance('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa');
128
+ // "50.00000000"
129
+
130
+ const { balance: ethBal } = await fetchEthBalance('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe');
131
+ // "0.000000"
132
+ ```
133
+
134
+ ## Dependencies
135
+
136
+ | Package | Purpose |
137
+ |---------|---------|
138
+ | `hd-wallet-wasm` | HD key derivation (BIP-32/39/44), WASM runtime |
139
+ | `@noble/curves` | secp256k1, ed25519, p256 elliptic curves |
140
+ | `@noble/hashes` | SHA-256, Keccak-256, RIPEMD-160, BLAKE2b |
141
+ | `@scure/base` | Base58, Base58Check encoding |
142
+ | `@scure/bip32` | BIP-32 extended key derivation |
143
+ | `bip39` | BIP-39 mnemonic generation/validation |
144
+ | `qrcode` | QR code rendering for addresses and vCards |
145
+ | `vcard-cryptoperson` | vCard 4.0 with cryptographic keys |
146
+ | `buffer` | Buffer polyfill for browser |
147
+
148
+ ## Tests
149
+
150
+ ```bash
151
+ npm test
152
+ ```
153
+
154
+ Runs unit tests for address derivation, Bech32 encoding, and coin configuration. See [test/](test/) for details.
155
+
156
+ ## License
157
+
158
+ Same as parent `hd-wallet-wasm` repository.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "hd-wallet-ui",
3
+ "version": "1.0.0",
4
+ "description": "HD Wallet modal UI — login, keys, identity, trust map, and security bond. Attach to any button in your app.",
5
+ "type": "module",
6
+ "main": "src/app.js",
7
+ "module": "src/app.js",
8
+ "exports": {
9
+ ".": "./src/app.js",
10
+ "./lib": "./src/lib.js",
11
+ "./styles": "./styles/main.css"
12
+ },
13
+ "files": [
14
+ "src/",
15
+ "styles/"
16
+ ],
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "build:wasm": "emcmake cmake -B ../build-wasm -S .. -DCMAKE_BUILD_TYPE=Release -DHD_WALLET_BUILD_WASM=ON && cmake --build ../build-wasm --target hd_wallet_wasm_npm -j8",
20
+ "dev": "vite",
21
+ "dev:full": "npm run build:wasm && vite",
22
+ "build": "npm run build:wasm && vite build",
23
+ "build:docs": "npm run build:wasm && vite build --outDir ../docs --emptyOutDir",
24
+ "preview": "vite preview",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ },
28
+ "dependencies": {
29
+ "@noble/curves": "^1.9.7",
30
+ "@noble/hashes": "^1.7.2",
31
+ "@peculiar/x509": "^1.14.3",
32
+ "@scure/base": "^1.2.4",
33
+ "@scure/bip32": "^2.0.1",
34
+ "bip39": "^3.1.0",
35
+ "buffer": "^6.0.3",
36
+ "flatbuffers": "^25.9.23",
37
+ "hd-wallet-wasm": "file:../wasm",
38
+ "qrcode": "^1.5.3",
39
+ "vcard-cryptoperson": "^1.1.10"
40
+ },
41
+ "devDependencies": {
42
+ "vite": "^5.0.0",
43
+ "vitest": "^4.0.18"
44
+ },
45
+ "keywords": [
46
+ "hd-wallet",
47
+ "wallet-ui",
48
+ "webassembly",
49
+ "wasm",
50
+ "bip32",
51
+ "bip39",
52
+ "bip44",
53
+ "cryptocurrency",
54
+ "bitcoin",
55
+ "ethereum",
56
+ "solana"
57
+ ],
58
+ "license": "Apache-2.0"
59
+ }
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Address Derivation Module
3
+ *
4
+ * Functions for generating blockchain addresses from public keys:
5
+ * BTC, ETH, SOL, SUI, Monad, Cardano
6
+ */
7
+
8
+ import { secp256k1 } from '@noble/curves/secp256k1';
9
+ import { blake2b } from '@noble/hashes/blake2b';
10
+ import { keccak_256 } from '@noble/hashes/sha3';
11
+ import { ripemd160 } from '@noble/hashes/ripemd160';
12
+ import { sha256 as sha256Noble } from '@noble/hashes/sha256';
13
+ import { base58check, base58 } from '@scure/base';
14
+
15
+ import { coinTypeToConfig } from './constants.js';
16
+
17
+ // =============================================================================
18
+ // API Proxy (dev mode CORS workaround)
19
+ // =============================================================================
20
+
21
+ const isDev = import.meta.env?.DEV ?? false;
22
+
23
+ const proxyMap = {
24
+ 'https://blockchain.info': '/api/blockchain',
25
+ 'https://cloudflare-eth.com': '/api/eth',
26
+ 'https://api.mainnet-beta.solana.com': '/api/solana/official',
27
+ 'https://solana-rpc.publicnode.com': '/api/solana/publicnode',
28
+ 'https://mainnet.helius-rpc.com': '/api/solana/helius',
29
+ /* Commented out — BTC/ETH/SOL only for now
30
+ 'https://fullnode.mainnet.sui.io:443': '/api/sui',
31
+ 'https://testnet-rpc.monad.xyz': '/api/monad',
32
+ 'https://api.koios.rest': '/api/koios',
33
+ 'https://s1.ripple.com:51234': '/api/xrp',
34
+ */
35
+ 'https://api.coinbase.com': '/api/coinbase',
36
+ 'https://api.hiro.so': '/api/hiro',
37
+ };
38
+
39
+ export function apiUrl(url) {
40
+ if (!isDev) return url;
41
+ for (const [origin, proxy] of Object.entries(proxyMap)) {
42
+ if (url.startsWith(origin)) {
43
+ return url.replace(origin, proxy);
44
+ }
45
+ }
46
+ return url;
47
+ }
48
+
49
+ // =============================================================================
50
+ // Utility Helpers
51
+ // =============================================================================
52
+
53
+ /**
54
+ * Convert a Uint8Array to a compact hex string (no spaces)
55
+ * @param {Uint8Array} bytes
56
+ * @returns {string}
57
+ */
58
+ export function toHexCompact(bytes) {
59
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
60
+ }
61
+
62
+ /**
63
+ * Alias for toHexCompact
64
+ */
65
+ export function toHex(bytes) {
66
+ return toHexCompact(bytes);
67
+ }
68
+
69
+ /**
70
+ * Convert hex string to Uint8Array
71
+ * @param {string} hex
72
+ * @returns {Uint8Array}
73
+ */
74
+ export function hexToBytes(hex) {
75
+ const bytes = new Uint8Array(hex.length / 2);
76
+ for (let i = 0; i < hex.length; i += 2) {
77
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
78
+ }
79
+ return bytes;
80
+ }
81
+
82
+ /**
83
+ * Ensure a value is a Uint8Array.
84
+ * Handles localStorage deserialization which produces plain objects.
85
+ * @param {*} value
86
+ * @returns {Uint8Array}
87
+ */
88
+ export function ensureUint8Array(value) {
89
+ if (value instanceof Uint8Array) {
90
+ return value;
91
+ }
92
+ if (typeof value === 'object' && value !== null) {
93
+ return new Uint8Array(Object.values(value));
94
+ }
95
+ return new Uint8Array(value);
96
+ }
97
+
98
+ // =============================================================================
99
+ // Base58Check encoder for Bitcoin
100
+ // =============================================================================
101
+
102
+ const base58checkBtc = base58check(sha256Noble);
103
+
104
+ // =============================================================================
105
+ // Address Generation Functions
106
+ // =============================================================================
107
+
108
+ /**
109
+ * Generate a Bitcoin P2PKH address from a compressed secp256k1 public key
110
+ * Uses @scure/base for proper Base58Check encoding
111
+ * @param {Uint8Array} publicKey - Compressed secp256k1 public key (33 bytes)
112
+ * @returns {string} Bitcoin address starting with '1'
113
+ */
114
+ export function generateBtcAddress(publicKey) {
115
+ const hash160 = ripemd160(sha256Noble(publicKey));
116
+ return base58checkBtc.encode(new Uint8Array([0x00, ...hash160]));
117
+ }
118
+
119
+ /**
120
+ * Generate an Ethereum address from a secp256k1 public key
121
+ * Uses @noble/hashes keccak_256 for proper Ethereum address derivation
122
+ * @param {Uint8Array} publicKey - Compressed secp256k1 public key (33 bytes)
123
+ * @returns {string} Ethereum address with 0x prefix
124
+ */
125
+ export function generateEthAddress(publicKey) {
126
+ const point = secp256k1.ProjectivePoint.fromHex(publicKey);
127
+ const uncompressed = point.toRawBytes(false); // 65 bytes: 04 || x || y
128
+ const hash = keccak_256(uncompressed.slice(1));
129
+ return '0x' + toHexCompact(hash.slice(-20));
130
+ }
131
+
132
+ /**
133
+ * Generate a Solana address from an Ed25519 public key
134
+ * Uses @scure/base for proper Base58 encoding
135
+ * @param {Uint8Array} publicKey - Ed25519 public key (32 bytes)
136
+ * @returns {string} Solana address
137
+ */
138
+ export function generateSolAddress(publicKey) {
139
+ return base58.encode(publicKey);
140
+ }
141
+
142
+ /**
143
+ * Generate an XRP address from a compressed secp256k1 public key
144
+ * Uses SHA-256 → RIPEMD-160 hash, then Base58Check with version byte 0x00
145
+ * @param {Uint8Array} publicKey - Compressed secp256k1 public key (33 bytes)
146
+ * @returns {string} XRP address starting with 'r'
147
+ */
148
+ export function generateXrpAddress(publicKey) {
149
+ const hash160 = ripemd160(sha256Noble(publicKey));
150
+ // XRP uses the same Base58Check as BTC but the alphabet produces 'r' prefix
151
+ return base58checkBtc.encode(new Uint8Array([0x00, ...hash160]));
152
+ }
153
+
154
+ /**
155
+ * Derive an Ethereum/Monad-compatible address from a secp256k1 public key.
156
+ * Handles compressed (33 bytes), uncompressed (65 bytes), and raw (64 bytes) formats.
157
+ * @param {Uint8Array} publicKey
158
+ * @returns {string|null} Ethereum address with 0x prefix, or null on failure
159
+ */
160
+ export function deriveEthAddress(publicKey) {
161
+ try {
162
+ if (publicKey.length === 33) {
163
+ const point = secp256k1.ProjectivePoint.fromHex(publicKey);
164
+ const uncompressed = point.toRawBytes(false).slice(1);
165
+ const hash = keccak_256(uncompressed);
166
+ return '0x' + toHex(hash.slice(-20));
167
+ }
168
+ if (publicKey.length === 65) {
169
+ const hash = keccak_256(publicKey.slice(1));
170
+ return '0x' + toHex(hash.slice(-20));
171
+ }
172
+ if (publicKey.length === 64) {
173
+ const hash = keccak_256(publicKey);
174
+ return '0x' + toHex(hash.slice(-20));
175
+ }
176
+ return null;
177
+ } catch (e) {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Derive a SUI address from a public key using BLAKE2b
184
+ * @param {Uint8Array} publicKey - Public key bytes
185
+ * @param {string} scheme - Key scheme: 'ed25519', 'secp256k1', or 'secp256r1'
186
+ * @returns {string} SUI address with 0x prefix
187
+ */
188
+ export function deriveSuiAddress(publicKey, scheme = 'ed25519') {
189
+ const schemeFlags = {
190
+ 'ed25519': 0x00,
191
+ 'secp256k1': 0x01,
192
+ 'secp256r1': 0x02,
193
+ };
194
+ const flag = schemeFlags[scheme] ?? 0x00;
195
+
196
+ const data = new Uint8Array(1 + publicKey.length);
197
+ data[0] = flag;
198
+ data.set(publicKey, 1);
199
+
200
+ const hash = blake2b(data, { dkLen: 32 });
201
+ return '0x' + Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('');
202
+ }
203
+
204
+ /**
205
+ * Derive a Monad address from a secp256k1 public key (same as Ethereum derivation)
206
+ * @param {Uint8Array} publicKey - secp256k1 public key (33 or 65 bytes)
207
+ * @returns {string} Monad address with 0x prefix
208
+ */
209
+ export function deriveMonadAddress(publicKey) {
210
+ let uncompressedPubKey;
211
+ if (publicKey.length === 33) {
212
+ const point = secp256k1.ProjectivePoint.fromHex(publicKey);
213
+ uncompressedPubKey = point.toRawBytes(false);
214
+ } else if (publicKey.length === 65) {
215
+ uncompressedPubKey = publicKey;
216
+ } else {
217
+ throw new Error('Invalid public key length for Monad address derivation');
218
+ }
219
+
220
+ const hash = keccak_256(uncompressedPubKey.slice(1));
221
+ const address = hash.slice(-20);
222
+ return '0x' + Array.from(address).map(b => b.toString(16).padStart(2, '0')).join('');
223
+ }
224
+
225
+ /**
226
+ * Derive a Cardano enterprise address from an Ed25519 public key
227
+ * Uses Bech32 encoding with "addr" prefix for mainnet
228
+ * @param {Uint8Array} publicKey - Ed25519 public key (32 bytes)
229
+ * @returns {string} Cardano address in Bech32 format
230
+ */
231
+ export function deriveCardanoAddress(publicKey) {
232
+ const keyHash = blake2b(publicKey, { dkLen: 28 }); // 224-bit hash
233
+ const addressBytes = new Uint8Array(29);
234
+ addressBytes[0] = 0x61; // Enterprise address, mainnet
235
+ addressBytes.set(keyHash, 1);
236
+ return bech32Encode('addr', addressBytes);
237
+ }
238
+
239
+ // =============================================================================
240
+ // Bech32 Encoding (for Cardano)
241
+ // =============================================================================
242
+
243
+ function bech32Encode(prefix, data) {
244
+ const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
245
+
246
+ const data5bit = convertBits(data, 8, 5, true);
247
+ const checksumData = expandHrp(prefix).concat(data5bit).concat([0, 0, 0, 0, 0, 0]);
248
+ const polymod = bech32Polymod(checksumData) ^ 1;
249
+ const checksum = [];
250
+ for (let i = 0; i < 6; i++) {
251
+ checksum.push((polymod >> (5 * (5 - i))) & 31);
252
+ }
253
+
254
+ let result = prefix + '1';
255
+ for (const d of data5bit.concat(checksum)) {
256
+ result += CHARSET[d];
257
+ }
258
+ return result;
259
+ }
260
+
261
+ function convertBits(data, fromBits, toBits, pad) {
262
+ let acc = 0;
263
+ let bits = 0;
264
+ const ret = [];
265
+ const maxv = (1 << toBits) - 1;
266
+ for (const value of data) {
267
+ acc = (acc << fromBits) | value;
268
+ bits += fromBits;
269
+ while (bits >= toBits) {
270
+ bits -= toBits;
271
+ ret.push((acc >> bits) & maxv);
272
+ }
273
+ }
274
+ if (pad) {
275
+ if (bits > 0) {
276
+ ret.push((acc << (toBits - bits)) & maxv);
277
+ }
278
+ }
279
+ return ret;
280
+ }
281
+
282
+ function expandHrp(hrp) {
283
+ const ret = [];
284
+ for (const c of hrp) {
285
+ ret.push(c.charCodeAt(0) >> 5);
286
+ }
287
+ ret.push(0);
288
+ for (const c of hrp) {
289
+ ret.push(c.charCodeAt(0) & 31);
290
+ }
291
+ return ret;
292
+ }
293
+
294
+ function bech32Polymod(values) {
295
+ const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
296
+ let chk = 1;
297
+ for (const v of values) {
298
+ const b = chk >> 25;
299
+ chk = ((chk & 0x1ffffff) << 5) ^ v;
300
+ for (let i = 0; i < 5; i++) {
301
+ if ((b >> i) & 1) {
302
+ chk ^= GEN[i];
303
+ }
304
+ }
305
+ }
306
+ return chk;
307
+ }
308
+
309
+ // =============================================================================
310
+ // Composite Address Generation
311
+ // =============================================================================
312
+
313
+ /**
314
+ * Generate BTC, ETH, and SOL addresses from a wallet key set
315
+ * @param {{ secp256k1: { publicKey: Uint8Array }, ed25519: { publicKey: Uint8Array } }} wallet
316
+ * @returns {{ btc: string, eth: string, sol: string }}
317
+ */
318
+ export function generateAddresses(wallet) {
319
+ return {
320
+ btc: generateBtcAddress(wallet.secp256k1.publicKey),
321
+ eth: generateEthAddress(wallet.secp256k1.publicKey),
322
+ sol: generateSolAddress(wallet.ed25519.publicKey),
323
+ // xrp: generateXrpAddress(wallet.secp256k1.publicKey), // Commented out — BTC/ETH/SOL only for now
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Generate address from public key based on coin type for HD derivation
329
+ * @param {Uint8Array} publicKey - The derived public key
330
+ * @param {number} coinType - BIP44 coin type
331
+ * @returns {string} The generated address
332
+ */
333
+ export function generateAddressForCoin(publicKey, coinType) {
334
+ const config = coinTypeToConfig[coinType];
335
+ if (!config) {
336
+ return toHexCompact(publicKey);
337
+ }
338
+
339
+ switch (coinType) {
340
+ case 0: // Bitcoin
341
+ case 2: // Litecoin
342
+ case 3: // Dogecoin
343
+ case 145: // Bitcoin Cash
344
+ return generateBtcAddress(publicKey);
345
+
346
+ case 60: // Ethereum
347
+ return generateEthAddress(publicKey);
348
+
349
+ case 501: // Solana - uses ed25519, but we generate from secp256k1 for demo
350
+ return base58.encode(publicKey.slice(0, 32));
351
+
352
+ case 144: // XRP
353
+ return generateXrpAddress(publicKey);
354
+
355
+ case 118: // Cosmos
356
+ case 330: // Algorand
357
+ case 354: // Polkadot
358
+ case 1815: { // Cardano
359
+ const hash = sha256Noble(publicKey);
360
+ return toHexCompact(hash.slice(0, 20));
361
+ }
362
+
363
+ default: {
364
+ const defaultHash = sha256Noble(publicKey);
365
+ return toHexCompact(defaultHash.slice(0, 20));
366
+ }
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Truncate an address for display
372
+ * @param {string} address
373
+ * @returns {string}
374
+ */
375
+ export function truncateAddress(address) {
376
+ if (address.length <= 16) return address;
377
+ return address.slice(0, 8) + '...' + address.slice(-6);
378
+ }
379
+
380
+ // =============================================================================
381
+ // Balance Fetching
382
+ // =============================================================================
383
+
384
+ /**
385
+ * Fetch Bitcoin balance
386
+ * @param {string} address
387
+ * @returns {Promise<{balance: string, error?: string}>}
388
+ */
389
+ export async function fetchBtcBalance(address) {
390
+ try {
391
+ const response = await fetch(apiUrl(`https://blockchain.info/q/addressbalance/${address}?cors=true`));
392
+ if (!response.ok) {
393
+ return { balance: '0', error: 'API error' };
394
+ }
395
+ const satoshis = await response.text();
396
+ const btc = parseInt(satoshis, 10) / 1e8;
397
+ return { balance: btc.toFixed(8) };
398
+ } catch (e) {
399
+ console.debug('BTC balance fetch unavailable:', e.message);
400
+ return { balance: '--', error: e.message };
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Fetch Ethereum balance
406
+ * @param {string} address
407
+ * @returns {Promise<{balance: string, error?: string}>}
408
+ */
409
+ export async function fetchEthBalance(address) {
410
+ try {
411
+ const response = await fetch(apiUrl('https://cloudflare-eth.com'), {
412
+ method: 'POST',
413
+ headers: { 'Content-Type': 'application/json' },
414
+ body: JSON.stringify({
415
+ jsonrpc: '2.0',
416
+ id: 1,
417
+ method: 'eth_getBalance',
418
+ params: [address, 'latest']
419
+ })
420
+ });
421
+ const data = await response.json();
422
+ if (data.error) {
423
+ return { balance: '0', error: data.error.message };
424
+ }
425
+ const balanceWei = BigInt(data.result || '0x0');
426
+ const balanceEth = Number(balanceWei) / 1e18;
427
+ return { balance: balanceEth.toFixed(6) };
428
+ } catch (e) {
429
+ console.debug('ETH balance fetch unavailable:', e.message);
430
+ return { balance: '--', error: e.message };
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Fetch Solana balance
436
+ * @param {string} address
437
+ * @returns {Promise<{balance: string, error?: string}>}
438
+ */
439
+ export async function fetchSolBalance(address) {
440
+ const endpoints = [
441
+ 'https://api.mainnet-beta.solana.com',
442
+ 'https://solana-rpc.publicnode.com',
443
+ 'https://mainnet.helius-rpc.com/?api-key=1d8740dc-e5f4-421c-b823-e1bad1889eda',
444
+ ];
445
+
446
+ for (const endpoint of endpoints) {
447
+ try {
448
+ const response = await fetch(apiUrl(endpoint), {
449
+ method: 'POST',
450
+ headers: { 'Content-Type': 'application/json' },
451
+ body: JSON.stringify({
452
+ jsonrpc: '2.0',
453
+ id: 1,
454
+ method: 'getBalance',
455
+ params: [address]
456
+ })
457
+ });
458
+ if (!response.ok) continue;
459
+ const data = await response.json();
460
+ if (data.error) continue;
461
+ const lamports = data.result?.value || 0;
462
+ const sol = lamports / 1e9;
463
+ return { balance: sol.toFixed(6) };
464
+ } catch (e) {
465
+ continue;
466
+ }
467
+ }
468
+ console.debug('SOL balance fetch unavailable: all endpoints failed');
469
+ return { balance: '--', error: 'No available endpoint' };
470
+ }
471
+
472
+ // Commented out — BTC/ETH/SOL only for now
473
+ // export async function fetchSuiBalance(address) { ... }
474
+ // export async function fetchMonadBalance(address) { ... }
475
+ // export async function fetchAdaBalance(address) { ... }
476
+ // export async function fetchXrpBalance(address) { ... }