@profullstack/coinpay 0.4.0 → 0.4.2
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 +13 -9
- package/package.json +1 -1
- package/src/wallet.js +266 -7
package/bin/coinpay.js
CHANGED
|
@@ -823,12 +823,16 @@ async function handleWallet(subcommand, args, flags) {
|
|
|
823
823
|
print.info('Importing wallet...');
|
|
824
824
|
|
|
825
825
|
try {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
826
|
+
let walletId = null;
|
|
827
|
+
try {
|
|
828
|
+
const wallet = await WalletClient.fromSeed(mnemonic, {
|
|
829
|
+
chains,
|
|
830
|
+
baseUrl,
|
|
831
|
+
});
|
|
832
|
+
walletId = wallet.getWalletId();
|
|
833
|
+
} catch (serverErr) {
|
|
834
|
+
print.warn(`Server registration failed (wallet saved locally only): ${serverErr.message || serverErr}`);
|
|
835
|
+
}
|
|
832
836
|
|
|
833
837
|
// Save encrypted locally (unless --no-save)
|
|
834
838
|
if (!noSave) {
|
|
@@ -860,9 +864,9 @@ async function handleWallet(subcommand, args, flags) {
|
|
|
860
864
|
config.walletFile = walletFile;
|
|
861
865
|
saveConfig(config);
|
|
862
866
|
|
|
863
|
-
print.success(`Wallet imported: ${walletId}`);
|
|
867
|
+
print.success(walletId ? `Wallet imported (ID: ${walletId})` : 'Wallet imported (local only)');
|
|
864
868
|
} catch (error) {
|
|
865
|
-
print.error(error.message);
|
|
869
|
+
print.error(error.message || String(error));
|
|
866
870
|
}
|
|
867
871
|
break;
|
|
868
872
|
}
|
|
@@ -878,7 +882,7 @@ async function handleWallet(subcommand, args, flags) {
|
|
|
878
882
|
const mnemonic = await getDecryptedMnemonic(flags);
|
|
879
883
|
|
|
880
884
|
print.success('Wallet unlocked');
|
|
881
|
-
print.info(`Wallet ID: ${config.walletId || '
|
|
885
|
+
print.info(`Wallet ID: ${config.walletId || '(local only)'}`);
|
|
882
886
|
print.info(`Wallet file: ${walletFile}`);
|
|
883
887
|
|
|
884
888
|
if (flags.show) {
|
package/package.json
CHANGED
package/src/wallet.js
CHANGED
|
@@ -10,8 +10,13 @@ import * as bip39 from '@scure/bip39';
|
|
|
10
10
|
import { wordlist } from '@scure/bip39/wordlists/english';
|
|
11
11
|
import { HDKey } from '@scure/bip32';
|
|
12
12
|
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
13
|
-
import { sha256 } from '@noble/hashes/
|
|
14
|
-
import {
|
|
13
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
14
|
+
import { ripemd160 } from '@noble/hashes/ripemd160.js';
|
|
15
|
+
import { keccak_256 } from '@noble/hashes/sha3.js';
|
|
16
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
17
|
+
import { hmac } from '@noble/hashes/hmac.js';
|
|
18
|
+
import { sha512 } from '@noble/hashes/sha2.js';
|
|
19
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
15
20
|
|
|
16
21
|
const DEFAULT_BASE_URL = 'https://coinpayportal.com/api';
|
|
17
22
|
|
|
@@ -61,6 +66,248 @@ const COIN_TYPES = {
|
|
|
61
66
|
*/
|
|
62
67
|
const SECP256K1_CHAINS = ['BTC', 'BCH', 'ETH', 'POL', 'BNB', 'USDC_ETH', 'USDC_POL', 'USDT_ETH', 'USDT_POL'];
|
|
63
68
|
|
|
69
|
+
// ============================================================
|
|
70
|
+
// Address derivation helpers
|
|
71
|
+
// ============================================================
|
|
72
|
+
|
|
73
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Base58 encode a byte array
|
|
77
|
+
*/
|
|
78
|
+
function base58Encode(bytes) {
|
|
79
|
+
const digits = [0];
|
|
80
|
+
for (const byte of bytes) {
|
|
81
|
+
let carry = byte;
|
|
82
|
+
for (let j = 0; j < digits.length; j++) {
|
|
83
|
+
carry += digits[j] << 8;
|
|
84
|
+
digits[j] = carry % 58;
|
|
85
|
+
carry = (carry / 58) | 0;
|
|
86
|
+
}
|
|
87
|
+
while (carry > 0) {
|
|
88
|
+
digits.push(carry % 58);
|
|
89
|
+
carry = (carry / 58) | 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Leading zeros
|
|
93
|
+
let result = '';
|
|
94
|
+
for (const byte of bytes) {
|
|
95
|
+
if (byte === 0) result += BASE58_ALPHABET[0];
|
|
96
|
+
else break;
|
|
97
|
+
}
|
|
98
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
99
|
+
result += BASE58_ALPHABET[digits[i]];
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Base58Check encode (used for BTC addresses)
|
|
106
|
+
*/
|
|
107
|
+
function base58CheckEncode(version, payload) {
|
|
108
|
+
const data = new Uint8Array(1 + payload.length);
|
|
109
|
+
data[0] = version;
|
|
110
|
+
data.set(payload, 1);
|
|
111
|
+
const checksum = sha256(sha256(data)).slice(0, 4);
|
|
112
|
+
const full = new Uint8Array(data.length + 4);
|
|
113
|
+
full.set(data);
|
|
114
|
+
full.set(checksum, data.length);
|
|
115
|
+
return base58Encode(full);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Hash160: SHA256 then RIPEMD160
|
|
120
|
+
*/
|
|
121
|
+
function hash160(data) {
|
|
122
|
+
return ripemd160(sha256(data));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Derive BTC P2PKH address from compressed public key
|
|
127
|
+
*/
|
|
128
|
+
function deriveBTCAddress(compressedPubKey) {
|
|
129
|
+
const h = hash160(compressedPubKey);
|
|
130
|
+
return base58CheckEncode(0x00, h);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Derive BCH CashAddr from compressed public key
|
|
135
|
+
* Uses simplified CashAddr encoding (lowercase bech32-like with bitcoincash: prefix)
|
|
136
|
+
*/
|
|
137
|
+
function deriveBCHAddress(compressedPubKey) {
|
|
138
|
+
const h = hash160(compressedPubKey);
|
|
139
|
+
// CashAddr encoding
|
|
140
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
141
|
+
|
|
142
|
+
// Convert hash160 to 5-bit groups with version byte
|
|
143
|
+
// Version byte: 0 = P2PKH, 160-bit hash → type=0, hash_size=0 → version_byte = 0
|
|
144
|
+
const payload8 = new Uint8Array(1 + h.length);
|
|
145
|
+
payload8[0] = 0; // version byte
|
|
146
|
+
payload8.set(h, 1);
|
|
147
|
+
|
|
148
|
+
// Convert 8-bit to 5-bit
|
|
149
|
+
const payload5 = convertBits(payload8, 8, 5, true);
|
|
150
|
+
|
|
151
|
+
// Polymod checksum
|
|
152
|
+
const prefixData = expandPrefix('bitcoincash');
|
|
153
|
+
const checksumInput = new Uint8Array(prefixData.length + payload5.length + 8);
|
|
154
|
+
checksumInput.set(prefixData);
|
|
155
|
+
checksumInput.set(payload5, prefixData.length);
|
|
156
|
+
// Last 8 bytes are zeros for checksum calculation
|
|
157
|
+
const polymod = cashAddrPolymod(checksumInput) ^ 1;
|
|
158
|
+
|
|
159
|
+
const checksum = new Uint8Array(8);
|
|
160
|
+
for (let i = 0; i < 8; i++) {
|
|
161
|
+
checksum[i] = (polymod >> (5 * (7 - i))) & 0x1f;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let encoded = 'bitcoincash:';
|
|
165
|
+
for (const val of payload5) encoded += CHARSET[val];
|
|
166
|
+
for (const val of checksum) encoded += CHARSET[val];
|
|
167
|
+
|
|
168
|
+
return encoded;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function convertBits(data, fromBits, toBits, pad) {
|
|
172
|
+
let acc = 0;
|
|
173
|
+
let bits = 0;
|
|
174
|
+
const result = [];
|
|
175
|
+
const maxv = (1 << toBits) - 1;
|
|
176
|
+
for (const value of data) {
|
|
177
|
+
acc = (acc << fromBits) | value;
|
|
178
|
+
bits += fromBits;
|
|
179
|
+
while (bits >= toBits) {
|
|
180
|
+
bits -= toBits;
|
|
181
|
+
result.push((acc >> bits) & maxv);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (pad && bits > 0) {
|
|
185
|
+
result.push((acc << (toBits - bits)) & maxv);
|
|
186
|
+
}
|
|
187
|
+
return new Uint8Array(result);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function expandPrefix(prefix) {
|
|
191
|
+
const result = new Uint8Array(prefix.length + 1);
|
|
192
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
193
|
+
result[i] = prefix.charCodeAt(i) & 0x1f;
|
|
194
|
+
}
|
|
195
|
+
result[prefix.length] = 0;
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cashAddrPolymod(values) {
|
|
200
|
+
const GENERATORS = [
|
|
201
|
+
0x98f2bc8e61n, 0x79b76d99e2n, 0xf33e5fb3c4n, 0xae2eabe2a8n, 0x1e4f43e470n
|
|
202
|
+
];
|
|
203
|
+
let c = 1n;
|
|
204
|
+
for (const d of values) {
|
|
205
|
+
const c0 = c >> 35n;
|
|
206
|
+
c = ((c & 0x07ffffffffn) << 5n) ^ BigInt(d);
|
|
207
|
+
for (let i = 0; i < 5; i++) {
|
|
208
|
+
if ((c0 >> BigInt(i)) & 1n) {
|
|
209
|
+
c ^= GENERATORS[i];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return Number(c);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Derive ETH/EVM address from compressed public key bytes
|
|
218
|
+
*/
|
|
219
|
+
function deriveETHAddress(compressedPubKey) {
|
|
220
|
+
// Decompress: get uncompressed point (65 bytes: 04 || x || y)
|
|
221
|
+
const point = secp256k1.ProjectivePoint.fromHex(compressedPubKey);
|
|
222
|
+
const uncompressed = point.toRawBytes(false); // 65 bytes with 04 prefix
|
|
223
|
+
// Keccak256 of the 64 bytes (without 04 prefix)
|
|
224
|
+
const hash = keccak_256(uncompressed.slice(1));
|
|
225
|
+
// Last 20 bytes
|
|
226
|
+
const addr = bytesToHex(hash.slice(12));
|
|
227
|
+
return '0x' + addr;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* SLIP-0010 ed25519 key derivation from seed
|
|
232
|
+
* Solana uses ed25519, derived via SLIP-0010 (not BIP32 secp256k1)
|
|
233
|
+
*/
|
|
234
|
+
function deriveEd25519Key(seed, path) {
|
|
235
|
+
// SLIP-0010 master key derivation
|
|
236
|
+
const I = hmac(sha512, new TextEncoder().encode('ed25519 seed'), seed);
|
|
237
|
+
let key = I.slice(0, 32);
|
|
238
|
+
let chainCode = I.slice(32);
|
|
239
|
+
|
|
240
|
+
// Parse path
|
|
241
|
+
const segments = path.replace('m/', '').split('/');
|
|
242
|
+
for (const seg of segments) {
|
|
243
|
+
const hardened = seg.endsWith("'");
|
|
244
|
+
const index = parseInt(seg.replace("'", ''), 10);
|
|
245
|
+
if (!hardened) throw new Error('SLIP-0010 ed25519 only supports hardened derivation');
|
|
246
|
+
|
|
247
|
+
const indexBuf = new Uint8Array(4);
|
|
248
|
+
const val = (index | 0x80000000) >>> 0;
|
|
249
|
+
indexBuf[0] = (val >> 24) & 0xff;
|
|
250
|
+
indexBuf[1] = (val >> 16) & 0xff;
|
|
251
|
+
indexBuf[2] = (val >> 8) & 0xff;
|
|
252
|
+
indexBuf[3] = val & 0xff;
|
|
253
|
+
|
|
254
|
+
const data = new Uint8Array(1 + 32 + 4);
|
|
255
|
+
data[0] = 0x00;
|
|
256
|
+
data.set(key, 1);
|
|
257
|
+
data.set(indexBuf, 33);
|
|
258
|
+
|
|
259
|
+
const derived = hmac(sha512, chainCode, data);
|
|
260
|
+
key = derived.slice(0, 32);
|
|
261
|
+
chainCode = derived.slice(32);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return key;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Derive SOL address from seed using SLIP-0010 ed25519
|
|
269
|
+
*/
|
|
270
|
+
function deriveSOLAddress(seed, index = 0) {
|
|
271
|
+
const path = `m/44'/501'/${index}'/0'`;
|
|
272
|
+
const privateKey = deriveEd25519Key(seed, path);
|
|
273
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
274
|
+
return { address: base58Encode(publicKey), publicKey, privateKey, path };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Derive the blockchain address for a given chain from seed/public key
|
|
279
|
+
* @param {Uint8Array} seed - Master seed
|
|
280
|
+
* @param {string} chain - Chain code
|
|
281
|
+
* @param {number} index - Derivation index
|
|
282
|
+
* @returns {string} The derived address
|
|
283
|
+
*/
|
|
284
|
+
export function deriveAddress(seed, chain, index = 0) {
|
|
285
|
+
// Solana-based chains
|
|
286
|
+
if (chain === 'SOL' || chain === 'USDC_SOL' || chain === 'USDT_SOL') {
|
|
287
|
+
return deriveSOLAddress(seed, index).address;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// secp256k1-based chains
|
|
291
|
+
const { publicKey } = deriveKeyPair(seed, chain, index);
|
|
292
|
+
|
|
293
|
+
switch (chain) {
|
|
294
|
+
case 'BTC':
|
|
295
|
+
return deriveBTCAddress(publicKey);
|
|
296
|
+
case 'BCH':
|
|
297
|
+
return deriveBCHAddress(publicKey);
|
|
298
|
+
case 'ETH':
|
|
299
|
+
case 'POL':
|
|
300
|
+
case 'BNB':
|
|
301
|
+
case 'USDC_ETH':
|
|
302
|
+
case 'USDC_POL':
|
|
303
|
+
case 'USDT_ETH':
|
|
304
|
+
case 'USDT_POL':
|
|
305
|
+
return deriveETHAddress(publicKey);
|
|
306
|
+
default:
|
|
307
|
+
throw new Error(`Unsupported chain for address derivation: ${chain}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
64
311
|
/**
|
|
65
312
|
* Generate a new mnemonic phrase
|
|
66
313
|
* @param {number} [words=12] - Number of words (12 or 24)
|
|
@@ -173,11 +420,23 @@ function signMessage(message, privateKey) {
|
|
|
173
420
|
* @private
|
|
174
421
|
*/
|
|
175
422
|
function deriveAddressInfo(seed, chain, index = 0) {
|
|
176
|
-
const
|
|
423
|
+
const address = deriveAddress(seed, chain, index);
|
|
424
|
+
const path = getDerivationPath(chain, index);
|
|
425
|
+
|
|
426
|
+
// For SOL chains, the public key is derived differently (ed25519)
|
|
427
|
+
let publicKeyHex;
|
|
428
|
+
if (chain === 'SOL' || chain === 'USDC_SOL' || chain === 'USDT_SOL') {
|
|
429
|
+
const solInfo = deriveSOLAddress(seed, index);
|
|
430
|
+
publicKeyHex = bytesToHex(solInfo.publicKey);
|
|
431
|
+
} else {
|
|
432
|
+
const { publicKey } = deriveKeyPair(seed, chain, index);
|
|
433
|
+
publicKeyHex = bytesToHex(publicKey);
|
|
434
|
+
}
|
|
177
435
|
|
|
178
436
|
return {
|
|
179
437
|
chain,
|
|
180
|
-
|
|
438
|
+
address,
|
|
439
|
+
publicKey: publicKeyHex,
|
|
181
440
|
derivation_path: path,
|
|
182
441
|
derivation_index: index,
|
|
183
442
|
};
|
|
@@ -246,7 +505,7 @@ export class WalletClient {
|
|
|
246
505
|
// The actual address would be derived client-side with full implementation
|
|
247
506
|
return {
|
|
248
507
|
chain: info.chain,
|
|
249
|
-
address: info.
|
|
508
|
+
address: info.address,
|
|
250
509
|
derivation_path: info.derivation_path,
|
|
251
510
|
};
|
|
252
511
|
});
|
|
@@ -300,7 +559,7 @@ export class WalletClient {
|
|
|
300
559
|
const info = deriveAddressInfo(seed, chain, 0);
|
|
301
560
|
return {
|
|
302
561
|
chain: info.chain,
|
|
303
|
-
address: info.
|
|
562
|
+
address: info.address,
|
|
304
563
|
derivation_path: info.derivation_path,
|
|
305
564
|
};
|
|
306
565
|
});
|
|
@@ -469,7 +728,7 @@ export class WalletClient {
|
|
|
469
728
|
method: 'POST',
|
|
470
729
|
body: JSON.stringify({
|
|
471
730
|
chain: info.chain,
|
|
472
|
-
address: info.
|
|
731
|
+
address: info.address,
|
|
473
732
|
derivation_index: info.derivation_index,
|
|
474
733
|
derivation_path: info.derivation_path,
|
|
475
734
|
}),
|