@profullstack/coinpay 0.3.9 → 0.4.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/bin/coinpay.js +1005 -235
- package/package.json +21 -4
- package/src/client.js +87 -0
- package/src/escrow.js +245 -0
- package/src/index.d.ts +65 -2
- package/src/index.js +82 -1
- package/src/swap.d.ts +254 -0
- package/src/swap.js +360 -0
- package/src/wallet.d.ts +259 -0
- package/src/wallet.js +757 -0
package/src/wallet.js
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet Module for CoinPay SDK
|
|
3
|
+
*
|
|
4
|
+
* Client-side wallet management with server-side address registration.
|
|
5
|
+
* IMPORTANT: Mnemonic/seed phrases are NEVER sent to the server.
|
|
6
|
+
* Only public keys and signed proofs are transmitted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as bip39 from '@scure/bip39';
|
|
10
|
+
import { wordlist } from '@scure/bip39/wordlists/english';
|
|
11
|
+
import { HDKey } from '@scure/bip32';
|
|
12
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
13
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
14
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_BASE_URL = 'https://coinpayportal.com/api';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supported blockchain chains
|
|
20
|
+
*/
|
|
21
|
+
export const WalletChain = {
|
|
22
|
+
BTC: 'BTC',
|
|
23
|
+
BCH: 'BCH',
|
|
24
|
+
ETH: 'ETH',
|
|
25
|
+
POL: 'POL',
|
|
26
|
+
SOL: 'SOL',
|
|
27
|
+
BNB: 'BNB',
|
|
28
|
+
USDC_ETH: 'USDC_ETH',
|
|
29
|
+
USDC_POL: 'USDC_POL',
|
|
30
|
+
USDC_SOL: 'USDC_SOL',
|
|
31
|
+
USDT_ETH: 'USDT_ETH',
|
|
32
|
+
USDT_POL: 'USDT_POL',
|
|
33
|
+
USDT_SOL: 'USDT_SOL',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default chains to derive on wallet creation
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_CHAINS = ['BTC', 'ETH', 'SOL', 'POL', 'BCH'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* BIP44 coin types for derivation paths
|
|
43
|
+
*/
|
|
44
|
+
const COIN_TYPES = {
|
|
45
|
+
BTC: 0,
|
|
46
|
+
BCH: 145,
|
|
47
|
+
ETH: 60,
|
|
48
|
+
POL: 60, // Uses ETH path
|
|
49
|
+
BNB: 60, // Uses ETH path (BSC is EVM)
|
|
50
|
+
SOL: 501,
|
|
51
|
+
USDC_ETH: 60,
|
|
52
|
+
USDC_POL: 60,
|
|
53
|
+
USDC_SOL: 501,
|
|
54
|
+
USDT_ETH: 60,
|
|
55
|
+
USDT_POL: 60,
|
|
56
|
+
USDT_SOL: 501,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Chains that use secp256k1 curve (vs ed25519 for Solana)
|
|
61
|
+
*/
|
|
62
|
+
const SECP256K1_CHAINS = ['BTC', 'BCH', 'ETH', 'POL', 'BNB', 'USDC_ETH', 'USDC_POL', 'USDT_ETH', 'USDT_POL'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a new mnemonic phrase
|
|
66
|
+
* @param {number} [words=12] - Number of words (12 or 24)
|
|
67
|
+
* @returns {string} BIP39 mnemonic phrase
|
|
68
|
+
*/
|
|
69
|
+
export function generateMnemonic(words = 12) {
|
|
70
|
+
if (words !== 12 && words !== 24) {
|
|
71
|
+
throw new Error('Invalid word count. Must be 12 or 24.');
|
|
72
|
+
}
|
|
73
|
+
const strength = words === 12 ? 128 : 256;
|
|
74
|
+
return bip39.generateMnemonic(wordlist, strength);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate a mnemonic phrase
|
|
79
|
+
* @param {string} mnemonic - BIP39 mnemonic phrase
|
|
80
|
+
* @returns {boolean} Whether the mnemonic is valid
|
|
81
|
+
*/
|
|
82
|
+
export function validateMnemonic(mnemonic) {
|
|
83
|
+
if (!mnemonic || typeof mnemonic !== 'string') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return bip39.validateMnemonic(mnemonic.trim(), wordlist);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Derive seed from mnemonic
|
|
91
|
+
* @param {string} mnemonic - BIP39 mnemonic phrase
|
|
92
|
+
* @returns {Uint8Array} Seed bytes
|
|
93
|
+
*/
|
|
94
|
+
function mnemonicToSeed(mnemonic) {
|
|
95
|
+
return bip39.mnemonicToSeedSync(mnemonic.trim());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get derivation path for a chain
|
|
100
|
+
* @param {string} chain - Chain code
|
|
101
|
+
* @param {number} [index=0] - Address index
|
|
102
|
+
* @returns {string} BIP44 derivation path
|
|
103
|
+
*/
|
|
104
|
+
export function getDerivationPath(chain, index = 0) {
|
|
105
|
+
const coinType = COIN_TYPES[chain];
|
|
106
|
+
if (coinType === undefined) {
|
|
107
|
+
throw new Error(`Unsupported chain: ${chain}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// BIP44 path: m / purpose' / coin_type' / account' / change / address_index
|
|
111
|
+
if (chain === 'SOL' || chain.startsWith('USDC_SOL') || chain.startsWith('USDT_SOL')) {
|
|
112
|
+
// Solana uses different derivation
|
|
113
|
+
return `m/44'/${coinType}'/${index}'/0'`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return `m/44'/${coinType}'/0'/0/${index}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Derive key pair from seed for a specific chain
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
function deriveKeyPair(seed, chain, index = 0) {
|
|
124
|
+
const path = getDerivationPath(chain, index);
|
|
125
|
+
const hdKey = HDKey.fromMasterSeed(seed);
|
|
126
|
+
const derived = hdKey.derive(path);
|
|
127
|
+
|
|
128
|
+
if (!derived.privateKey) {
|
|
129
|
+
throw new Error('Failed to derive private key');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
privateKey: derived.privateKey,
|
|
134
|
+
publicKey: derived.publicKey,
|
|
135
|
+
path,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get public key hex from seed for secp256k1 chains
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
function getSecp256k1PublicKey(seed) {
|
|
144
|
+
// Use ETH derivation for the master secp256k1 key
|
|
145
|
+
const hdKey = HDKey.fromMasterSeed(seed);
|
|
146
|
+
const derived = hdKey.derive("m/44'/60'/0'/0/0");
|
|
147
|
+
return bytesToHex(derived.publicKey);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Sign a message with secp256k1 private key
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
function signMessage(message, privateKey) {
|
|
155
|
+
const messageHash = sha256(new TextEncoder().encode(message));
|
|
156
|
+
const signature = secp256k1.sign(messageHash, privateKey);
|
|
157
|
+
// Handle different noble-curves versions:
|
|
158
|
+
// v1.x returns Signature object with toCompactHex()
|
|
159
|
+
// v2.x returns raw Uint8Array directly
|
|
160
|
+
if (signature instanceof Uint8Array) {
|
|
161
|
+
return bytesToHex(signature);
|
|
162
|
+
}
|
|
163
|
+
if (typeof signature.toCompactHex === 'function') {
|
|
164
|
+
return signature.toCompactHex();
|
|
165
|
+
}
|
|
166
|
+
// Fallback: try toCompactRawBytes
|
|
167
|
+
return bytesToHex(signature.toCompactRawBytes());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Derive address placeholder - actual address derivation happens on client
|
|
172
|
+
* This returns the public key that the server can use to verify ownership
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
function deriveAddressInfo(seed, chain, index = 0) {
|
|
176
|
+
const { publicKey, path } = deriveKeyPair(seed, chain, index);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
chain,
|
|
180
|
+
publicKey: bytesToHex(publicKey),
|
|
181
|
+
derivation_path: path,
|
|
182
|
+
derivation_index: index,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* WalletClient - Manages wallet operations
|
|
188
|
+
*
|
|
189
|
+
* The wallet client handles:
|
|
190
|
+
* - Local key derivation (seed never leaves client)
|
|
191
|
+
* - Server registration of public keys/addresses
|
|
192
|
+
* - Authenticated API calls using signature-based auth
|
|
193
|
+
*/
|
|
194
|
+
export class WalletClient {
|
|
195
|
+
#mnemonic;
|
|
196
|
+
#seed;
|
|
197
|
+
#walletId;
|
|
198
|
+
#authToken;
|
|
199
|
+
#baseUrl;
|
|
200
|
+
#timeout;
|
|
201
|
+
#publicKeySecp256k1;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create a wallet client
|
|
205
|
+
* @private - Use WalletClient.create() or WalletClient.fromSeed() instead
|
|
206
|
+
*/
|
|
207
|
+
constructor(options = {}) {
|
|
208
|
+
this.#baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
209
|
+
this.#timeout = options.timeout || 30000;
|
|
210
|
+
this.#mnemonic = options.mnemonic || null;
|
|
211
|
+
this.#seed = options.seed || null;
|
|
212
|
+
this.#walletId = options.walletId || null;
|
|
213
|
+
this.#authToken = options.authToken || null;
|
|
214
|
+
this.#publicKeySecp256k1 = options.publicKeySecp256k1 || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create a new wallet with a fresh mnemonic
|
|
219
|
+
* @param {Object} options - Creation options
|
|
220
|
+
* @param {number} [options.words=12] - Number of words (12 or 24)
|
|
221
|
+
* @param {string[]} [options.chains] - Chains to derive initial addresses for
|
|
222
|
+
* @param {string} [options.baseUrl] - API base URL
|
|
223
|
+
* @param {number} [options.timeout] - Request timeout
|
|
224
|
+
* @returns {Promise<WalletClient>} Wallet client with fresh mnemonic
|
|
225
|
+
*/
|
|
226
|
+
static async create(options = {}) {
|
|
227
|
+
const words = options.words || 12;
|
|
228
|
+
const chains = options.chains || DEFAULT_CHAINS;
|
|
229
|
+
|
|
230
|
+
const mnemonic = generateMnemonic(words);
|
|
231
|
+
const seed = mnemonicToSeed(mnemonic);
|
|
232
|
+
const publicKeySecp256k1 = getSecp256k1PublicKey(seed);
|
|
233
|
+
|
|
234
|
+
const client = new WalletClient({
|
|
235
|
+
baseUrl: options.baseUrl,
|
|
236
|
+
timeout: options.timeout,
|
|
237
|
+
mnemonic,
|
|
238
|
+
seed,
|
|
239
|
+
publicKeySecp256k1,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Register wallet with server
|
|
243
|
+
const initialAddresses = chains.map(chain => {
|
|
244
|
+
const info = deriveAddressInfo(seed, chain, 0);
|
|
245
|
+
// For registration, we need to provide a placeholder address
|
|
246
|
+
// The actual address would be derived client-side with full implementation
|
|
247
|
+
return {
|
|
248
|
+
chain: info.chain,
|
|
249
|
+
address: info.publicKey.slice(0, 42), // Placeholder - real impl derives actual address
|
|
250
|
+
derivation_path: info.derivation_path,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const result = await client.#request('/web-wallet/create', {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
public_key_secp256k1: publicKeySecp256k1,
|
|
258
|
+
initial_addresses: initialAddresses,
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
client.#walletId = result.wallet_id;
|
|
263
|
+
|
|
264
|
+
return client;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Import an existing wallet from a mnemonic
|
|
269
|
+
* @param {string} mnemonic - BIP39 mnemonic phrase
|
|
270
|
+
* @param {Object} options - Import options
|
|
271
|
+
* @param {string[]} [options.chains] - Chains to derive addresses for
|
|
272
|
+
* @param {string} [options.baseUrl] - API base URL
|
|
273
|
+
* @param {number} [options.timeout] - Request timeout
|
|
274
|
+
* @returns {Promise<WalletClient>} Wallet client with imported mnemonic
|
|
275
|
+
*/
|
|
276
|
+
static async fromSeed(mnemonic, options = {}) {
|
|
277
|
+
if (!validateMnemonic(mnemonic)) {
|
|
278
|
+
throw new Error('Invalid mnemonic phrase');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const chains = options.chains || DEFAULT_CHAINS;
|
|
282
|
+
const seed = mnemonicToSeed(mnemonic);
|
|
283
|
+
const publicKeySecp256k1 = getSecp256k1PublicKey(seed);
|
|
284
|
+
|
|
285
|
+
const client = new WalletClient({
|
|
286
|
+
baseUrl: options.baseUrl,
|
|
287
|
+
timeout: options.timeout,
|
|
288
|
+
mnemonic: mnemonic.trim(),
|
|
289
|
+
seed,
|
|
290
|
+
publicKeySecp256k1,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Create proof of ownership
|
|
294
|
+
const proofMessage = `CoinPay Wallet Import: ${Date.now()}`;
|
|
295
|
+
const { privateKey } = deriveKeyPair(seed, 'ETH', 0);
|
|
296
|
+
const signature = signMessage(proofMessage, privateKey);
|
|
297
|
+
|
|
298
|
+
// Derive addresses for registration
|
|
299
|
+
const addresses = chains.map(chain => {
|
|
300
|
+
const info = deriveAddressInfo(seed, chain, 0);
|
|
301
|
+
return {
|
|
302
|
+
chain: info.chain,
|
|
303
|
+
address: info.publicKey.slice(0, 42), // Placeholder
|
|
304
|
+
derivation_path: info.derivation_path,
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Register/import wallet with server
|
|
309
|
+
const result = await client.#request('/web-wallet/import', {
|
|
310
|
+
method: 'POST',
|
|
311
|
+
body: JSON.stringify({
|
|
312
|
+
public_key_secp256k1: publicKeySecp256k1,
|
|
313
|
+
addresses,
|
|
314
|
+
proof_of_ownership: {
|
|
315
|
+
message: proofMessage,
|
|
316
|
+
signature,
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
client.#walletId = result.wallet_id;
|
|
322
|
+
|
|
323
|
+
return client;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Make an API request
|
|
328
|
+
* @private
|
|
329
|
+
*/
|
|
330
|
+
async #request(endpoint, options = {}) {
|
|
331
|
+
const url = `${this.#baseUrl}${endpoint}`;
|
|
332
|
+
const controller = new AbortController();
|
|
333
|
+
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
|
|
334
|
+
|
|
335
|
+
const headers = {
|
|
336
|
+
'Content-Type': 'application/json',
|
|
337
|
+
...options.headers,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Add auth token if we have one
|
|
341
|
+
if (this.#authToken) {
|
|
342
|
+
headers['Authorization'] = `Bearer ${this.#authToken}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const response = await fetch(url, {
|
|
347
|
+
...options,
|
|
348
|
+
signal: controller.signal,
|
|
349
|
+
headers,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const data = await response.json();
|
|
353
|
+
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
const error = new Error(data.error || `HTTP ${response.status}`);
|
|
356
|
+
error.status = response.status;
|
|
357
|
+
error.code = data.code;
|
|
358
|
+
error.response = data;
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return data;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (error.name === 'AbortError') {
|
|
365
|
+
throw new Error(`Request timeout after ${this.#timeout}ms`);
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
} finally {
|
|
369
|
+
clearTimeout(timeoutId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Authenticate with the server to get a JWT token
|
|
375
|
+
* @returns {Promise<void>}
|
|
376
|
+
*/
|
|
377
|
+
async authenticate() {
|
|
378
|
+
if (!this.#walletId || !this.#seed) {
|
|
379
|
+
throw new Error('Wallet not initialized. Use create() or fromSeed() first.');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Get challenge
|
|
383
|
+
const { challenge } = await this.#request(`/web-wallet/auth/challenge`, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
body: JSON.stringify({ wallet_id: this.#walletId }),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Sign challenge
|
|
389
|
+
const { privateKey } = deriveKeyPair(this.#seed, 'ETH', 0);
|
|
390
|
+
const signature = signMessage(challenge, privateKey);
|
|
391
|
+
|
|
392
|
+
// Verify and get token
|
|
393
|
+
const result = await this.#request('/web-wallet/auth/verify', {
|
|
394
|
+
method: 'POST',
|
|
395
|
+
body: JSON.stringify({
|
|
396
|
+
wallet_id: this.#walletId,
|
|
397
|
+
challenge_id: result.challenge_id,
|
|
398
|
+
signature,
|
|
399
|
+
}),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
this.#authToken = result.auth_token;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get the mnemonic phrase (for backup)
|
|
407
|
+
* @returns {string|null} Mnemonic phrase
|
|
408
|
+
*/
|
|
409
|
+
getMnemonic() {
|
|
410
|
+
return this.#mnemonic;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get the wallet ID
|
|
415
|
+
* @returns {string|null} Wallet ID
|
|
416
|
+
*/
|
|
417
|
+
getWalletId() {
|
|
418
|
+
return this.#walletId;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get wallet info
|
|
423
|
+
* @returns {Promise<Object>} Wallet information
|
|
424
|
+
*/
|
|
425
|
+
async getInfo() {
|
|
426
|
+
if (!this.#walletId) {
|
|
427
|
+
throw new Error('Wallet not initialized');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return this.#request(`/web-wallet/${this.#walletId}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get all addresses for this wallet
|
|
435
|
+
* @param {Object} [options] - Query options
|
|
436
|
+
* @param {string} [options.chain] - Filter by chain
|
|
437
|
+
* @param {boolean} [options.activeOnly=true] - Only return active addresses
|
|
438
|
+
* @returns {Promise<Object>} Address list
|
|
439
|
+
*/
|
|
440
|
+
async getAddresses(options = {}) {
|
|
441
|
+
if (!this.#walletId) {
|
|
442
|
+
throw new Error('Wallet not initialized');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const params = new URLSearchParams();
|
|
446
|
+
if (options.chain) params.set('chain', options.chain);
|
|
447
|
+
if (options.activeOnly !== false) params.set('active_only', 'true');
|
|
448
|
+
|
|
449
|
+
const query = params.toString();
|
|
450
|
+
const endpoint = `/web-wallet/${this.#walletId}/addresses${query ? `?${query}` : ''}`;
|
|
451
|
+
|
|
452
|
+
return this.#request(endpoint);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Derive a new address for a chain
|
|
457
|
+
* @param {string} chain - Blockchain chain code
|
|
458
|
+
* @param {number} [index=0] - Derivation index
|
|
459
|
+
* @returns {Promise<Object>} Derived address info
|
|
460
|
+
*/
|
|
461
|
+
async deriveAddress(chain, index = 0) {
|
|
462
|
+
if (!this.#walletId || !this.#seed) {
|
|
463
|
+
throw new Error('Wallet not initialized');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const info = deriveAddressInfo(this.#seed, chain, index);
|
|
467
|
+
|
|
468
|
+
const result = await this.#request(`/web-wallet/${this.#walletId}/derive`, {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
body: JSON.stringify({
|
|
471
|
+
chain: info.chain,
|
|
472
|
+
address: info.publicKey.slice(0, 42), // Placeholder
|
|
473
|
+
derivation_index: info.derivation_index,
|
|
474
|
+
derivation_path: info.derivation_path,
|
|
475
|
+
}),
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Derive addresses for any missing chains
|
|
483
|
+
* @param {string[]} [targetChains] - Chains to check (default: all supported)
|
|
484
|
+
* @returns {Promise<Object[]>} Newly derived addresses
|
|
485
|
+
*/
|
|
486
|
+
async deriveMissingChains(targetChains) {
|
|
487
|
+
const chains = targetChains || Object.keys(WalletChain);
|
|
488
|
+
const { addresses } = await this.getAddresses({ activeOnly: true });
|
|
489
|
+
|
|
490
|
+
const existingChains = new Set(addresses.map(a => a.chain));
|
|
491
|
+
const missingChains = chains.filter(c => !existingChains.has(c));
|
|
492
|
+
|
|
493
|
+
const results = [];
|
|
494
|
+
for (const chain of missingChains) {
|
|
495
|
+
try {
|
|
496
|
+
const result = await this.deriveAddress(chain, 0);
|
|
497
|
+
results.push(result);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.warn(`Failed to derive ${chain}: ${error.message}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return results;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Get all balances for this wallet
|
|
508
|
+
* @param {Object} [options] - Query options
|
|
509
|
+
* @param {string} [options.chain] - Filter by chain
|
|
510
|
+
* @param {boolean} [options.refresh=false] - Force refresh from blockchain
|
|
511
|
+
* @returns {Promise<Object>} Balances
|
|
512
|
+
*/
|
|
513
|
+
async getBalances(options = {}) {
|
|
514
|
+
if (!this.#walletId) {
|
|
515
|
+
throw new Error('Wallet not initialized');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const params = new URLSearchParams();
|
|
519
|
+
if (options.chain) params.set('chain', options.chain);
|
|
520
|
+
if (options.refresh) params.set('refresh', 'true');
|
|
521
|
+
|
|
522
|
+
const query = params.toString();
|
|
523
|
+
const endpoint = `/web-wallet/${this.#walletId}/balances${query ? `?${query}` : ''}`;
|
|
524
|
+
|
|
525
|
+
return this.#request(endpoint);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get balance for a specific chain
|
|
530
|
+
* @param {string} chain - Chain code
|
|
531
|
+
* @returns {Promise<Object>} Balance for the chain
|
|
532
|
+
*/
|
|
533
|
+
async getBalance(chain) {
|
|
534
|
+
return this.getBalances({ chain });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Send a transaction
|
|
539
|
+
* @param {Object} options - Send options
|
|
540
|
+
* @param {string} options.chain - Target chain
|
|
541
|
+
* @param {string} options.to - Recipient address
|
|
542
|
+
* @param {string} options.amount - Amount to send
|
|
543
|
+
* @param {string} [options.priority='medium'] - Fee priority (low/medium/high)
|
|
544
|
+
* @returns {Promise<Object>} Transaction result
|
|
545
|
+
*/
|
|
546
|
+
async send(options) {
|
|
547
|
+
if (!this.#walletId || !this.#seed) {
|
|
548
|
+
throw new Error('Wallet not initialized');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const { chain, to, amount, priority = 'medium' } = options;
|
|
552
|
+
|
|
553
|
+
if (!chain || !to || !amount) {
|
|
554
|
+
throw new Error('chain, to, and amount are required');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Get our address for this chain
|
|
558
|
+
const { addresses } = await this.getAddresses({ chain });
|
|
559
|
+
if (!addresses || addresses.length === 0) {
|
|
560
|
+
throw new Error(`No address found for chain ${chain}`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const fromAddress = addresses[0].address;
|
|
564
|
+
|
|
565
|
+
// Step 1: Prepare the transaction
|
|
566
|
+
const prepareResult = await this.#request(`/web-wallet/${this.#walletId}/prepare-tx`, {
|
|
567
|
+
method: 'POST',
|
|
568
|
+
body: JSON.stringify({
|
|
569
|
+
from_address: fromAddress,
|
|
570
|
+
to_address: to,
|
|
571
|
+
chain,
|
|
572
|
+
amount,
|
|
573
|
+
priority,
|
|
574
|
+
}),
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Step 2: Sign the transaction locally
|
|
578
|
+
// Note: This is a simplified version - real implementation would need
|
|
579
|
+
// chain-specific signing logic
|
|
580
|
+
const { privateKey } = deriveKeyPair(this.#seed, chain, 0);
|
|
581
|
+
const unsignedTx = prepareResult.unsigned_tx;
|
|
582
|
+
|
|
583
|
+
// For EVM chains, sign the transaction hash
|
|
584
|
+
// For BTC, sign each input
|
|
585
|
+
// This is simplified - real implementation needs chain-specific logic
|
|
586
|
+
const signedTx = signMessage(unsignedTx, privateKey);
|
|
587
|
+
|
|
588
|
+
// Step 3: Broadcast the signed transaction
|
|
589
|
+
const broadcastResult = await this.#request(`/web-wallet/${this.#walletId}/broadcast`, {
|
|
590
|
+
method: 'POST',
|
|
591
|
+
body: JSON.stringify({
|
|
592
|
+
tx_id: prepareResult.tx_id,
|
|
593
|
+
signed_tx: signedTx,
|
|
594
|
+
chain,
|
|
595
|
+
}),
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
return broadcastResult;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get transaction history
|
|
603
|
+
* @param {Object} [options] - Query options
|
|
604
|
+
* @param {string} [options.chain] - Filter by chain
|
|
605
|
+
* @param {string} [options.direction] - Filter by direction (incoming/outgoing)
|
|
606
|
+
* @param {number} [options.limit=50] - Number of results
|
|
607
|
+
* @param {number} [options.offset=0] - Pagination offset
|
|
608
|
+
* @returns {Promise<Object>} Transaction history
|
|
609
|
+
*/
|
|
610
|
+
async getHistory(options = {}) {
|
|
611
|
+
if (!this.#walletId) {
|
|
612
|
+
throw new Error('Wallet not initialized');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const params = new URLSearchParams();
|
|
616
|
+
if (options.chain) params.set('chain', options.chain);
|
|
617
|
+
if (options.direction) params.set('direction', options.direction);
|
|
618
|
+
if (options.limit) params.set('limit', String(options.limit));
|
|
619
|
+
if (options.offset) params.set('offset', String(options.offset));
|
|
620
|
+
|
|
621
|
+
const query = params.toString();
|
|
622
|
+
const endpoint = `/web-wallet/${this.#walletId}/transactions${query ? `?${query}` : ''}`;
|
|
623
|
+
|
|
624
|
+
return this.#request(endpoint);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Estimate transaction fee
|
|
629
|
+
* @param {string} chain - Target chain
|
|
630
|
+
* @param {string} [to] - Recipient address (optional, for more accurate estimate)
|
|
631
|
+
* @param {string} [amount] - Amount (optional, for more accurate estimate)
|
|
632
|
+
* @returns {Promise<Object>} Fee estimates
|
|
633
|
+
*/
|
|
634
|
+
async estimateFee(chain, to, amount) {
|
|
635
|
+
if (!this.#walletId) {
|
|
636
|
+
throw new Error('Wallet not initialized');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const body = { chain };
|
|
640
|
+
if (to) body.to_address = to;
|
|
641
|
+
if (amount) body.amount = amount;
|
|
642
|
+
|
|
643
|
+
return this.#request(`/web-wallet/${this.#walletId}/estimate-fee`, {
|
|
644
|
+
method: 'POST',
|
|
645
|
+
body: JSON.stringify(body),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Encrypt and backup the seed phrase
|
|
651
|
+
* @param {string} password - Encryption password
|
|
652
|
+
* @returns {Promise<string>} Encrypted seed (base64)
|
|
653
|
+
*/
|
|
654
|
+
async backupSeed(password) {
|
|
655
|
+
if (!this.#mnemonic) {
|
|
656
|
+
throw new Error('No mnemonic available');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!password || password.length < 8) {
|
|
660
|
+
throw new Error('Password must be at least 8 characters');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Simple encryption using Web Crypto API
|
|
664
|
+
const encoder = new TextEncoder();
|
|
665
|
+
const data = encoder.encode(this.#mnemonic);
|
|
666
|
+
|
|
667
|
+
// Derive key from password
|
|
668
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
669
|
+
'raw',
|
|
670
|
+
encoder.encode(password),
|
|
671
|
+
'PBKDF2',
|
|
672
|
+
false,
|
|
673
|
+
['deriveBits', 'deriveKey']
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
677
|
+
const key = await crypto.subtle.deriveKey(
|
|
678
|
+
{
|
|
679
|
+
name: 'PBKDF2',
|
|
680
|
+
salt,
|
|
681
|
+
iterations: 100000,
|
|
682
|
+
hash: 'SHA-256',
|
|
683
|
+
},
|
|
684
|
+
keyMaterial,
|
|
685
|
+
{ name: 'AES-GCM', length: 256 },
|
|
686
|
+
false,
|
|
687
|
+
['encrypt']
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
691
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
692
|
+
{ name: 'AES-GCM', iv },
|
|
693
|
+
key,
|
|
694
|
+
data
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// Combine salt + iv + encrypted data
|
|
698
|
+
const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
|
|
699
|
+
combined.set(salt, 0);
|
|
700
|
+
combined.set(iv, salt.length);
|
|
701
|
+
combined.set(new Uint8Array(encrypted), salt.length + iv.length);
|
|
702
|
+
|
|
703
|
+
// Return as base64
|
|
704
|
+
return btoa(String.fromCharCode(...combined));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Restore a seed from encrypted backup
|
|
710
|
+
* @param {string} encryptedBackup - Base64 encrypted backup
|
|
711
|
+
* @param {string} password - Decryption password
|
|
712
|
+
* @returns {Promise<string>} Decrypted mnemonic
|
|
713
|
+
*/
|
|
714
|
+
export async function restoreFromBackup(encryptedBackup, password) {
|
|
715
|
+
const encoder = new TextEncoder();
|
|
716
|
+
const decoder = new TextDecoder();
|
|
717
|
+
|
|
718
|
+
// Decode base64
|
|
719
|
+
const combined = Uint8Array.from(atob(encryptedBackup), c => c.charCodeAt(0));
|
|
720
|
+
|
|
721
|
+
// Extract salt, iv, and encrypted data
|
|
722
|
+
const salt = combined.slice(0, 16);
|
|
723
|
+
const iv = combined.slice(16, 28);
|
|
724
|
+
const encrypted = combined.slice(28);
|
|
725
|
+
|
|
726
|
+
// Derive key from password
|
|
727
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
728
|
+
'raw',
|
|
729
|
+
encoder.encode(password),
|
|
730
|
+
'PBKDF2',
|
|
731
|
+
false,
|
|
732
|
+
['deriveBits', 'deriveKey']
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
const key = await crypto.subtle.deriveKey(
|
|
736
|
+
{
|
|
737
|
+
name: 'PBKDF2',
|
|
738
|
+
salt,
|
|
739
|
+
iterations: 100000,
|
|
740
|
+
hash: 'SHA-256',
|
|
741
|
+
},
|
|
742
|
+
keyMaterial,
|
|
743
|
+
{ name: 'AES-GCM', length: 256 },
|
|
744
|
+
false,
|
|
745
|
+
['decrypt']
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
749
|
+
{ name: 'AES-GCM', iv },
|
|
750
|
+
key,
|
|
751
|
+
encrypted
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
return decoder.decode(decrypted);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export default WalletClient;
|