@profullstack/coinpay 0.6.11 → 0.9.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/CHANGELOG.md +39 -0
- package/README.md +189 -0
- package/bin/coinpay.js +1869 -93
- package/package.json +41 -9
- package/src/auth.js +2 -1
- package/src/card-payments.js +23 -14
- package/src/client.d.ts +52 -0
- package/src/client.js +93 -9
- package/src/finances-tui.js +1005 -0
- package/src/finances.d.ts +333 -0
- package/src/finances.js +445 -0
- package/src/index.d.ts +37 -1
- package/src/index.js +67 -0
- package/src/invoices.d.ts +78 -0
- package/src/invoices.js +250 -0
- package/src/lightning.js +8 -3
- package/src/swap.js +36 -5
- package/src/taskmarket.js +329 -0
- package/src/tokens.d.ts +14 -0
- package/src/tokens.js +34 -0
- package/src/wallet.d.ts +11 -0
- package/src/wallet.js +283 -25
- package/src/webhooks.js +37 -3
- package/src/x402-browser.js +318 -0
- package/src/x402-pay-server.js +272 -0
- package/src/x402-v2.js +416 -0
- package/src/x402.js +164 -19
package/src/tokens.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CoinPayClient } from './client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* List checkout tokens for a business.
|
|
5
|
+
* @param {Object} params
|
|
6
|
+
* @param {string} [params.apiKey] - API key if not using client
|
|
7
|
+
* @param {CoinPayClient} [params.client] - Existing client
|
|
8
|
+
* @param {string} [params.businessId] - Business ID
|
|
9
|
+
* @param {boolean} [params.activeOnly] - Only return active wallets
|
|
10
|
+
* @returns {Promise<Object>} Tokens response
|
|
11
|
+
*/
|
|
12
|
+
export async function getTokens({ apiKey, client, businessId, activeOnly } = {}) {
|
|
13
|
+
const coinpay = client || new CoinPayClient({ apiKey });
|
|
14
|
+
return coinpay.getTokens({ businessId, activeOnly });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* List supported coins for a business.
|
|
19
|
+
* @param {Object} params
|
|
20
|
+
* @param {string} [params.apiKey] - API key if not using client
|
|
21
|
+
* @param {CoinPayClient} [params.client] - Existing client
|
|
22
|
+
* @param {string} [params.businessId] - Business ID
|
|
23
|
+
* @param {boolean} [params.activeOnly] - Only return active wallets
|
|
24
|
+
* @returns {Promise<Object>} Supported coins response
|
|
25
|
+
*/
|
|
26
|
+
export async function getSupportedCoins({ apiKey, client, businessId, activeOnly } = {}) {
|
|
27
|
+
const coinpay = client || new CoinPayClient({ apiKey });
|
|
28
|
+
return coinpay.getSupportedCoins({ businessId, activeOnly });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
getTokens,
|
|
33
|
+
getSupportedCoins,
|
|
34
|
+
};
|
package/src/wallet.d.ts
CHANGED
|
@@ -251,6 +251,17 @@ export declare function validateMnemonic(mnemonic: string): boolean;
|
|
|
251
251
|
*/
|
|
252
252
|
export declare function getDerivationPath(chain: string, index?: number): string;
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Derive the address for a chain from a raw BIP-39 seed.
|
|
256
|
+
* (Standalone counterpart to `WalletClient.deriveAddress`; exported from
|
|
257
|
+
* wallet.js but previously missing from these type declarations.)
|
|
258
|
+
*
|
|
259
|
+
* @param seed - Raw seed bytes, e.g. bip39.mnemonicToSeedSync(mnemonic)
|
|
260
|
+
* @param chain - Chain id (e.g. 'BTC', 'ETH', 'SOL', 'POL', 'BCH')
|
|
261
|
+
* @param index - Address index (default 0)
|
|
262
|
+
*/
|
|
263
|
+
export declare function deriveAddress(seed: Uint8Array, chain: string, index?: number): string;
|
|
264
|
+
|
|
254
265
|
/**
|
|
255
266
|
* Restore a seed from encrypted backup
|
|
256
267
|
*/
|
package/src/wallet.js
CHANGED
|
@@ -33,6 +33,7 @@ export const WalletChain = {
|
|
|
33
33
|
USDC_ETH: 'USDC_ETH',
|
|
34
34
|
USDC_POL: 'USDC_POL',
|
|
35
35
|
USDC_SOL: 'USDC_SOL',
|
|
36
|
+
USDC_BASE: 'USDC_BASE',
|
|
36
37
|
USDT_ETH: 'USDT_ETH',
|
|
37
38
|
USDT_POL: 'USDT_POL',
|
|
38
39
|
USDT_SOL: 'USDT_SOL',
|
|
@@ -56,6 +57,7 @@ const COIN_TYPES = {
|
|
|
56
57
|
USDC_ETH: 60,
|
|
57
58
|
USDC_POL: 60,
|
|
58
59
|
USDC_SOL: 501,
|
|
60
|
+
USDC_BASE: 60, // Base is EVM, uses ETH path — same address as USDC_ETH
|
|
59
61
|
USDT_ETH: 60,
|
|
60
62
|
USDT_POL: 60,
|
|
61
63
|
USDT_SOL: 501,
|
|
@@ -64,7 +66,7 @@ const COIN_TYPES = {
|
|
|
64
66
|
/**
|
|
65
67
|
* Chains that use secp256k1 curve (vs ed25519 for Solana)
|
|
66
68
|
*/
|
|
67
|
-
const SECP256K1_CHAINS = ['BTC', 'BCH', 'ETH', 'POL', 'BNB', 'USDC_ETH', 'USDC_POL', 'USDT_ETH', 'USDT_POL'];
|
|
69
|
+
const SECP256K1_CHAINS = ['BTC', 'BCH', 'ETH', 'POL', 'BNB', 'USDC_ETH', 'USDC_POL', 'USDC_BASE', 'USDT_ETH', 'USDT_POL'];
|
|
68
70
|
|
|
69
71
|
// ============================================================
|
|
70
72
|
// Address derivation helpers
|
|
@@ -305,6 +307,7 @@ export function deriveAddress(seed, chain, index = 0) {
|
|
|
305
307
|
case 'BNB':
|
|
306
308
|
case 'USDC_ETH':
|
|
307
309
|
case 'USDC_POL':
|
|
310
|
+
case 'USDC_BASE':
|
|
308
311
|
case 'USDT_ETH':
|
|
309
312
|
case 'USDT_POL':
|
|
310
313
|
return deriveETHAddress(publicKey);
|
|
@@ -404,8 +407,17 @@ function getSecp256k1PublicKey(seed) {
|
|
|
404
407
|
* @private
|
|
405
408
|
*/
|
|
406
409
|
function signMessage(message, privateKey) {
|
|
407
|
-
|
|
408
|
-
|
|
410
|
+
// Signed over the RAW message bytes, not sha256(message).
|
|
411
|
+
//
|
|
412
|
+
// This used to hash first. The server verifies proofs by passing the raw
|
|
413
|
+
// encoded message straight to `secp256k1.verify` (see
|
|
414
|
+
// web-wallet/auth.ts::verifySecp256k1Signature), and noble treats that
|
|
415
|
+
// argument as the thing that was signed — so a signature over the digest
|
|
416
|
+
// verifies false against the message. Every proof of ownership this SDK
|
|
417
|
+
// produced was rejected, which meant `WalletClient.fromSeed()` could not
|
|
418
|
+
// import a wallet at all. Found while wiring the same proof into
|
|
419
|
+
// `create()` for V-04.
|
|
420
|
+
const signature = secp256k1.sign(new TextEncoder().encode(message), privateKey);
|
|
409
421
|
// Handle different noble-curves versions:
|
|
410
422
|
// v1.x returns Signature object with toCompactHex()
|
|
411
423
|
// v2.x returns raw Uint8Array directly
|
|
@@ -419,6 +431,160 @@ function signMessage(message, privateKey) {
|
|
|
419
431
|
return bytesToHex(signature.toCompactRawBytes());
|
|
420
432
|
}
|
|
421
433
|
|
|
434
|
+
// ================================================================
|
|
435
|
+
// Arbitrary tx / message signing
|
|
436
|
+
// ----------------------------------------------------------------
|
|
437
|
+
// These primitives let callers (like b1dz.com's DEX engine) sign
|
|
438
|
+
// EVM contract calls and Solana transactions that CoinPay's built-in
|
|
439
|
+
// `send` flow doesn't cover (e.g., Uniswap swaps, Jupiter routes).
|
|
440
|
+
//
|
|
441
|
+
// The API intentionally stops at the cryptographic primitive:
|
|
442
|
+
// - EVM: caller builds the unsigned tx with their own library
|
|
443
|
+
// (viem / ethers / ethers-v5), hashes it per EIP-1559 or
|
|
444
|
+
// EIP-155, hands us the 32-byte digest, gets back a
|
|
445
|
+
// recoverable signature
|
|
446
|
+
// - Solana: caller builds the unsigned Transaction / VersionedMessage,
|
|
447
|
+
// hands us the raw message bytes, gets back an ed25519
|
|
448
|
+
// signature they can attach to the tx
|
|
449
|
+
//
|
|
450
|
+
// This keeps the crypto surface minimal and correct. We don't try to
|
|
451
|
+
// re-implement RLP, EIP-712, or Solana's transaction encoder inside
|
|
452
|
+
// CoinPay — those live in the caller's toolchain.
|
|
453
|
+
// ================================================================
|
|
454
|
+
|
|
455
|
+
/** Chains that sign via secp256k1 (32-byte digest → recoverable sig). */
|
|
456
|
+
function isSecp256k1Chain(chain) {
|
|
457
|
+
return SECP256K1_CHAINS.includes(chain);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Chains that sign via ed25519 (arbitrary message bytes → 64-byte sig). */
|
|
461
|
+
function isEd25519Chain(chain) {
|
|
462
|
+
return chain === 'SOL' || chain === 'USDC_SOL' || chain === 'USDT_SOL';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function normalizeHexInput(hex, label = 'hex') {
|
|
466
|
+
if (typeof hex !== 'string') throw new Error(`${label} must be a string`);
|
|
467
|
+
const stripped = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
468
|
+
if (stripped.length === 0) throw new Error(`${label} is empty`);
|
|
469
|
+
if (stripped.length % 2 !== 0) throw new Error(`${label} has odd length`);
|
|
470
|
+
if (!/^[0-9a-fA-F]+$/.test(stripped)) throw new Error(`${label} contains non-hex chars`);
|
|
471
|
+
return hexToBytes(stripped);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Sign a 32-byte digest with a secp256k1 chain's derived key.
|
|
476
|
+
* Returns a 65-byte recoverable signature formatted as `r(32) || s(32) || v(1)`
|
|
477
|
+
* where v is the low recovery id (0 or 1). This is the layout every
|
|
478
|
+
* EVM client (viem, ethers v6) expects for post-typed-transaction
|
|
479
|
+
* (EIP-1559 / EIP-2930) signatures.
|
|
480
|
+
*
|
|
481
|
+
* For legacy / EIP-155 transactions the caller is responsible for
|
|
482
|
+
* converting v to `chainId * 2 + 35 + recId` after receiving the sig.
|
|
483
|
+
* For EIP-191 (personal_sign) the caller adds 27.
|
|
484
|
+
*
|
|
485
|
+
* @param {Uint8Array} seed - Master seed
|
|
486
|
+
* @param {string} chain - BIP44 chain code (ETH, POL, BNB, ...)
|
|
487
|
+
* @param {Uint8Array} digest - Exactly 32 bytes
|
|
488
|
+
* @returns {string} `0x`-prefixed 65-byte hex signature
|
|
489
|
+
*/
|
|
490
|
+
export function signDigestSecp256k1(seed, chain, digest) {
|
|
491
|
+
if (!isSecp256k1Chain(chain)) {
|
|
492
|
+
throw new Error(`chain ${chain} does not use secp256k1`);
|
|
493
|
+
}
|
|
494
|
+
if (!(digest instanceof Uint8Array) || digest.length !== 32) {
|
|
495
|
+
throw new Error('digest must be a 32-byte Uint8Array');
|
|
496
|
+
}
|
|
497
|
+
const { privateKey } = deriveKeyPair(seed, chain, 0);
|
|
498
|
+
|
|
499
|
+
// noble-curves has two incompatible signing APIs across major versions:
|
|
500
|
+
// v1.x: sign(hash, key) → Signature object with .r, .s, .recovery
|
|
501
|
+
// v2.x: sign(hash, key, { format: 'recovered' }) → 65-byte Uint8Array
|
|
502
|
+
// [recovery, r(32), s(32)]
|
|
503
|
+
// Detect at runtime so this works with whichever version is pinned.
|
|
504
|
+
// Requesting 'recovered' on v1 is harmless — v1 ignores the option
|
|
505
|
+
// and still returns a Signature object.
|
|
506
|
+
const raw = secp256k1.sign(digest, privateKey, { format: 'recovered' });
|
|
507
|
+
let r, s, recovery;
|
|
508
|
+
if (raw instanceof Uint8Array && raw.length === 65) {
|
|
509
|
+
recovery = raw[0] & 0x01;
|
|
510
|
+
r = raw.subarray(1, 33);
|
|
511
|
+
s = raw.subarray(33, 65);
|
|
512
|
+
} else if (raw && typeof raw === 'object' && 'r' in raw && 's' in raw) {
|
|
513
|
+
if (typeof raw.recovery !== 'number') {
|
|
514
|
+
throw new Error('signature missing recovery id');
|
|
515
|
+
}
|
|
516
|
+
recovery = raw.recovery & 0x01;
|
|
517
|
+
r = bigIntToBytes32(raw.r);
|
|
518
|
+
s = bigIntToBytes32(raw.s);
|
|
519
|
+
} else {
|
|
520
|
+
throw new Error(`unexpected signature shape (type=${raw?.constructor?.name})`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const evmSig = new Uint8Array(65);
|
|
524
|
+
evmSig.set(r, 0);
|
|
525
|
+
evmSig.set(s, 32);
|
|
526
|
+
evmSig[64] = recovery;
|
|
527
|
+
return '0x' + bytesToHex(evmSig);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Big-endian 32-byte encoding of a bigint. */
|
|
531
|
+
function bigIntToBytes32(n) {
|
|
532
|
+
const out = new Uint8Array(32);
|
|
533
|
+
let x = BigInt(n);
|
|
534
|
+
for (let i = 31; i >= 0; i--) {
|
|
535
|
+
out[i] = Number(x & 0xffn);
|
|
536
|
+
x >>= 8n;
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Sign an EIP-191 "personal_sign" message. Wraps the message with the
|
|
543
|
+
* `\x19Ethereum Signed Message:\n<length>` prefix, keccak256-hashes,
|
|
544
|
+
* then signs the digest. Returns a 65-byte sig with v bumped to 27/28
|
|
545
|
+
* per EIP-191 convention (compatible with Solidity's ecrecover).
|
|
546
|
+
*
|
|
547
|
+
* @param {Uint8Array} seed
|
|
548
|
+
* @param {string} chain
|
|
549
|
+
* @param {string|Uint8Array} message - UTF-8 string or raw bytes
|
|
550
|
+
* @returns {string}
|
|
551
|
+
*/
|
|
552
|
+
export function signEip191Message(seed, chain, message) {
|
|
553
|
+
if (!isSecp256k1Chain(chain)) {
|
|
554
|
+
throw new Error(`chain ${chain} does not use secp256k1`);
|
|
555
|
+
}
|
|
556
|
+
const bytes = typeof message === 'string'
|
|
557
|
+
? new TextEncoder().encode(message)
|
|
558
|
+
: message;
|
|
559
|
+
const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${bytes.length}`);
|
|
560
|
+
const payload = new Uint8Array(prefix.length + bytes.length);
|
|
561
|
+
payload.set(prefix, 0);
|
|
562
|
+
payload.set(bytes, prefix.length);
|
|
563
|
+
const digest = keccak_256(payload);
|
|
564
|
+
const sig = hexToBytes(signDigestSecp256k1(seed, chain, digest).slice(2));
|
|
565
|
+
sig[64] = sig[64] + 27; // EIP-191 uses 27/28 for v
|
|
566
|
+
return '0x' + bytesToHex(sig);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Sign arbitrary Solana transaction message bytes with the derived
|
|
571
|
+
* ed25519 key. Returns a 64-byte signature that the caller attaches
|
|
572
|
+
* to the `VersionedTransaction.signatures` array.
|
|
573
|
+
*
|
|
574
|
+
* @param {Uint8Array} seed
|
|
575
|
+
* @param {Uint8Array} messageBytes - The serialized Solana tx message
|
|
576
|
+
* @param {number} [index=0] - Derivation index
|
|
577
|
+
* @returns {string} 64-byte hex signature
|
|
578
|
+
*/
|
|
579
|
+
export function signSolanaMessage(seed, messageBytes, index = 0) {
|
|
580
|
+
if (!(messageBytes instanceof Uint8Array)) {
|
|
581
|
+
throw new Error('messageBytes must be a Uint8Array');
|
|
582
|
+
}
|
|
583
|
+
const { privateKey } = deriveSOLAddress(seed, index);
|
|
584
|
+
const signature = ed25519.sign(messageBytes, privateKey);
|
|
585
|
+
return '0x' + bytesToHex(signature);
|
|
586
|
+
}
|
|
587
|
+
|
|
422
588
|
/**
|
|
423
589
|
* Derive address placeholder - actual address derivation happens on client
|
|
424
590
|
* This returns the public key that the server can use to verify ownership
|
|
@@ -515,11 +681,24 @@ export class WalletClient {
|
|
|
515
681
|
};
|
|
516
682
|
});
|
|
517
683
|
|
|
684
|
+
// V-04: /create accepted a public key and a list of on-chain addresses
|
|
685
|
+
// from anyone, with no proof that either was theirs, while /import required
|
|
686
|
+
// a signature for the same write. `wallet_addresses` is globally unique on
|
|
687
|
+
// (address, chain), so an unproved registration permanently denies an
|
|
688
|
+
// address to whoever actually owns it.
|
|
689
|
+
const proofMessage = `CoinPay Wallet Create: ${Date.now()}`;
|
|
690
|
+
const { privateKey: proofKey } = deriveKeyPair(seed, 'ETH', 0);
|
|
691
|
+
const proofSignature = signMessage(proofMessage, proofKey);
|
|
692
|
+
|
|
518
693
|
const result = await client.#request('/web-wallet/create', {
|
|
519
694
|
method: 'POST',
|
|
520
695
|
body: JSON.stringify({
|
|
521
696
|
public_key_secp256k1: publicKeySecp256k1,
|
|
522
697
|
initial_addresses: initialAddresses,
|
|
698
|
+
proof_of_ownership: {
|
|
699
|
+
message: proofMessage,
|
|
700
|
+
signature: proofSignature,
|
|
701
|
+
},
|
|
523
702
|
}),
|
|
524
703
|
});
|
|
525
704
|
|
|
@@ -840,30 +1019,109 @@ export class WalletClient {
|
|
|
840
1019
|
}),
|
|
841
1020
|
});
|
|
842
1021
|
|
|
843
|
-
// Step 2: Sign the transaction locally
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1022
|
+
// Step 2: Sign the transaction locally.
|
|
1023
|
+
//
|
|
1024
|
+
// NOT IMPLEMENTED, and it now says so instead of pretending.
|
|
1025
|
+
//
|
|
1026
|
+
// This used to call `signMessage(unsignedTx, privateKey)` — a generic
|
|
1027
|
+
// message signature over the JSON of the unsigned transaction — and post
|
|
1028
|
+
// the result as `signed_tx`. That is not a signed transaction on any chain:
|
|
1029
|
+
// a real one is the RLP-encoded (EVM), PSBT-finalised (BTC) or
|
|
1030
|
+
// wire-serialised (Solana) transaction carrying the signature in the right
|
|
1031
|
+
// field. What was produced could never be broadcast, so this method failed
|
|
1032
|
+
// for every integrator who called it — silently, by producing plausible
|
|
1033
|
+
// output that only failed later at the node.
|
|
1034
|
+
//
|
|
1035
|
+
// Failing here rather than guessing is deliberate. Serialising each chain
|
|
1036
|
+
// correctly needs the exact shape of `prepareResult.unsigned_tx` (defined by
|
|
1037
|
+
// the server, not this package) and verification against a live node.
|
|
1038
|
+
// Shipping an unverified serialiser would reintroduce the same class of bug
|
|
1039
|
+
// with more code behind it.
|
|
1040
|
+
//
|
|
1041
|
+
// The primitives to do this yourself are exported and do work:
|
|
1042
|
+
// `signDigest()` for EVM digests and `signMessage()` for EIP-191. Build the
|
|
1043
|
+
// transaction with viem / ethers / @solana/web3.js, sign the digest here,
|
|
1044
|
+
// and post the serialised result to the broadcast endpoint.
|
|
1045
|
+
void prepareResult;
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
'WalletClient.send() is not implemented: chain-specific transaction ' +
|
|
1048
|
+
'serialisation is not available in this package. Build and serialise the ' +
|
|
1049
|
+
'transaction with a chain library, sign the digest with signDigest(), and ' +
|
|
1050
|
+
'POST it to /web-wallet/:id/broadcast. See docs/sdk for an example.'
|
|
1051
|
+
);
|
|
865
1052
|
}
|
|
866
1053
|
|
|
1054
|
+
// ==========================================================
|
|
1055
|
+
// Arbitrary-transaction signing (PR: arbitrary-tx-signing)
|
|
1056
|
+
//
|
|
1057
|
+
// These methods expose raw cryptographic signing so callers can
|
|
1058
|
+
// use CoinPay as a wallet provider for flows CoinPay itself
|
|
1059
|
+
// doesn't implement (Uniswap swaps, Jupiter routes, EIP-712
|
|
1060
|
+
// typed data, etc.). The caller builds the unsigned tx with
|
|
1061
|
+
// their own library (viem / ethers / @solana/web3.js), passes
|
|
1062
|
+
// the digest or message bytes to CoinPay, and receives the
|
|
1063
|
+
// signature back. CoinPay never learns the tx semantics — it
|
|
1064
|
+
// just produces cryptographic output.
|
|
1065
|
+
//
|
|
1066
|
+
// SECURITY: these methods are intentionally unprivileged from
|
|
1067
|
+
// CoinPay's perspective. If you expose them to untrusted
|
|
1068
|
+
// callers you're handing over signing authority for the wallet.
|
|
1069
|
+
// The CLI `wallet sign-digest` / `sign-message` commands
|
|
1070
|
+
// require the GPG-encrypted wallet to be unlocked, same as
|
|
1071
|
+
// `wallet send`.
|
|
1072
|
+
// ==========================================================
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Sign a 32-byte digest with the wallet's secp256k1 key for the
|
|
1076
|
+
* given EVM chain. Returns a 65-byte recoverable signature laid
|
|
1077
|
+
* out as `r(32) || s(32) || v(1)`, where v is the raw recovery
|
|
1078
|
+
* bit (0 or 1). Suitable for EIP-1559 / EIP-2930 typed txs.
|
|
1079
|
+
*
|
|
1080
|
+
* For legacy EIP-155 signatures the caller converts v to
|
|
1081
|
+
* `chainId * 2 + 35 + v`. For EIP-191 personal_sign, use
|
|
1082
|
+
* `signMessage()` below instead.
|
|
1083
|
+
*
|
|
1084
|
+
* @param {{chain: string, digest: string|Uint8Array}} params
|
|
1085
|
+
* @returns {string} 0x-prefixed 65-byte signature hex
|
|
1086
|
+
*/
|
|
1087
|
+
signDigest({ chain, digest }) {
|
|
1088
|
+
if (!this.#seed) throw new Error('Wallet not initialized');
|
|
1089
|
+
const digestBytes = typeof digest === 'string'
|
|
1090
|
+
? normalizeHexInput(digest, 'digest')
|
|
1091
|
+
: digest;
|
|
1092
|
+
return signDigestSecp256k1(this.#seed, chain, digestBytes);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Sign an EIP-191 "personal_sign" message with the wallet's
|
|
1097
|
+
* secp256k1 key. Handles the `\x19Ethereum Signed Message:\n`
|
|
1098
|
+
* prefix internally.
|
|
1099
|
+
*
|
|
1100
|
+
* @param {{chain: string, message: string|Uint8Array}} params
|
|
1101
|
+
* @returns {string} 0x-prefixed 65-byte signature hex
|
|
1102
|
+
*/
|
|
1103
|
+
signMessage({ chain, message }) {
|
|
1104
|
+
if (!this.#seed) throw new Error('Wallet not initialized');
|
|
1105
|
+
return signEip191Message(this.#seed, chain, message);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Sign raw Solana transaction message bytes with the derived
|
|
1110
|
+
* ed25519 key. Returns a 64-byte signature the caller attaches
|
|
1111
|
+
* to their `VersionedTransaction.signatures` array.
|
|
1112
|
+
*
|
|
1113
|
+
* @param {{message: string|Uint8Array, index?: number}} params
|
|
1114
|
+
* `message` is either hex-encoded bytes or a raw Uint8Array.
|
|
1115
|
+
* @returns {string} 0x-prefixed 64-byte signature hex
|
|
1116
|
+
*/
|
|
1117
|
+
signSolanaMessage({ message, index = 0 }) {
|
|
1118
|
+
if (!this.#seed) throw new Error('Wallet not initialized');
|
|
1119
|
+
const bytes = typeof message === 'string'
|
|
1120
|
+
? normalizeHexInput(message, 'message')
|
|
1121
|
+
: message;
|
|
1122
|
+
return signSolanaMessage(this.#seed, bytes, index);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
867
1125
|
/**
|
|
868
1126
|
* Get transaction history
|
|
869
1127
|
* @param {Object} [options] - Query options
|
package/src/webhooks.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Webhook utilities for CoinPay SDK
|
|
3
|
+
*
|
|
4
|
+
* Contract:
|
|
5
|
+
* - Each business has ONE webhook URL + ONE webhook secret. Both crypto
|
|
6
|
+
* and card payment events route to the same endpoint, signed with the
|
|
7
|
+
* same secret. Distinguish rails via data.metadata.payment_rail
|
|
8
|
+
* ("crypto" or "card").
|
|
9
|
+
* - Header: X-CoinPay-Signature: t=<unix_seconds>,v1=<hex_hmac>
|
|
10
|
+
* - HMAC body: `${timestamp}.${rawBody}`
|
|
11
|
+
* - Algorithm: HMAC-SHA256
|
|
12
|
+
* - Tolerance: 300 seconds
|
|
13
|
+
*
|
|
14
|
+
* The server-side signer (src/lib/webhooks/service.ts) and the d0rz
|
|
15
|
+
* verifier (lib/coinpay-client.ts) are kept in lockstep with this file
|
|
16
|
+
* by src/lib/webhooks/contract.test.ts in the main repo. Don't drift.
|
|
3
17
|
*/
|
|
4
18
|
|
|
5
19
|
import { createHmac, timingSafeEqual } from 'crypto';
|
|
@@ -44,7 +58,12 @@ export function verifyWebhookSignature({
|
|
|
44
58
|
const signatureParts = {};
|
|
45
59
|
|
|
46
60
|
for (const part of parts) {
|
|
47
|
-
|
|
61
|
+
// Split on the FIRST '=' only. Splitting on every one truncates any value
|
|
62
|
+
// that legitimately contains '=' (base64 padding, for instance).
|
|
63
|
+
const separator = part.indexOf('=');
|
|
64
|
+
if (separator === -1) continue;
|
|
65
|
+
const key = part.slice(0, separator).trim();
|
|
66
|
+
const value = part.slice(separator + 1).trim();
|
|
48
67
|
signatureParts[key] = value;
|
|
49
68
|
}
|
|
50
69
|
|
|
@@ -55,8 +74,23 @@ export function verifyWebhookSignature({
|
|
|
55
74
|
return false;
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
// Check timestamp tolerance
|
|
59
|
-
|
|
77
|
+
// Check timestamp tolerance.
|
|
78
|
+
//
|
|
79
|
+
// parseInt('abc', 10) is NaN, and every comparison against NaN is false —
|
|
80
|
+
// so `Math.abs(NaN) > tolerance` was false and the freshness check silently
|
|
81
|
+
// PASSED for any non-numeric timestamp. A security check that no-ops on
|
|
82
|
+
// malformed input is worse than no check, because it reads as one. Parse
|
|
83
|
+
// strictly and reject anything that is not a plain integer.
|
|
84
|
+
if (!/^\d+$/.test(String(timestamp))) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const signedAt = Number(timestamp);
|
|
89
|
+
if (!Number.isSafeInteger(signedAt) || signedAt <= 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const timestampAge = Math.floor(Date.now() / 1000) - signedAt;
|
|
60
94
|
if (Math.abs(timestampAge) > tolerance) {
|
|
61
95
|
return false;
|
|
62
96
|
}
|