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 +158 -0
- package/package.json +59 -0
- package/src/address-derivation.js +476 -0
- package/src/app.js +3302 -0
- package/src/blockchain-trust.js +699 -0
- package/src/constants.js +87 -0
- package/src/lib.js +63 -0
- package/src/template.js +348 -0
- package/src/trust-ui.js +745 -0
- package/src/wallet-storage.js +696 -0
- package/styles/main.css +4521 -0
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) { ... }
|