@txnod/sdk 1.0.1
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/AGENTS.md +29 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +434 -0
- package/dist/_shared/index.d.ts +68 -0
- package/dist/client-sandbox.d.ts +396 -0
- package/dist/client-sandbox.d.ts.map +1 -0
- package/dist/client-sandbox.js +448 -0
- package/dist/client-sandbox.js.map +1 -0
- package/dist/client.d.ts +429 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +588 -0
- package/dist/client.js.map +1 -0
- package/dist/env.d.ts +29 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +44 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +1887 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2107 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/internals/error-ctor-map.d.ts +11 -0
- package/dist/internals/error-ctor-map.d.ts.map +1 -0
- package/dist/internals/error-ctor-map.js +75 -0
- package/dist/internals/error-ctor-map.js.map +1 -0
- package/dist/internals/fetch-with-retry.d.ts +34 -0
- package/dist/internals/fetch-with-retry.d.ts.map +1 -0
- package/dist/internals/fetch-with-retry.js +233 -0
- package/dist/internals/fetch-with-retry.js.map +1 -0
- package/dist/internals/hmac.d.ts +2 -0
- package/dist/internals/hmac.d.ts.map +1 -0
- package/dist/internals/hmac.js +10 -0
- package/dist/internals/hmac.js.map +1 -0
- package/dist/internals/logger.d.ts +9 -0
- package/dist/internals/logger.d.ts.map +1 -0
- package/dist/internals/logger.js +16 -0
- package/dist/internals/logger.js.map +1 -0
- package/dist/internals/parse-problem-details.d.ts +3 -0
- package/dist/internals/parse-problem-details.d.ts.map +1 -0
- package/dist/internals/parse-problem-details.js +76 -0
- package/dist/internals/parse-problem-details.js.map +1 -0
- package/dist/internals/synthetic-details.d.ts +12 -0
- package/dist/internals/synthetic-details.d.ts.map +1 -0
- package/dist/internals/synthetic-details.js +19 -0
- package/dist/internals/synthetic-details.js.map +1 -0
- package/dist/verify/chains/bsc.d.ts +17 -0
- package/dist/verify/chains/bsc.d.ts.map +1 -0
- package/dist/verify/chains/bsc.js +15 -0
- package/dist/verify/chains/bsc.js.map +1 -0
- package/dist/verify/chains/btc.d.ts +22 -0
- package/dist/verify/chains/btc.d.ts.map +1 -0
- package/dist/verify/chains/btc.js +55 -0
- package/dist/verify/chains/btc.js.map +1 -0
- package/dist/verify/chains/cardano.d.ts +73 -0
- package/dist/verify/chains/cardano.d.ts.map +1 -0
- package/dist/verify/chains/cardano.js +175 -0
- package/dist/verify/chains/cardano.js.map +1 -0
- package/dist/verify/chains/evm.d.ts +21 -0
- package/dist/verify/chains/evm.d.ts.map +1 -0
- package/dist/verify/chains/evm.js +46 -0
- package/dist/verify/chains/evm.js.map +1 -0
- package/dist/verify/chains/polygon.d.ts +17 -0
- package/dist/verify/chains/polygon.d.ts.map +1 -0
- package/dist/verify/chains/polygon.js +15 -0
- package/dist/verify/chains/polygon.js.map +1 -0
- package/dist/verify/chains/secp256k1-bip32.d.ts +20 -0
- package/dist/verify/chains/secp256k1-bip32.d.ts.map +1 -0
- package/dist/verify/chains/secp256k1-bip32.js +88 -0
- package/dist/verify/chains/secp256k1-bip32.js.map +1 -0
- package/dist/verify/chains/ton-cell.d.ts +179 -0
- package/dist/verify/chains/ton-cell.d.ts.map +1 -0
- package/dist/verify/chains/ton-cell.js +614 -0
- package/dist/verify/chains/ton-cell.js.map +1 -0
- package/dist/verify/chains/ton.d.ts +84 -0
- package/dist/verify/chains/ton.d.ts.map +1 -0
- package/dist/verify/chains/ton.js +131 -0
- package/dist/verify/chains/ton.js.map +1 -0
- package/dist/verify/chains/tron.d.ts +21 -0
- package/dist/verify/chains/tron.d.ts.map +1 -0
- package/dist/verify/chains/tron.js +42 -0
- package/dist/verify/chains/tron.js.map +1 -0
- package/dist/verify/config.d.ts +41 -0
- package/dist/verify/config.d.ts.map +1 -0
- package/dist/verify/config.js +120 -0
- package/dist/verify/config.js.map +1 -0
- package/dist/verify/errors.d.ts +56 -0
- package/dist/verify/errors.d.ts.map +1 -0
- package/dist/verify/errors.js +58 -0
- package/dist/verify/errors.js.map +1 -0
- package/dist/verify/index.d.ts +119 -0
- package/dist/verify/index.d.ts.map +1 -0
- package/dist/verify/index.js +166 -0
- package/dist/verify/index.js.map +1 -0
- package/dist/verify/xpub-safety.d.ts +33 -0
- package/dist/verify/xpub-safety.d.ts.map +1 -0
- package/dist/verify/xpub-safety.js +54 -0
- package/dist/verify/xpub-safety.js.map +1 -0
- package/dist/verify-webhook-signature.d.ts +30 -0
- package/dist/verify-webhook-signature.d.ts.map +1 -0
- package/dist/verify-webhook-signature.js +84 -0
- package/dist/verify-webhook-signature.js.map +1 -0
- package/docs/00-getting-started.md +135 -0
- package/docs/01-authentication.md +114 -0
- package/docs/02-invoices.md +216 -0
- package/docs/03-rates-and-quotes.md +82 -0
- package/docs/04-webhooks.md +126 -0
- package/docs/05-errors.md +199 -0
- package/docs/05-sandbox.md +159 -0
- package/docs/06-idempotency.md +132 -0
- package/docs/examples/express-webhook-receiver.md +97 -0
- package/docs/examples/nextjs-route-handler.md +206 -0
- package/docs/examples/sandbox-vitest-suite.md +263 -0
- package/docs/index.md +66 -0
- package/docs/reference/client.md +392 -0
- package/docs/reference/errors.md +161 -0
- package/docs/reference/types.md +400 -0
- package/package.json +53 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-chain address-verification dispatcher consumed by TxnodClient.createInvoice.
|
|
3
|
+
* Routes the invoice's coin → chain → per-chain verifier with multi-xpub
|
|
4
|
+
* iteration (newest-first per AD-10) and selective error rethrow.
|
|
5
|
+
*/
|
|
6
|
+
import type { Chain, InvoiceResponse, WalletEdgeKind } from '../_shared/index.js';
|
|
7
|
+
import type { TonXpubConfig } from './config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Input shape for {@link verifyAddress}. The `invoice` field is a `Pick` of
|
|
10
|
+
* the fields actually consumed (`coin`, `address`, `derivation_path`, plus
|
|
11
|
+
* optional `id` for log correlation), so test fixtures are trivial.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { verifyAddress, type VerifyAddressInput } from '@txnod/sdk';
|
|
16
|
+
*
|
|
17
|
+
* const input: VerifyAddressInput = {
|
|
18
|
+
* invoice: {
|
|
19
|
+
* id: '01HK8MAR2QEXAMPLE000000000',
|
|
20
|
+
* coin: 'btc',
|
|
21
|
+
* address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
|
|
22
|
+
* derivation_path: "m/84'/0'/0'/0/0",
|
|
23
|
+
* },
|
|
24
|
+
* config: {
|
|
25
|
+
* btc: ['zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'],
|
|
26
|
+
* eth: [],
|
|
27
|
+
* tron: [],
|
|
28
|
+
* ada: [],
|
|
29
|
+
* polygon: [],
|
|
30
|
+
* bsc: [],
|
|
31
|
+
* },
|
|
32
|
+
* };
|
|
33
|
+
* verifyAddress(input);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export interface VerifyAddressInput {
|
|
37
|
+
invoice: Pick<InvoiceResponse, 'coin' | 'address' | 'derivation_path'> & {
|
|
38
|
+
id?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Optional explicit project kind override. The invoice envelope no longer
|
|
41
|
+
* carries a `network` discriminator — kind is implied from the project a
|
|
42
|
+
* given API key is bound to. Pass `kind` here only if the partner is
|
|
43
|
+
* verifying a TON invoice and needs to assert the user-friendly address
|
|
44
|
+
* encoding flag. Defaults to `'production'`.
|
|
45
|
+
*/
|
|
46
|
+
kind?: WalletEdgeKind;
|
|
47
|
+
};
|
|
48
|
+
config: Record<Chain, string[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Optional TON-specific config. When the invoice's chain resolves to `ton`
|
|
51
|
+
* and this is set, the SDK calls {@link verifyTonAddress} automatically.
|
|
52
|
+
* When `undefined`, TON verification is skipped silently — same behaviour as
|
|
53
|
+
* an HD chain with no `TXNOD_<chain>_XPUB` configured. Populate via
|
|
54
|
+
* {@link parseTonConfig} (env-driven).
|
|
55
|
+
*/
|
|
56
|
+
tonConfig?: TonXpubConfig;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Result type for {@link verifyAddress}. The function returns void on success
|
|
60
|
+
* or skip and throws on mismatch — this alias documents the void contract.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { verifyAddress, type VerificationResult } from '@txnod/sdk';
|
|
65
|
+
*
|
|
66
|
+
* const result: VerificationResult = verifyAddress({
|
|
67
|
+
* invoice: {
|
|
68
|
+
* coin: 'btc',
|
|
69
|
+
* address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
|
|
70
|
+
* derivation_path: "m/84'/0'/0'/0/0",
|
|
71
|
+
* },
|
|
72
|
+
* config: { btc: [], eth: [], tron: [], ada: [], polygon: [], bsc: [] },
|
|
73
|
+
* });
|
|
74
|
+
* console.log(result); // undefined
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export type VerificationResult = void;
|
|
78
|
+
/**
|
|
79
|
+
* Verify that the API-returned invoice address derives from one of the
|
|
80
|
+
* partner's configured xpubs for the invoice's chain. Returns void on
|
|
81
|
+
* success, on edge case (a) (missing `derivation_path` → log+skip), or on
|
|
82
|
+
* edge case (b) (no xpub configured → silent skip). Throws
|
|
83
|
+
* `AddressVerificationError` when all configured xpubs fail to derive the
|
|
84
|
+
* address. Rethrows `TxnodInvalidXpubFormatError` (and other non-
|
|
85
|
+
* `AddressVerificationError`) immediately — a malformed xpub is a config
|
|
86
|
+
* bug, not a "try the next one" signal.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { verifyAddress, AddressVerificationError } from '@txnod/sdk';
|
|
91
|
+
*
|
|
92
|
+
* try {
|
|
93
|
+
* verifyAddress({
|
|
94
|
+
* invoice: {
|
|
95
|
+
* id: '01HK8MAR2QEXAMPLE000000000',
|
|
96
|
+
* coin: 'btc',
|
|
97
|
+
* address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
|
|
98
|
+
* derivation_path: "m/84'/0'/0'/0/0",
|
|
99
|
+
* },
|
|
100
|
+
* config: {
|
|
101
|
+
* btc: ['zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'],
|
|
102
|
+
* eth: [],
|
|
103
|
+
* tron: [],
|
|
104
|
+
* ada: [],
|
|
105
|
+
* polygon: [],
|
|
106
|
+
* bsc: [],
|
|
107
|
+
* },
|
|
108
|
+
* });
|
|
109
|
+
* } catch (err) {
|
|
110
|
+
* if (err instanceof AddressVerificationError) {
|
|
111
|
+
* console.error('verify failed', err.chain, err.expected_address, err.derived_address);
|
|
112
|
+
* } else {
|
|
113
|
+
* throw err;
|
|
114
|
+
* }
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export declare function verifyAddress(input: VerifyAddressInput): VerificationResult;
|
|
119
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/verify/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAQ,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAUlF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAqBjD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,CAAC,GAAG;QACvE,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ;;;;;;WAMG;QACH,IAAI,CAAC,EAAE,cAAc,CAAC;KACvB,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAqCtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CA+D3E"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-chain address-verification dispatcher consumed by TxnodClient.createInvoice.
|
|
3
|
+
* Routes the invoice's coin → chain → per-chain verifier with multi-xpub
|
|
4
|
+
* iteration (newest-first per AD-10) and selective error rethrow.
|
|
5
|
+
*/
|
|
6
|
+
import { logger } from '../internals/logger.js';
|
|
7
|
+
import { AddressVerificationError } from './errors.js';
|
|
8
|
+
import { verifyBtc } from './chains/btc.js';
|
|
9
|
+
import { verifyEvm } from './chains/evm.js';
|
|
10
|
+
import { verifyTron } from './chains/tron.js';
|
|
11
|
+
import { verifyCardano } from './chains/cardano.js';
|
|
12
|
+
import { verifyPolygon } from './chains/polygon.js';
|
|
13
|
+
import { verifyBsc } from './chains/bsc.js';
|
|
14
|
+
import { verifyTonAddress } from './chains/ton.js';
|
|
15
|
+
// Mirrors packages/shared/src/schemas/coin.ts COIN_TO_CHAIN — drift is coupled with the shared table.
|
|
16
|
+
const COIN_TO_CHAIN_LITERAL = {
|
|
17
|
+
btc: 'btc',
|
|
18
|
+
eth: 'eth',
|
|
19
|
+
usdt_erc20: 'eth',
|
|
20
|
+
usdc_erc20: 'eth',
|
|
21
|
+
trx: 'tron',
|
|
22
|
+
usdt_trc20: 'tron',
|
|
23
|
+
ada: 'ada',
|
|
24
|
+
pol: 'polygon',
|
|
25
|
+
usdt_polygon: 'polygon',
|
|
26
|
+
usdc_polygon: 'polygon',
|
|
27
|
+
bnb: 'bsc',
|
|
28
|
+
usdt_bep20: 'bsc',
|
|
29
|
+
usdc_bep20: 'bsc',
|
|
30
|
+
ton: 'ton',
|
|
31
|
+
usdt_ton: 'ton',
|
|
32
|
+
};
|
|
33
|
+
function dispatchByChain(chain, xpub, derivation_path, expected_address) {
|
|
34
|
+
switch (chain) {
|
|
35
|
+
case 'btc':
|
|
36
|
+
verifyBtc({ xpub, derivation_path, expected_address });
|
|
37
|
+
return;
|
|
38
|
+
case 'eth':
|
|
39
|
+
verifyEvm({ xpub, derivation_path, expected_address, chain: 'eth' });
|
|
40
|
+
return;
|
|
41
|
+
case 'tron':
|
|
42
|
+
verifyTron({ xpub, derivation_path, expected_address });
|
|
43
|
+
return;
|
|
44
|
+
case 'ada':
|
|
45
|
+
verifyCardano({ xpub, derivation_path, expected_address });
|
|
46
|
+
return;
|
|
47
|
+
case 'polygon':
|
|
48
|
+
verifyPolygon({ xpub, derivation_path, expected_address });
|
|
49
|
+
return;
|
|
50
|
+
case 'bsc':
|
|
51
|
+
verifyBsc({ xpub, derivation_path, expected_address });
|
|
52
|
+
return;
|
|
53
|
+
case 'ton':
|
|
54
|
+
// Unreachable — TON is handled in verifyAddress() before dispatchByChain.
|
|
55
|
+
return;
|
|
56
|
+
default: {
|
|
57
|
+
const _exhaustive = chain;
|
|
58
|
+
throw new Error(`unreachable chain: ${_exhaustive}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Verify that the API-returned invoice address derives from one of the
|
|
64
|
+
* partner's configured xpubs for the invoice's chain. Returns void on
|
|
65
|
+
* success, on edge case (a) (missing `derivation_path` → log+skip), or on
|
|
66
|
+
* edge case (b) (no xpub configured → silent skip). Throws
|
|
67
|
+
* `AddressVerificationError` when all configured xpubs fail to derive the
|
|
68
|
+
* address. Rethrows `TxnodInvalidXpubFormatError` (and other non-
|
|
69
|
+
* `AddressVerificationError`) immediately — a malformed xpub is a config
|
|
70
|
+
* bug, not a "try the next one" signal.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { verifyAddress, AddressVerificationError } from '@txnod/sdk';
|
|
75
|
+
*
|
|
76
|
+
* try {
|
|
77
|
+
* verifyAddress({
|
|
78
|
+
* invoice: {
|
|
79
|
+
* id: '01HK8MAR2QEXAMPLE000000000',
|
|
80
|
+
* coin: 'btc',
|
|
81
|
+
* address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu',
|
|
82
|
+
* derivation_path: "m/84'/0'/0'/0/0",
|
|
83
|
+
* },
|
|
84
|
+
* config: {
|
|
85
|
+
* btc: ['zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'],
|
|
86
|
+
* eth: [],
|
|
87
|
+
* tron: [],
|
|
88
|
+
* ada: [],
|
|
89
|
+
* polygon: [],
|
|
90
|
+
* bsc: [],
|
|
91
|
+
* },
|
|
92
|
+
* });
|
|
93
|
+
* } catch (err) {
|
|
94
|
+
* if (err instanceof AddressVerificationError) {
|
|
95
|
+
* console.error('verify failed', err.chain, err.expected_address, err.derived_address);
|
|
96
|
+
* } else {
|
|
97
|
+
* throw err;
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function verifyAddress(input) {
|
|
103
|
+
const { invoice, config, tonConfig } = input;
|
|
104
|
+
const chain = COIN_TO_CHAIN_LITERAL[invoice.coin];
|
|
105
|
+
// TON has no xpub/derivation — operator pubkey + wallet metadata are
|
|
106
|
+
// supplied via TonXpubConfig (TXNOD_TON_PUBKEY + siblings). When unset, the
|
|
107
|
+
// SDK silently skips verification (parity with omitting TXNOD_<chain>_XPUB
|
|
108
|
+
// for HD chains); `createInvoice` still surfaces a single warn so partners
|
|
109
|
+
// are not silently relying on server trust.
|
|
110
|
+
if (chain === 'ton') {
|
|
111
|
+
if (tonConfig === undefined) {
|
|
112
|
+
logger.warn({
|
|
113
|
+
msg: 'SDK verify: TON address verification skipped — set TXNOD_TON_PUBKEY to enable',
|
|
114
|
+
invoice_id: invoice.id,
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const kind = invoice.kind ?? 'production';
|
|
119
|
+
try {
|
|
120
|
+
verifyTonAddress({
|
|
121
|
+
chain: 'ton',
|
|
122
|
+
publicKeyHex: tonConfig.publicKeyHex,
|
|
123
|
+
walletVersion: tonConfig.walletVersion,
|
|
124
|
+
subwalletId: tonConfig.subwalletId,
|
|
125
|
+
workchain: tonConfig.workchain,
|
|
126
|
+
kind,
|
|
127
|
+
expectedAddress: invoice.address,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (err instanceof AddressVerificationError)
|
|
132
|
+
throw err;
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const xpubs = config[chain] ?? [];
|
|
138
|
+
if (xpubs.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
if (!invoice.derivation_path) {
|
|
141
|
+
logger.warn({
|
|
142
|
+
msg: 'SDK verify: derivation_path absent on response; skipping verification',
|
|
143
|
+
chain,
|
|
144
|
+
invoice_id: invoice.id,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const xpub of xpubs) {
|
|
149
|
+
try {
|
|
150
|
+
dispatchByChain(chain, xpub, invoice.derivation_path, invoice.address);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (!(err instanceof AddressVerificationError))
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Sentinel: count of attempted xpubs is encoded in derived_address to keep AddressVerificationError shape unchanged.
|
|
159
|
+
throw new AddressVerificationError({
|
|
160
|
+
chain,
|
|
161
|
+
derivation_path: invoice.derivation_path,
|
|
162
|
+
expected_address: invoice.address,
|
|
163
|
+
derived_address: `<no match across ${xpubs.length} configured xpub${xpubs.length === 1 ? '' : 's'} for chain '${chain}'>`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/verify/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Chain } from '../_shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the 4-char prefix of an xpub-shaped string. Used by the xpub-prefix
|
|
4
|
+
* guard to detect testnet-shape xpubs (`tpub`/`vpub`/`upub`) configured for
|
|
5
|
+
* production environments.
|
|
6
|
+
*/
|
|
7
|
+
export declare function detectXpubPrefix(xpub: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Synchronous guard called from `TxnodClient.constructor` and
|
|
10
|
+
* `refreshXpubConfig()`. Iterates every chain × xpub in `xpubConfig`; throws
|
|
11
|
+
* `TxnodSandboxXpubInProductionError` for the first testnet-shape prefix
|
|
12
|
+
* found when `env === 'production'`.
|
|
13
|
+
*
|
|
14
|
+
* Carve-outs (no-ops, by spec §7.2 layer 3 + §4.6):
|
|
15
|
+
* - **Cardano (`ada`)**: CIP-5 hrp does not distinguish testnet at the
|
|
16
|
+
* account-pubkey level. Network safety lives at the address-level
|
|
17
|
+
* NetworkId byte, enforced server-side by the chain watcher and at the
|
|
18
|
+
* SDK boundary by `verify/chains/cardano.ts`. The xpub prefix is
|
|
19
|
+
* structurally non-discriminating for ADA.
|
|
20
|
+
* - **TON (`ton`)**: there is no xpub concept — `TXNOD_TON_PUBKEY` is a
|
|
21
|
+
* 32-byte ed25519 public key, never an extended key. The guard does not
|
|
22
|
+
* inspect `TonXpubConfig` at all (that's why the function takes only
|
|
23
|
+
* `xpubConfig`).
|
|
24
|
+
* - **EVM-family `xpub` ambiguity (`eth`/`polygon`/`bsc`)**: mainnet and
|
|
25
|
+
* testnet on EVM chains both commonly use `xpub`-prefixed extended keys;
|
|
26
|
+
* network differentiation lives in the per-address checksum and the chain
|
|
27
|
+
* id, not in the xpub prefix. The guard ONLY catches `tpub`/`vpub`/`upub`
|
|
28
|
+
* — an `xpub` configured for a testnet EVM chain falls through to the
|
|
29
|
+
* server-side cross-mode check (§7.2 layer 4) and the webhook envelope
|
|
30
|
+
* `mode` discriminator (§7.2 layer 5).
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertNoTestnetXpubsInProduction(xpubConfig: Record<Chain, string[]>, env: 'production' | 'non-production' | 'unknown'): void;
|
|
33
|
+
//# sourceMappingURL=xpub-safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xpub-safety.d.ts","sourceRoot":"","sources":["../../src/verify/xpub-safety.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAM3C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EACnC,GAAG,EAAE,YAAY,GAAG,gBAAgB,GAAG,SAAS,GAC/C,IAAI,CAYN"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { TxnodSandboxXpubInProductionError } from '../errors.js';
|
|
2
|
+
const TESTNET_XPUB_PREFIXES = ['tpub', 'vpub', 'upub'];
|
|
3
|
+
const PREFIX_LENGTH = 4;
|
|
4
|
+
/**
|
|
5
|
+
* Returns the 4-char prefix of an xpub-shaped string. Used by the xpub-prefix
|
|
6
|
+
* guard to detect testnet-shape xpubs (`tpub`/`vpub`/`upub`) configured for
|
|
7
|
+
* production environments.
|
|
8
|
+
*/
|
|
9
|
+
export function detectXpubPrefix(xpub) {
|
|
10
|
+
return xpub.slice(0, PREFIX_LENGTH);
|
|
11
|
+
}
|
|
12
|
+
function isTestnetPrefix(prefix) {
|
|
13
|
+
return TESTNET_XPUB_PREFIXES.includes(prefix);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Synchronous guard called from `TxnodClient.constructor` and
|
|
17
|
+
* `refreshXpubConfig()`. Iterates every chain × xpub in `xpubConfig`; throws
|
|
18
|
+
* `TxnodSandboxXpubInProductionError` for the first testnet-shape prefix
|
|
19
|
+
* found when `env === 'production'`.
|
|
20
|
+
*
|
|
21
|
+
* Carve-outs (no-ops, by spec §7.2 layer 3 + §4.6):
|
|
22
|
+
* - **Cardano (`ada`)**: CIP-5 hrp does not distinguish testnet at the
|
|
23
|
+
* account-pubkey level. Network safety lives at the address-level
|
|
24
|
+
* NetworkId byte, enforced server-side by the chain watcher and at the
|
|
25
|
+
* SDK boundary by `verify/chains/cardano.ts`. The xpub prefix is
|
|
26
|
+
* structurally non-discriminating for ADA.
|
|
27
|
+
* - **TON (`ton`)**: there is no xpub concept — `TXNOD_TON_PUBKEY` is a
|
|
28
|
+
* 32-byte ed25519 public key, never an extended key. The guard does not
|
|
29
|
+
* inspect `TonXpubConfig` at all (that's why the function takes only
|
|
30
|
+
* `xpubConfig`).
|
|
31
|
+
* - **EVM-family `xpub` ambiguity (`eth`/`polygon`/`bsc`)**: mainnet and
|
|
32
|
+
* testnet on EVM chains both commonly use `xpub`-prefixed extended keys;
|
|
33
|
+
* network differentiation lives in the per-address checksum and the chain
|
|
34
|
+
* id, not in the xpub prefix. The guard ONLY catches `tpub`/`vpub`/`upub`
|
|
35
|
+
* — an `xpub` configured for a testnet EVM chain falls through to the
|
|
36
|
+
* server-side cross-mode check (§7.2 layer 4) and the webhook envelope
|
|
37
|
+
* `mode` discriminator (§7.2 layer 5).
|
|
38
|
+
*/
|
|
39
|
+
export function assertNoTestnetXpubsInProduction(xpubConfig, env) {
|
|
40
|
+
if (env !== 'production')
|
|
41
|
+
return;
|
|
42
|
+
for (const chain of Object.keys(xpubConfig)) {
|
|
43
|
+
if (chain === 'ada' || chain === 'ton')
|
|
44
|
+
continue;
|
|
45
|
+
const xpubs = xpubConfig[chain] ?? [];
|
|
46
|
+
for (const xpub of xpubs) {
|
|
47
|
+
const prefix = detectXpubPrefix(xpub);
|
|
48
|
+
if (isTestnetPrefix(prefix)) {
|
|
49
|
+
throw new TxnodSandboxXpubInProductionError(chain, prefix);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=xpub-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xpub-safety.js","sourceRoot":"","sources":["../../src/verify/xpub-safety.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iCAAiC,EAAE,MAAM,cAAc,CAAC;AAEjE,MAAM,qBAAqB,GAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9E,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO,qBAAqB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,gCAAgC,CAC9C,UAAmC,EACnC,GAAgD;IAEhD,IAAI,GAAG,KAAK,YAAY;QAAE,OAAO;IACjC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAY,EAAE,CAAC;QACvD,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK;YAAE,SAAS;QACjD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,iCAAiC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WebhookEvent } from './_shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Verifies an inbound webhook signature and returns a typed event. Parses
|
|
4
|
+
* the `X-Txnod-Signature: t=<unix>,v1=<hex>` header, recomputes HMAC-SHA256
|
|
5
|
+
* over `${timestamp}.${rawBody}` using the project secret, compares in
|
|
6
|
+
* constant time, and enforces a ±300s timestamp window. Throws typed errors
|
|
7
|
+
* on any failure.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { verifyWebhookSignature, TxnodHmacError, TxnodTimestampError } from '@txnod/sdk';
|
|
12
|
+
*
|
|
13
|
+
* export async function POST(req: Request) {
|
|
14
|
+
* const rawBody = await req.text();
|
|
15
|
+
* try {
|
|
16
|
+
* const event = verifyWebhookSignature(req.headers, rawBody, process.env.TXNOD_WEBHOOK_SECRET!);
|
|
17
|
+
* if (event.event_type === 'invoice.paid') {
|
|
18
|
+
* console.log('paid', event.event_id);
|
|
19
|
+
* }
|
|
20
|
+
* return Response.json({ ok: true });
|
|
21
|
+
* } catch (err) {
|
|
22
|
+
* if (err instanceof TxnodHmacError) return new Response('bad sig', { status: 401 });
|
|
23
|
+
* if (err instanceof TxnodTimestampError) return new Response('stale', { status: 401 });
|
|
24
|
+
* throw err;
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyWebhookSignature(headers: Headers | Record<string, string> | Record<string, string[]>, rawBody: string, secret: string): WebhookEvent;
|
|
30
|
+
//# sourceMappingURL=verify-webhook-signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-webhook-signature.d.ts","sourceRoot":"","sources":["../src/verify-webhook-signature.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AA8BlD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EACpE,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,YAAY,CAqCd"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { TxnodHmacError, TxnodSignatureFormatError, TxnodTimestampError, TxnodWebhookPayloadParseError, } from './errors.js';
|
|
3
|
+
const TIMESTAMP_WINDOW_SECONDS = 300;
|
|
4
|
+
const DIGEST_BYTES = 32;
|
|
5
|
+
const SIGNATURE_HEADER = 'x-txnod-signature';
|
|
6
|
+
const HEADER_PATTERN = /^t=(\d+),v1=([0-9a-fA-F]+)$/;
|
|
7
|
+
function getHeader(headers, name) {
|
|
8
|
+
if (headers instanceof Headers) {
|
|
9
|
+
return headers.get(name) ?? undefined;
|
|
10
|
+
}
|
|
11
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
12
|
+
if (key.toLowerCase() !== name)
|
|
13
|
+
continue;
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value[0];
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Verifies an inbound webhook signature and returns a typed event. Parses
|
|
23
|
+
* the `X-Txnod-Signature: t=<unix>,v1=<hex>` header, recomputes HMAC-SHA256
|
|
24
|
+
* over `${timestamp}.${rawBody}` using the project secret, compares in
|
|
25
|
+
* constant time, and enforces a ±300s timestamp window. Throws typed errors
|
|
26
|
+
* on any failure.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { verifyWebhookSignature, TxnodHmacError, TxnodTimestampError } from '@txnod/sdk';
|
|
31
|
+
*
|
|
32
|
+
* export async function POST(req: Request) {
|
|
33
|
+
* const rawBody = await req.text();
|
|
34
|
+
* try {
|
|
35
|
+
* const event = verifyWebhookSignature(req.headers, rawBody, process.env.TXNOD_WEBHOOK_SECRET!);
|
|
36
|
+
* if (event.event_type === 'invoice.paid') {
|
|
37
|
+
* console.log('paid', event.event_id);
|
|
38
|
+
* }
|
|
39
|
+
* return Response.json({ ok: true });
|
|
40
|
+
* } catch (err) {
|
|
41
|
+
* if (err instanceof TxnodHmacError) return new Response('bad sig', { status: 401 });
|
|
42
|
+
* if (err instanceof TxnodTimestampError) return new Response('stale', { status: 401 });
|
|
43
|
+
* throw err;
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function verifyWebhookSignature(headers, rawBody, secret) {
|
|
49
|
+
const raw = getHeader(headers, SIGNATURE_HEADER);
|
|
50
|
+
if (raw === undefined) {
|
|
51
|
+
throw new TxnodSignatureFormatError();
|
|
52
|
+
}
|
|
53
|
+
const match = HEADER_PATTERN.exec(raw);
|
|
54
|
+
if (match === null) {
|
|
55
|
+
throw new TxnodSignatureFormatError();
|
|
56
|
+
}
|
|
57
|
+
const timestampStr = match[1];
|
|
58
|
+
const providedHex = match[2];
|
|
59
|
+
if (providedHex.length !== DIGEST_BYTES * 2) {
|
|
60
|
+
throw new TxnodSignatureFormatError();
|
|
61
|
+
}
|
|
62
|
+
const timestamp = Number(timestampStr);
|
|
63
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
64
|
+
const skewSeconds = nowSec - timestamp;
|
|
65
|
+
if (Math.abs(skewSeconds) > TIMESTAMP_WINDOW_SECONDS) {
|
|
66
|
+
throw new TxnodTimestampError(skewSeconds);
|
|
67
|
+
}
|
|
68
|
+
const expected = createHmac('sha256', secret)
|
|
69
|
+
.update(`${timestamp}.${rawBody}`)
|
|
70
|
+
.digest();
|
|
71
|
+
// `provided` is structurally `DIGEST_BYTES` long: HEADER_PATTERN + the
|
|
72
|
+
// `providedHex.length === DIGEST_BYTES * 2` check above guarantee 64 hex
|
|
73
|
+
// chars in, which `Buffer.from(_, 'hex')` decodes to exactly 32 bytes.
|
|
74
|
+
if (!timingSafeEqual(Buffer.from(providedHex, 'hex'), expected)) {
|
|
75
|
+
throw new TxnodHmacError();
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(rawBody);
|
|
79
|
+
}
|
|
80
|
+
catch (cause) {
|
|
81
|
+
throw new TxnodWebhookPayloadParseError(cause);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=verify-webhook-signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-webhook-signature.js","sourceRoot":"","sources":["../src/verify-webhook-signature.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EACL,cAAc,EACd,yBAAyB,EACzB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,aAAa,CAAC;AAErB,MAAM,wBAAwB,GAAG,GAAG,CAAC;AACrC,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAC7C,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD,SAAS,SAAS,CAChB,OAAoE,EACpE,IAAY;IAEZ,IAAI,OAAO,YAAY,OAAO,EAAE,CAAC;QAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;IACxC,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI;YAAE,SAAS;QACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAoE,EACpE,OAAe,EACf,MAAc;IAEd,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;IACjD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,yBAAyB,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,yBAAyB,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IAC/B,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;IAC9B,IAAI,WAAW,CAAC,MAAM,KAAK,YAAY,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,yBAAyB,EAAE,CAAC;IACxC,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAC;IACvC,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,wBAAwB,EAAE,CAAC;QACrD,MAAM,IAAI,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC1C,MAAM,CAAC,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC;SACjC,MAAM,EAAE,CAAC;IACZ,uEAAuE;IACvE,yEAAyE;IACzE,uEAAuE;IACvE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,cAAc,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiB,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,6BAA6B,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Getting started"
|
|
3
|
+
description: "Install @txnod/sdk, configure env vars, create the first invoice, verify the first webhook."
|
|
4
|
+
sdk_version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Getting started
|
|
8
|
+
|
|
9
|
+
## Runtime requirements
|
|
10
|
+
|
|
11
|
+
- Node ≥ 20 (the SDK uses `node:crypto`, `globalThis.fetch`, and the `Headers` Web API).
|
|
12
|
+
- Server-side only. Do not import `@txnod/sdk` into browser bundles — the `apiSecret` must never leave the server.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @txnod/sdk
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @txnod/sdk
|
|
20
|
+
# or
|
|
21
|
+
yarn add @txnod/sdk
|
|
22
|
+
# or
|
|
23
|
+
bun add @txnod/sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The tarball ships **zero dependencies** — runtime or type-level. The compiled JS has no external imports beyond `node:crypto`. Published `.d.ts` types are fully expanded to plain TypeScript at build time (no `zod` type constructs, no `@txnod/*` references), so the SDK coexists with any version of zod your project may already use — or none at all. Bundle size is enforced in CI.
|
|
27
|
+
|
|
28
|
+
## Configure environment variables
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# .env.local (never commit)
|
|
32
|
+
TXNOD_PROJECT_ID=01JXXXXXXXXXXXXXXXXXXXXXXX
|
|
33
|
+
TXNOD_API_SECRET=<64-hex-character secret>
|
|
34
|
+
TXNOD_WEBHOOK_SECRET=<same value as TXNOD_API_SECRET>
|
|
35
|
+
|
|
36
|
+
# Optional — omit unless targeting a non-default endpoint.
|
|
37
|
+
# TXNOD_BASE_URL=https://txnod.com
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `TXNOD_PROJECT_ID` is a ULID surfaced when the operator creates a project in the dashboard.
|
|
41
|
+
- `TXNOD_API_SECRET` is shown **exactly once** on key generation. Copy into secrets manager immediately; the server stores only a hash.
|
|
42
|
+
- `TXNOD_WEBHOOK_SECRET` intentionally equals `TXNOD_API_SECRET`. Two names let callers wire distinct env vars in their framework without re-deriving the relationship.
|
|
43
|
+
- `TXNOD_BASE_URL` is optional and defaults to `https://txnod.com`. Set it to point the SDK at a self-hosted txnod-compatible API (for example when someone runs the project under a different domain) or at a staging instance. The HMAC scheme and all endpoints are endpoint-agnostic — only the origin changes.
|
|
44
|
+
|
|
45
|
+
See [`01-authentication.md`](./01-authentication.md) for how these values are consumed.
|
|
46
|
+
|
|
47
|
+
## First invoice
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { TxnodClient } from '@txnod/sdk';
|
|
51
|
+
|
|
52
|
+
const client = new TxnodClient({
|
|
53
|
+
projectId: process.env.TXNOD_PROJECT_ID!,
|
|
54
|
+
apiSecret: process.env.TXNOD_API_SECRET!,
|
|
55
|
+
// Omit baseUrl (defaults to https://txnod.com) unless you are pointing at
|
|
56
|
+
// a self-hosted instance or a staging environment. Passing process.env
|
|
57
|
+
// conditionally keeps prod and dev wiring in the same file:
|
|
58
|
+
...(process.env.TXNOD_BASE_URL ? { baseUrl: process.env.TXNOD_BASE_URL } : {}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const invoice = await client.createInvoice({
|
|
62
|
+
amount_usd: 9.99,
|
|
63
|
+
coin: 'usdt_trc20',
|
|
64
|
+
external_id: 'order-42',
|
|
65
|
+
callback_url: 'https://your-site.com/api/txnod-webhook',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Show these to the payer:
|
|
69
|
+
// invoice.address — deposit address for this invoice
|
|
70
|
+
// invoice.amount_crypto — decimal string, e.g. "9.990000"
|
|
71
|
+
// invoice.amount_crypto_units — integer string in base units (sats, wei, ...)
|
|
72
|
+
// invoice.payment_uri — BIP-21 / EIP-681 / Cardano URI for QR generation
|
|
73
|
+
// invoice.expires_at_iso — invoice auto-expires at this ISO-8601 UTC timestamp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`external_id` is your side's idempotency key — see [`06-idempotency.md`](./06-idempotency.md). `coin` must be one of the 15 supported coins listed in [`reference/types.md`](./reference/types.md#coin).
|
|
77
|
+
|
|
78
|
+
For staging or development, ask the operator to spin up a separate `kind='testnet'` project (dashboard → New project → Testnet). The SDK code path is identical — partner code talks to the testnet project's API key and the SDK accepts testnet-prefix xpubs (`tpub`/`vpub`/`zpub`, `addr_test1...` for Cardano) for address verification automatically. See [`02-invoices.md`](./02-invoices.md#testnet) for the canonical testnet setup.
|
|
79
|
+
|
|
80
|
+
For deterministic integration tests, the SDK ships a sandbox surface (`client.sandbox.*`) that walks the invoice state machine without touching a chain. See [`05-sandbox.md`](./05-sandbox.md) for the full surface, environment-detection guards, and the [`examples/sandbox-vitest-suite.md`](./examples/sandbox-vitest-suite.md) end-to-end harness.
|
|
81
|
+
|
|
82
|
+
## First verified webhook
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// Next.js 16 App Router — app/api/txnod-webhook/route.ts
|
|
86
|
+
import {
|
|
87
|
+
verifyWebhookSignature,
|
|
88
|
+
TxnodHmacError,
|
|
89
|
+
TxnodTimestampError,
|
|
90
|
+
} from '@txnod/sdk';
|
|
91
|
+
|
|
92
|
+
export async function POST(request: Request): Promise<Response> {
|
|
93
|
+
const rawBody = await request.text(); // must be the exact bytes, pre-parsing
|
|
94
|
+
try {
|
|
95
|
+
const event = verifyWebhookSignature(
|
|
96
|
+
request.headers,
|
|
97
|
+
rawBody,
|
|
98
|
+
process.env.TXNOD_WEBHOOK_SECRET!,
|
|
99
|
+
);
|
|
100
|
+
if (event.event_type === 'invoice.paid') {
|
|
101
|
+
// event is narrowed to the paid variant. See 04-webhooks.md for the discriminants.
|
|
102
|
+
}
|
|
103
|
+
return Response.json({ ok: true });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof TxnodHmacError) return new Response('bad sig', { status: 401 });
|
|
106
|
+
if (err instanceof TxnodTimestampError) return new Response('stale', { status: 401 });
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Critical:** do not parse the body before signing verification. HMAC is computed over the raw bytes as sent; any middleware that re-serializes JSON breaks the signature.
|
|
113
|
+
|
|
114
|
+
## Self-hosted or alternative-origin deployments
|
|
115
|
+
|
|
116
|
+
`TxnodClient` accepts `baseUrl?: string` explicitly, which always wins over `TXNOD_BASE_URL`. The SDK's HMAC scheme, retry logic, and `verifyWebhookSignature` helper are endpoint-agnostic — they work unchanged against any server that implements the TxNod API contract. Typical uses:
|
|
117
|
+
|
|
118
|
+
- **Default:** omit `baseUrl`. The SDK targets `https://txnod.com`.
|
|
119
|
+
- **Staging:** set `TXNOD_BASE_URL=https://staging.txnod.com` in the staging environment only.
|
|
120
|
+
- **Self-hosted TxNod:** if someone operates their own non-custodial txnod-compatible gateway, their partners point the SDK at that origin the same way (`baseUrl: 'https://pay.mycompany.com'`). No code changes, no rebuild — the SDK is origin-neutral.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const client = new TxnodClient({
|
|
124
|
+
projectId: process.env.TXNOD_PROJECT_ID!,
|
|
125
|
+
apiSecret: process.env.TXNOD_API_SECRET!,
|
|
126
|
+
baseUrl: 'https://pay.mycompany.com', // trailing slash optional; SDK normalises it.
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Where to go next
|
|
131
|
+
|
|
132
|
+
- To understand the HMAC scheme and headers → [`01-authentication.md`](./01-authentication.md).
|
|
133
|
+
- To branch on every invoice status and error_code → [`02-invoices.md`](./02-invoices.md) and [`05-errors.md`](./05-errors.md).
|
|
134
|
+
- To read each webhook event shape → [`04-webhooks.md`](./04-webhooks.md).
|
|
135
|
+
- Full working code → [`examples/nextjs-route-handler.md`](./examples/nextjs-route-handler.md).
|