@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.
Files changed (121) hide show
  1. package/AGENTS.md +29 -0
  2. package/CHANGELOG.md +22 -0
  3. package/LICENSE +21 -0
  4. package/README.md +434 -0
  5. package/dist/_shared/index.d.ts +68 -0
  6. package/dist/client-sandbox.d.ts +396 -0
  7. package/dist/client-sandbox.d.ts.map +1 -0
  8. package/dist/client-sandbox.js +448 -0
  9. package/dist/client-sandbox.js.map +1 -0
  10. package/dist/client.d.ts +429 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +588 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/env.d.ts +29 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +44 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/errors.d.ts +1887 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +2107 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/index.d.ts +35 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +32 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/internals/error-ctor-map.d.ts +11 -0
  27. package/dist/internals/error-ctor-map.d.ts.map +1 -0
  28. package/dist/internals/error-ctor-map.js +75 -0
  29. package/dist/internals/error-ctor-map.js.map +1 -0
  30. package/dist/internals/fetch-with-retry.d.ts +34 -0
  31. package/dist/internals/fetch-with-retry.d.ts.map +1 -0
  32. package/dist/internals/fetch-with-retry.js +233 -0
  33. package/dist/internals/fetch-with-retry.js.map +1 -0
  34. package/dist/internals/hmac.d.ts +2 -0
  35. package/dist/internals/hmac.d.ts.map +1 -0
  36. package/dist/internals/hmac.js +10 -0
  37. package/dist/internals/hmac.js.map +1 -0
  38. package/dist/internals/logger.d.ts +9 -0
  39. package/dist/internals/logger.d.ts.map +1 -0
  40. package/dist/internals/logger.js +16 -0
  41. package/dist/internals/logger.js.map +1 -0
  42. package/dist/internals/parse-problem-details.d.ts +3 -0
  43. package/dist/internals/parse-problem-details.d.ts.map +1 -0
  44. package/dist/internals/parse-problem-details.js +76 -0
  45. package/dist/internals/parse-problem-details.js.map +1 -0
  46. package/dist/internals/synthetic-details.d.ts +12 -0
  47. package/dist/internals/synthetic-details.d.ts.map +1 -0
  48. package/dist/internals/synthetic-details.js +19 -0
  49. package/dist/internals/synthetic-details.js.map +1 -0
  50. package/dist/verify/chains/bsc.d.ts +17 -0
  51. package/dist/verify/chains/bsc.d.ts.map +1 -0
  52. package/dist/verify/chains/bsc.js +15 -0
  53. package/dist/verify/chains/bsc.js.map +1 -0
  54. package/dist/verify/chains/btc.d.ts +22 -0
  55. package/dist/verify/chains/btc.d.ts.map +1 -0
  56. package/dist/verify/chains/btc.js +55 -0
  57. package/dist/verify/chains/btc.js.map +1 -0
  58. package/dist/verify/chains/cardano.d.ts +73 -0
  59. package/dist/verify/chains/cardano.d.ts.map +1 -0
  60. package/dist/verify/chains/cardano.js +175 -0
  61. package/dist/verify/chains/cardano.js.map +1 -0
  62. package/dist/verify/chains/evm.d.ts +21 -0
  63. package/dist/verify/chains/evm.d.ts.map +1 -0
  64. package/dist/verify/chains/evm.js +46 -0
  65. package/dist/verify/chains/evm.js.map +1 -0
  66. package/dist/verify/chains/polygon.d.ts +17 -0
  67. package/dist/verify/chains/polygon.d.ts.map +1 -0
  68. package/dist/verify/chains/polygon.js +15 -0
  69. package/dist/verify/chains/polygon.js.map +1 -0
  70. package/dist/verify/chains/secp256k1-bip32.d.ts +20 -0
  71. package/dist/verify/chains/secp256k1-bip32.d.ts.map +1 -0
  72. package/dist/verify/chains/secp256k1-bip32.js +88 -0
  73. package/dist/verify/chains/secp256k1-bip32.js.map +1 -0
  74. package/dist/verify/chains/ton-cell.d.ts +179 -0
  75. package/dist/verify/chains/ton-cell.d.ts.map +1 -0
  76. package/dist/verify/chains/ton-cell.js +614 -0
  77. package/dist/verify/chains/ton-cell.js.map +1 -0
  78. package/dist/verify/chains/ton.d.ts +84 -0
  79. package/dist/verify/chains/ton.d.ts.map +1 -0
  80. package/dist/verify/chains/ton.js +131 -0
  81. package/dist/verify/chains/ton.js.map +1 -0
  82. package/dist/verify/chains/tron.d.ts +21 -0
  83. package/dist/verify/chains/tron.d.ts.map +1 -0
  84. package/dist/verify/chains/tron.js +42 -0
  85. package/dist/verify/chains/tron.js.map +1 -0
  86. package/dist/verify/config.d.ts +41 -0
  87. package/dist/verify/config.d.ts.map +1 -0
  88. package/dist/verify/config.js +120 -0
  89. package/dist/verify/config.js.map +1 -0
  90. package/dist/verify/errors.d.ts +56 -0
  91. package/dist/verify/errors.d.ts.map +1 -0
  92. package/dist/verify/errors.js +58 -0
  93. package/dist/verify/errors.js.map +1 -0
  94. package/dist/verify/index.d.ts +119 -0
  95. package/dist/verify/index.d.ts.map +1 -0
  96. package/dist/verify/index.js +166 -0
  97. package/dist/verify/index.js.map +1 -0
  98. package/dist/verify/xpub-safety.d.ts +33 -0
  99. package/dist/verify/xpub-safety.d.ts.map +1 -0
  100. package/dist/verify/xpub-safety.js +54 -0
  101. package/dist/verify/xpub-safety.js.map +1 -0
  102. package/dist/verify-webhook-signature.d.ts +30 -0
  103. package/dist/verify-webhook-signature.d.ts.map +1 -0
  104. package/dist/verify-webhook-signature.js +84 -0
  105. package/dist/verify-webhook-signature.js.map +1 -0
  106. package/docs/00-getting-started.md +135 -0
  107. package/docs/01-authentication.md +114 -0
  108. package/docs/02-invoices.md +216 -0
  109. package/docs/03-rates-and-quotes.md +82 -0
  110. package/docs/04-webhooks.md +126 -0
  111. package/docs/05-errors.md +199 -0
  112. package/docs/05-sandbox.md +159 -0
  113. package/docs/06-idempotency.md +132 -0
  114. package/docs/examples/express-webhook-receiver.md +97 -0
  115. package/docs/examples/nextjs-route-handler.md +206 -0
  116. package/docs/examples/sandbox-vitest-suite.md +263 -0
  117. package/docs/index.md +66 -0
  118. package/docs/reference/client.md +392 -0
  119. package/docs/reference/errors.md +161 -0
  120. package/docs/reference/types.md +400 -0
  121. package/package.json +53 -0
@@ -0,0 +1,392 @@
1
+ ---
2
+ title: "TxnodClient reference"
3
+ description: "Every method of TxnodClient and the verifyWebhookSignature helper: signature, behaviour, and a minimal example."
4
+ sdk_version: 1.0.0
5
+ ---
6
+
7
+ # `TxnodClient` reference
8
+
9
+ ```ts
10
+ import { TxnodClient } from '@txnod/sdk';
11
+
12
+ const client = new TxnodClient({
13
+ projectId: string; // required — your TXNOD_PROJECT_ID
14
+ apiSecret: string; // required — your TXNOD_API_SECRET
15
+ baseUrl?: string; // optional — defaults to 'https://txnod.com'
16
+ requestTimeoutMs?: number; // optional — per-attempt HTTP timeout (default 30_000)
17
+ maxResponseBytes?: number; // optional — body size cap (default 1 MiB)
18
+ requestLogger?: TxnodRequestLogger; // optional — per-attempt observer
19
+ });
20
+ ```
21
+
22
+ `baseUrl` controls only the HTTP origin the SDK talks to. The HMAC scheme, header names, request/response shapes, and retry semantics are all endpoint-agnostic. Any trailing slash is stripped by the constructor. Typical values:
23
+
24
+ - Omitted → `https://txnod.com` (default).
25
+ - `TXNOD_BASE_URL=https://staging.txnod.com` for a staging environment.
26
+ - `'https://pay.mycompany.com'` for a self-hosted txnod-compatible deployment.
27
+
28
+ Every method returns a `Promise<T>` and throws a subclass of `TxnodError` on a non-2xx response. Retries for `429` (×3, honouring `Retry-After`) and `5xx` (×2, full-jitter backoff, base 500 ms, cap 30 s) happen inside the SDK.
29
+
30
+ After every call (success or error) the client stores the server's `X-Txnod-Request-Id` header on `client.lastRequestId` (`string | undefined`). On error paths the same id is also surfaced as `err.request_id` on the thrown `TxnodError`. The field is per-instance — concurrent calls on a shared client overwrite each other; create a client per request scope, or read the field synchronously after `await`-ing.
31
+
32
+ Methods below are ordered by how you will reach for them in a typical integration.
33
+
34
+ ---
35
+
36
+ ## `createInvoice`
37
+
38
+ Create a new invoice. Idempotent on `(project_id, external_id)`.
39
+
40
+ **Signature**
41
+
42
+ ```ts
43
+ createInvoice(body: InvoiceCreateRequest): Promise<InvoiceResponse>
44
+ ```
45
+
46
+ **Parameters**
47
+
48
+ | Field | Type | Required | Notes |
49
+ |---|---|---|---|
50
+ | `external_id` | `string` (1..128) | yes | Your-side stable id for this charge |
51
+ | `coin` | `Coin` | yes | One of the 15 supported coins |
52
+ | `amount_usd` | `number` positive finite | either-or | Mutually exclusive with `amount_crypto` |
53
+ | `amount_crypto` | `string` (`^\d+(\.\d+)?$`) | either-or | Mutually exclusive with `amount_usd` |
54
+ | `callback_url` | `string` (https URL) | no | Webhook target for this invoice |
55
+ | `metadata` | `Record<string, unknown>` | no | Free-form JSON returned on the invoice |
56
+
57
+ **Throws**: `TxnodExternalIdConflictError` (idempotent — fetch existing), `TxnodValidationError`, `TxnodInvalidCoinError`, `TxnodCoinNotEnabledError`, `TxnodAmountOutOfRangeError`, `TxnodXpubNotVerifiedError`, `TxnodWalletNotFoundError`, `TxnodWalletNotBoundError`, `TxnodInvalidWebhookUrlError`, `TxnodTronNoActivatedAddressesError` (TRON only — operator must activate addresses), `TxnodSubscriptionExpiredError` (HTTP 402 — operator must renew subscription), `TxnodPoolExhaustedError` (retryable), auth/identity family (see [`../05-errors.md`](../05-errors.md)). The SDK also raises `AddressVerificationError` (not a `TxnodError` subclass) when the returned `address` does not derive from a configured xpub — see [`../02-invoices.md`](../02-invoices.md#address-verification).
58
+
59
+ **Example**
60
+
61
+ ```ts
62
+ const invoice = await client.createInvoice({
63
+ external_id: 'order-42',
64
+ coin: 'usdt_trc20',
65
+ amount_usd: 9.99,
66
+ callback_url: 'https://your-site.com/api/txnod-webhook',
67
+ });
68
+ ```
69
+
70
+ ---
71
+
72
+ ---
73
+
74
+ ## `createOrGetInvoice`
75
+
76
+ Idempotent invoice creation. Tries `createInvoice`; on `TxnodExternalIdConflictError` fetches the pre-existing invoice via `searchInvoices({ external_id })` and returns it.
77
+
78
+ **Signature**
79
+
80
+ ```ts
81
+ createOrGetInvoice(body: InvoiceCreateRequest): Promise<InvoiceResponse>
82
+ ```
83
+
84
+ **Throws**: every error that `createInvoice` can throw EXCEPT `TxnodExternalIdConflictError`. If the conflict is raised but the matching invoice cannot be fetched (race, recently deleted), the original conflict error rethrows.
85
+
86
+ ---
87
+
88
+ ## `refreshXpubConfig`
89
+
90
+ Re-read `TXNOD_<chain>_XPUB` env vars (and `TXNOD_TON_PUBKEY` siblings) into the client's xpub config. Call after wallet rotation in long-lived processes.
91
+
92
+ **Signature**
93
+
94
+ ```ts
95
+ refreshXpubConfig(): void
96
+ ```
97
+
98
+ Idempotent. No-op when env vars are unchanged.
99
+
100
+ ---
101
+
102
+ ## `getInvoice`
103
+
104
+ Fetch an invoice by its ULID.
105
+
106
+ **Signature**
107
+
108
+ ```ts
109
+ getInvoice(id: string): Promise<InvoiceResponse>
110
+ ```
111
+
112
+ **Throws**: `TxnodInvoiceNotFoundError` on unknown id.
113
+
114
+ **Example**
115
+
116
+ ```ts
117
+ const invoice = await client.getInvoice('01HK8MAR2QEXAMPLE000000000');
118
+ ```
119
+
120
+ ---
121
+
122
+ ## `searchInvoices`
123
+
124
+ Cursor-paginated search.
125
+
126
+ **Signature**
127
+
128
+ ```ts
129
+ searchInvoices(query: InvoiceSearchQuery): Promise<CursorPaginatedInvoiceResponse>
130
+ ```
131
+
132
+ **Query fields** (all optional)
133
+
134
+ | Field | Type | Notes |
135
+ |---|---|---|
136
+ | `external_id` | `string` | Exact match |
137
+ | `address` | `string` | Exact match on deposit address |
138
+ | `tx_hash` | `string` | Invoices that received this tx |
139
+ | `amount` | `string` | Crypto amount as decimal string |
140
+ | `status` | `InvoiceStatus` | See [`../02-invoices.md`](../02-invoices.md#lifecycle) |
141
+ | `date_from` / `date_to` | ISO-8601 datetime | Created-at window |
142
+ | `cursor` | ULID | `next_cursor` from the previous page |
143
+ | `limit` | `1..200` | Default `50` |
144
+
145
+ **Response**: `{ items: InvoiceResponse[], next_cursor?: string }`. `next_cursor` is omitted on the last page.
146
+
147
+ **Example**
148
+
149
+ ```ts
150
+ const page = await client.searchInvoices({ status: 'paid', limit: 50 });
151
+ for (const invoice of page.items) { /* ... */ }
152
+ ```
153
+
154
+ ---
155
+
156
+ ## `cancelInvoice`
157
+
158
+ **Signature**
159
+
160
+ ```ts
161
+ cancelInvoice(id: string): Promise<InvoiceResponse>
162
+ ```
163
+
164
+ **Throws**: `TxnodInvoiceNotFoundError`, `TxnodInvoiceNotCancellableError` when the invoice is past its cancellable state.
165
+
166
+ ---
167
+
168
+ ## `getRates`
169
+
170
+ Current indicative USD→coin rates. See [`../03-rates-and-quotes.md`](../03-rates-and-quotes.md).
171
+
172
+ **Signature**
173
+
174
+ ```ts
175
+ getRates(query: { coins?: string }): Promise<RatesResponse>
176
+ ```
177
+
178
+ `coins` is an optional comma-separated list. Omit for all enabled coins.
179
+
180
+ ---
181
+
182
+ ## `quoteAmount`
183
+
184
+ Indicative USD → per-coin crypto quote. See [`../03-rates-and-quotes.md`](../03-rates-and-quotes.md).
185
+
186
+ **Signature**
187
+
188
+ ```ts
189
+ quoteAmount(query: { amount_usd: number; coins?: string }): Promise<QuoteResponse>
190
+ ```
191
+
192
+ ---
193
+
194
+ ## `listOrphanPayments`
195
+
196
+ Paginated list of on-chain receipts that did not match any open invoice.
197
+
198
+ **Signature**
199
+
200
+ ```ts
201
+ listOrphanPayments(query: OrphanPaymentListQuery): Promise<CursorPaginatedOrphanPaymentResponse>
202
+ ```
203
+
204
+ **Query fields** (all optional)
205
+
206
+ | Field | Type | Notes |
207
+ |---|---|---|
208
+ | `chain` | `Chain` | `btc` / `eth` / `tron` / `ada` / `polygon` / `bsc` / `ton` |
209
+ | `attributed` | `boolean` | Filter by attribution state (SDK serialises to `?attributed=true`/`false`) |
210
+ | `tx_hash` | `string` | Exact |
211
+ | `date_from` / `date_to` | ISO-8601 datetime | |
212
+ | `amount_units_gte` / `amount_units_lte` | integer string | Filter in smallest unit |
213
+ | `cursor` | ULID | |
214
+ | `limit` | `1..200` | Default `50` |
215
+
216
+ ---
217
+
218
+ ## `attributeOrphanPayment`
219
+
220
+ Post-hoc bind an orphan on-chain receipt to an `external_id`.
221
+
222
+ **Signature**
223
+
224
+ ```ts
225
+ attributeOrphanPayment(
226
+ txHash: string,
227
+ body: OrphanAttributeRequest,
228
+ ): Promise<InvoiceResponse>
229
+ ```
230
+
231
+ **Body**
232
+
233
+ | Field | Type | Required | Notes |
234
+ |---|---|---|---|
235
+ | `external_id` | `string` (1..128) | yes | Target invoice's external id |
236
+ | `user_id` | `string` (1..256) | no | Your-side user id for audit trail |
237
+ | `metadata` | `Record<string, unknown>` | no | |
238
+ | `to_address` | `string` | no | Disambiguate among multi-output txs |
239
+ | `tx_output_index` | `int ≥ 0` | no | Disambiguate among multi-output txs |
240
+
241
+ **Throws**: `TxnodOrphanNotFoundError`, `TxnodOrphanAlreadyAttributedError` (idempotent — treat as success).
242
+
243
+ ---
244
+
245
+ ## `listWebhookEvents`
246
+
247
+ Paginated audit list of outbound webhook events for the calling project.
248
+
249
+ **Signature**
250
+
251
+ ```ts
252
+ listWebhookEvents(query: WebhookEventListApiQuery): Promise<WebhookEventListResponse>
253
+ ```
254
+
255
+ **Query fields** (all optional)
256
+
257
+ | Field | Type | Notes |
258
+ |---|---|---|
259
+ | `status` | `'delivered' \| 'retrying' \| 'dlq' \| 'skipped'` | |
260
+ | `event_type` | `WebhookEventType` | See [`../04-webhooks.md`](../04-webhooks.md#event-types) |
261
+ | `mode` | `'production' \| 'testnet' \| 'sandbox'` | Filter events by the source project's kind (mode = project.kind on the server side) |
262
+ | `since` | ISO-8601 datetime | |
263
+ | `invoice_id` | ULID | |
264
+ | `cursor` | ULID | |
265
+ | `limit` | `1..200` | Default `50` |
266
+
267
+ ---
268
+
269
+ ## `resendWebhookEvent`
270
+
271
+ Re-enqueue a previously dispatched webhook event.
272
+
273
+ **Signature**
274
+
275
+ ```ts
276
+ resendWebhookEvent(eventId: string): Promise<WebhookEventResendResponse>
277
+ ```
278
+
279
+ **Response**
280
+
281
+ | Field | Notes |
282
+ |---|---|
283
+ | `event_id` | New ULID for the resent delivery |
284
+ | `original_event_id` | ULID of the original event |
285
+ | `event_type` | Same as the original |
286
+ | `project_id` / `invoice_id` / `target_url` | Lineage |
287
+
288
+ **Throws**: `TxnodEventNotFoundError`.
289
+
290
+ ---
291
+
292
+ ## Sandbox surface (`client.sandbox.*`)
293
+
294
+ Lazy accessor (`client.sandbox` is constructed on first use; bundlers tree-shake the entire namespace when never referenced) onto the 14 sandbox-mode endpoints introduced by Story 37.2. Use it from a sandbox-API-secret-authenticated client (`apiSecret: 'sk_sandbox_...'` + `environment: 'non-production'`) — production secrets cannot reach these routes (the server returns `production_key_against_sandbox_project`). See [`../05-sandbox.md`](../05-sandbox.md) for the lifecycle picture and per-method worked examples.
295
+
296
+ ```ts
297
+ const client = new TxnodClient({
298
+ projectId: process.env.TXNOD_SANDBOX_PROJECT_ID!,
299
+ apiSecret: process.env.TXNOD_SANDBOX_API_SECRET!, // sk_sandbox_*
300
+ environment: 'non-production',
301
+ });
302
+
303
+ // Lazy — TxnodClientSandbox constructed on first access.
304
+ const r = await client.sandbox.simulatePaid('01HK8MAR2QEXAMPLE000000000');
305
+ ```
306
+
307
+ **Method surface:**
308
+
309
+ | Method | HTTP | Returns | Throws |
310
+ |---|---|---|---|
311
+ | `simulateDetect(invoiceId, opts?)` | `POST /api/v1/sandbox/invoices/{invoiceId}/simulate-detect` | `{ event_id, status: 'detected' }` | `TxnodSandboxInvoiceTransitionInvalidError` (invoice not in `pending`), `TxnodSandboxInvoiceNotFoundError` |
312
+ | `simulatePaid(invoiceId, opts?)` | `POST .../simulate-paid` | `{ event_id, status: 'paid' }` | `TxnodSandboxInvoiceTransitionInvalidError` (invoice not in `detected`) |
313
+ | `simulateOverpaid(invoiceId, params)` | `POST .../simulate-overpaid` | `{ event_id, status: 'paid' }` | `TxnodSandboxInvoiceTransitionInvalidError` |
314
+ | `simulatePartial(invoiceId, params)` | `POST .../simulate-partial` | `{ event_id, status: 'detected' }` | `TxnodSandboxInvoiceTransitionInvalidError` |
315
+ | `simulateExpire(invoiceId)` | `POST .../simulate-expire` | `{ event_id, status: 'expired' }` | `TxnodSandboxInvoiceTransitionInvalidError` (invoice already terminal) |
316
+ | `simulateLatePayment(invoiceId, opts?)` | `POST .../simulate-late-payment` | `{ event_id, status: 'expired_paid_late' }` | `TxnodSandboxInvoiceTransitionInvalidError` (invoice not in `expired`) |
317
+ | `simulateReorg(invoiceId)` | `POST .../simulate-reorg` | `{ event_id, status: 'reverted' }` | `TxnodSandboxInvoiceTransitionInvalidError` (invoice not in `paid`/`overpaid`/`partial`) |
318
+ | `simulateReconfirm(invoiceId)` | `POST .../simulate-reconfirm` | `{ event_id, status: 'paid' }` | `TxnodSandboxInvoiceTransitionInvalidError` (invoice not in `reverted`) |
319
+ | `simulateDuplicateDelivery(invoiceId)` | `POST .../simulate-duplicate-delivery` | `{ event_id }` | `TxnodSandboxInvoiceTerminalError` (no terminal event yet) |
320
+ | `simulateEvent(invoiceId, eventInput)` | `POST .../simulate-event` | `{ event_id, status }` | `TxnodSandboxInvoiceTransitionInvalidError` (current status != `expectedCurrentStatus`) |
321
+ | `clockAdvance(projectId, params)` | `POST /api/v1/sandbox/{projectId}/clock/advance` | `{ advanced, remaining }` | `TxnodSandboxRateLimitExceededError` (>10/min/project) |
322
+ | `reset(projectId)` | `POST /api/v1/sandbox/{projectId}/reset` | `{ status: 'reset' }` | `TxnodSandboxResetFailedError` |
323
+ | `destroy(projectId)` | `DELETE /api/v1/sandbox/{projectId}` | `{ status: 'deleted' }` | `TxnodSandboxDeleteFailedError` |
324
+ | `listWallets(projectId)` | `GET /api/v1/sandbox/{projectId}/wallets` | `WalletsListResponse` | (read-only; no transition errors) |
325
+
326
+ Every `simulate*` and lifecycle method can additionally throw `TxnodSandboxInvoiceNotFoundError` (cross-tenant invoice id), `TxnodSandboxRateLimitExceededError` (per-project caps), and the auth/identity family. The full sandbox error catalogue lives in [`../05-errors.md`](../05-errors.md#sandbox-lifecycle--simulation).
327
+
328
+ **Result envelope types** (SDK-only — these aren't exported from `@txnod/shared`):
329
+
330
+ ```ts
331
+ interface SandboxSimulateResult {
332
+ event_id: string | null; // outbox row id; null when no webhook is enqueued (e.g. simulateDuplicateDelivery re-emits an existing event)
333
+ status: string; // resulting invoice status — narrowed per-method in the table above
334
+ }
335
+
336
+ interface SandboxClockAdvanceResult {
337
+ advanced: number; // count of detected→terminal transitions this call performed
338
+ remaining: number; // detected invoices still under threshold after the bump
339
+ }
340
+
341
+ interface SandboxLifecycleResult {
342
+ status: 'reset' | 'deleted'; // discriminant for which lifecycle op completed
343
+ }
344
+ ```
345
+
346
+ Each method's worked example lives on its JSDoc — IDE hovers surface the full snippet (e.g. hover `client.sandbox.simulatePaid` to see the imports + a runnable invocation).
347
+
348
+ ---
349
+
350
+ # `verifyWebhookSignature`
351
+
352
+ Synchronously verifies an inbound webhook, parses the body, and returns the typed `WebhookEvent`. See [`../04-webhooks.md`](../04-webhooks.md).
353
+
354
+ **Signature**
355
+
356
+ ```ts
357
+ function verifyWebhookSignature(
358
+ headers: Headers | Record<string, string> | Record<string, string[]>,
359
+ rawBody: string,
360
+ secret: string,
361
+ ): WebhookEvent
362
+ ```
363
+
364
+ **Throws** (all are `TxnodError` subclasses):
365
+
366
+ - `TxnodSignatureFormatError` — header absent or not `t=<int>,v1=<hex>`
367
+ - `TxnodTimestampError` — `|now - t| > 300` seconds; `.skew_seconds` is signed delta
368
+ - `TxnodHmacError` — computed HMAC mismatch
369
+ - `TxnodWebhookPayloadParseError` — HMAC passed but body is not valid JSON; `.cause` carries the underlying `SyntaxError`. Treat as 401-class (do not parse, do not act)
370
+
371
+ **Example**
372
+
373
+ ```ts
374
+ const event = verifyWebhookSignature(
375
+ request.headers,
376
+ await request.text(),
377
+ process.env.TXNOD_WEBHOOK_SECRET!,
378
+ );
379
+ ```
380
+
381
+ ---
382
+
383
+ # Named exports
384
+
385
+ Also exported from `@txnod/sdk`:
386
+
387
+ - `PACKAGE_NAME` — the literal `'@txnod/sdk'`.
388
+ - `SDK_VERSION` — the current version string, in sync with `package.json`.
389
+ - Type re-exports: `Coin`, `Chain`, `Network`, `InvoiceStatus`, `WebhookEvent`, `WebhookEventType` (definitions are fully expanded plain-TypeScript aliases inside this tarball — no `zod`, no `@txnod/*` needed at install or typecheck time). Use them when annotating wrappers around `createInvoice` / `searchInvoices` so the union literals stay in sync with the SDK.
390
+ - `verifyAddress(input)` and the corresponding `VerifyAddressInput` / `VerificationResult` types — invoked automatically by `createInvoice` when the matching `TXNOD_<chain>_XPUB` env var is set, but exported so partners can run the same check on a stored invoice off-flow.
391
+ - `AddressVerificationError` — thrown by `verifyAddress` (and by `createInvoice` post-response) on mismatch. Not a `TxnodError` subclass; carries `expected_address` / `derived_address`.
392
+ - Every `Txnod*` error class — see [`errors.md`](./errors.md).
@@ -0,0 +1,161 @@
1
+ ---
2
+ title: "Error class catalogue"
3
+ description: "Complete error_code → class lookup and the narrative groups."
4
+ sdk_version: 1.0.0
5
+ ---
6
+
7
+ # Error class catalogue
8
+
9
+ All classes extend `TxnodError` and are exported from `@txnod/sdk`. The base class carries `error_code`, `status`, `request_id`, and the full RFC 7807 `raw` envelope. See [`../05-errors.md`](../05-errors.md) for usage patterns.
10
+
11
+ ## `error_code` → class
12
+
13
+ The 55-row lookup below covers every server-emitted code in `ERROR_CODES` (58 entries, see [`packages/shared/src/errors/error-codes.ts`](../../../shared/src/errors/error-codes.ts)) that the SDK can surface. Three server-only auth codes (`otp_expired` / `otp_used` / `invite_invalid`) are intentionally not mapped — they only fire from dashboard auth/invite routes that no `TxnodClient` method can reach. If a server bug leaks one of those, `parseProblemDetails` falls through to a generic `TxnodError` via the `degraded()` branch.
14
+
15
+ ### Validation / config
16
+
17
+ | `error_code` | Class | HTTP status | Notes |
18
+ |---|---|---|---|
19
+ | `validation_error` | `TxnodValidationError` | 400 | `err.raw.errors` carries per-field issues |
20
+ | `invalid_coin` | `TxnodInvalidCoinError` | 400 | |
21
+ | `invalid_xpub_format` | `TxnodInvalidXpubFormatError` | 400 | |
22
+ | `invalid_webhook_url` | `TxnodInvalidWebhookUrlError` | 400 | |
23
+ | `coin_not_enabled` | `TxnodCoinNotEnabledError` | 422 | Check enabled coins via `getRates()` |
24
+ | `amount_out_of_range` | `TxnodAmountOutOfRangeError` | 422 | Respect project min/max |
25
+ | `xpub_not_verified` | `TxnodXpubNotVerifiedError` | 409 | Complete verification challenge first |
26
+ | `invoice_not_cancellable` | `TxnodInvoiceNotCancellableError` | 409 | |
27
+ | `invalid_state_transition` | `TxnodInvalidStateTransitionError` | 409 | |
28
+
29
+ ### Auth / identity
30
+
31
+ | `error_code` | Class | HTTP status | Notes |
32
+ |---|---|---|---|
33
+ | `auth_invalid` | `TxnodAuthInvalidError` | 401 | Missing / wrong headers |
34
+ | `signature_invalid` | `TxnodSignatureInvalidError` | 401 | HMAC mismatch |
35
+ | `signature_replayed` | `TxnodSignatureReplayedError` | 401 | Same `(timestamp, signature)` seen before — caller must mint a fresh nonce per attempt |
36
+ | `timestamp_out_of_window` | `TxnodTimestampOutOfWindowError` | 401 | Fix NTP |
37
+ | `key_suspended` | `TxnodKeySuspendedError` | 403 | Operator action |
38
+ | `key_revoked` | `TxnodKeyRevokedError` | 403 | Rotate to new key |
39
+ | `project_suspended` | `TxnodProjectSuspendedError` | 403 | Operator action |
40
+ | `permission_denied` | `TxnodPermissionDeniedError` | 403 | Missing capability |
41
+ | `subscription_expired` | `TxnodSubscriptionExpiredError` | 402 | Operator subscription not `active`; writes blocked. Renew via dashboard `/billing` |
42
+
43
+ ### Not found
44
+
45
+ | `error_code` | Class | HTTP status | Notes |
46
+ |---|---|---|---|
47
+ | `invoice_not_found` | `TxnodInvoiceNotFoundError` | 404 | |
48
+ | `project_not_found` | `TxnodProjectNotFoundError` | 404 | |
49
+ | `wallet_not_found` | `TxnodWalletNotFoundError` | 404 | |
50
+ | `orphan_not_found` | `TxnodOrphanNotFoundError` | 404 | |
51
+ | `event_not_found` | `TxnodEventNotFoundError` | 404 | |
52
+
53
+ ### Conflict / idempotent
54
+
55
+ | `error_code` | Class | HTTP status | Notes |
56
+ |---|---|---|---|
57
+ | `external_id_conflict` | `TxnodExternalIdConflictError` | 409 | **Idempotent** — fetch existing |
58
+ | `orphan_already_attributed` | `TxnodOrphanAlreadyAttributedError` | 409 | **Idempotent** — treat as success |
59
+ | `wallet_not_bound` | `TxnodWalletNotBoundError` | 422 | Operator must bind a verified wallet of matching kind for the requested chain before invoice creation succeeds |
60
+ | `wallet_not_owned` | `TxnodWalletNotOwnedError` | 403 | **Dashboard-only** — never reaches partner API |
61
+ | `wallet_has_active_bindings` | `TxnodWalletHasActiveBindingsError` | 409 | **Dashboard-only** — never reaches partner API |
62
+ | `wallet_kind_mismatch` | `TxnodWalletKindMismatchError` | 422 | **Dashboard-only** — cross-kind bind attempt blocked |
63
+
64
+ ### Chain-runtime — TRON / TON
65
+
66
+ | `error_code` | Class | HTTP status | Notes |
67
+ |---|---|---|---|
68
+ | `tron_no_activated_addresses_available` | `TxnodTronNoActivatedAddressesError` | 422 | TRON pool has zero activated rows; `.walletId` carries the operator wallet id. Operator-action-required |
69
+ | `ton_operator_wallet_not_deployed` | `TxnodTonOperatorWalletNotDeployedError` | 422 | TON operator wallet has not been broadcast on-chain yet — operator must perform the first send to deploy `StateInit` |
70
+ | `ton_invalid_wallet_version` | `TxnodTonInvalidWalletVersionError` | 422 | Configured `TXNOD_TON_WALLET_VERSION` does not match the deployed wallet contract |
71
+ | `ton_jetton_resolve_failed` | `TxnodTonJettonResolveFailedError` | 503 | Toncenter/TONAPI hiccup resolving the jetton master/wallet pair — retry with backoff |
72
+ | `ton_comment_parse_failed` | `TxnodTonCommentParseFailedError` | 422 | Inbound TON tx carried a malformed `payment_token` comment |
73
+
74
+ ### TonConnect (operator-onboarding)
75
+
76
+ These never reach partner methods — they fire on the operator's TonConnect signing flow during xpub onboarding. Exported so the dashboard's typed-error catch can branch correctly.
77
+
78
+ | `error_code` | Class | HTTP status | Notes |
79
+ |---|---|---|---|
80
+ | `tonconnect_payload_expired` | `TxnodTonConnectPayloadExpiredError` | 400 | Challenge payload signed past TTL |
81
+ | `tonconnect_payload_unknown` | `TxnodTonConnectPayloadUnknownError` | 400 | Replay or stale browser tab |
82
+ | `tonconnect_domain_mismatch` | `TxnodTonConnectDomainMismatchError` | 400 | `proof.domain.value` vs `TXNOD_TONCONNECT_DOMAIN` mismatch |
83
+ | `tonconnect_timestamp_skew` | `TxnodTonConnectTimestampSkewError` | 400 | Wallet clock skew outside window |
84
+ | `tonconnect_unknown_wallet_version` | `TxnodTonConnectUnknownWalletVersionError` | 400 | Unrecognised `walletStateInit` version |
85
+ | `tonconnect_signature_invalid` | `TxnodTonConnectSignatureInvalidError` | 400 | Ed25519 verification failed |
86
+ | `tonconnect_network_mismatch` | `TxnodTonConnectNetworkMismatchError` | 400 | Wallet on `-3` (testnet) vs server-configured mainnet, or vice versa |
87
+
88
+ ### Sandbox (lifecycle + simulation)
89
+
90
+ Surfaced by `/api/v1/sandbox/*` REST endpoints, the 14 `client.sandbox.*` SDK methods, and the `sandbox:simulate`-scoped MCP tools.
91
+
92
+ | `error_code` | Class | HTTP status | Notes |
93
+ |---|---|---|---|
94
+ | `sandbox_project_required` | `TxnodSandboxProjectRequiredError` | 403 | Sandbox endpoint hit against a `kind: 'production'` project |
95
+ | `sandbox_per_operator_cap_reached` | `TxnodSandboxPerOperatorCapReachedError` | 422 | Operator at the per-operator sandbox-project cap |
96
+ | `sandbox_key_against_production_project` | `TxnodSandboxKeyAgainstProductionProjectError` | 400 | `sk_sandbox_` secret used against a production project |
97
+ | `production_key_against_sandbox_project` | `TxnodProductionKeyAgainstSandboxProjectError` | 400 | Production secret used against a sandbox project |
98
+ | `sandbox_provisioning_failed` | `TxnodSandboxProvisioningFailedError` | 500 | Server failed to provision the sandbox shell — retry |
99
+ | `sandbox_invoice_transition_invalid` | `TxnodSandboxInvoiceTransitionInvalidError` | 422 | `simulate*` against an invoice in a state that disallows the transition |
100
+ | `sandbox_invoice_not_found` | `TxnodSandboxInvoiceNotFoundError` | 404 | Sandbox invoice not in this sandbox project (oracle-safe across miss reasons) |
101
+ | `sandbox_invoice_terminal` | `TxnodSandboxInvoiceTerminalError` | 422 | `simulateDuplicateDelivery` requires a prior terminal webhook |
102
+ | `sandbox_rate_limit_exceeded` | `TxnodSandboxRateLimitExceededError` | 429 | Per-project sandbox cap hit (e.g. `clockAdvance` ≤ 10/min) |
103
+ | `sandbox_reset_failed` | `TxnodSandboxResetFailedError` | 500 | `client.sandbox.reset` failed mid-purge — retry |
104
+ | `sandbox_delete_failed` | `TxnodSandboxDeleteFailedError` | 500 | `client.sandbox.destroy` failed mid-delete — retry |
105
+ | `sandbox_active_invoice_cap_reached` | `TxnodSandboxActiveInvoiceCapReachedError` | 422 | Sandbox project has too many concurrently-open invoices — finalise or expire some first |
106
+
107
+ ### Transient
108
+
109
+ | `error_code` | Class | HTTP status | Notes |
110
+ |---|---|---|---|
111
+ | `rate_limit_exceeded` | `TxnodRateLimitError` | 429 | `.retry_after_seconds` |
112
+ | `pool_exhausted` | `TxnodPoolExhaustedError` | 503 | `.retry_after_seconds` — hard-cap hit, wait for cooldown |
113
+ | `webhook_capacity_exhausted` | `TxnodWebhookCapacityExhaustedError` | 503 | `.retry_after_seconds` always `0` — partner retry alone won't free capacity, treat as transient outage |
114
+ | `internal_error` | `TxnodServerError` | 5xx | Log `request_id`, retryable |
115
+
116
+ ### SDK-local synthesised (no `error_code` — init-time / verify-time guards)
117
+
118
+ These have no `error_code` in the runtime `CODE_TO_CTOR` map — they synthesise a `ProblemDetails` envelope locally so the `instanceof TxnodError` invariant holds. Catch them around `new TxnodClient(...)`, `parseXpubConfig(...)`, and `verifyWebhookSignature(...)`.
119
+
120
+ The `Synthetic status` column carries a representative HTTP code so the envelope shape stays uniform with server-emitted errors — no HTTP exchange actually happens for the init-time classes (they fire before any request leaves the process); for the verify-time webhook trio, 401 reflects the canonical "treat as unauthenticated" disposition partners should map them to.
121
+
122
+ | Class | `kind` | Synthetic status | When |
123
+ |---|---|---|---|
124
+ | `TxnodSignatureFormatError` | `signature_format` | 401 | `verifyWebhookSignature` — header missing or not `t=<int>,v1=<hex>` |
125
+ | `TxnodHmacError` | `hmac` | 401 | `verifyWebhookSignature` — computed HMAC mismatch |
126
+ | `TxnodTimestampError` | `timestamp` | 401 | `verifyWebhookSignature` — `\|now − t\| > 300`; `.skew_seconds` exposes signed delta |
127
+ | `TxnodWebhookPayloadParseError` | `webhook_payload_parse` | 401 | `verifyWebhookSignature` — HMAC passed but body is not valid JSON; `.cause` is the underlying `SyntaxError` |
128
+ | `TxnodSandboxKeyInProductionError` | `sandbox_key_in_production` | 401 | `new TxnodClient(...)` — `sk_sandbox_` secret with `environment: 'production'` (or production-detected `NODE_ENV`); `.signal`, `.secretPrefix`, `.detectedEnvironment` |
129
+ | `TxnodEnvironmentUnknownError` | `environment_unknown` | 401 | `new TxnodClient(...)` — `sk_sandbox_` secret without explicit `environment` and `NODE_ENV` unset/unknown |
130
+ | `TxnodSandboxXpubInProductionError` | `sandbox_xpub_in_production` | 401 | `parseXpubConfig` / `parseTonConfig` — testnet xpub loaded for a chain whose env-detected mode is production; `.chain`, `.xpubPrefix`, `.detectedEnvironment` |
131
+
132
+ ## Fields on specific classes
133
+
134
+ - `TxnodRateLimitError.retry_after_seconds: number` — value of the `Retry-After` header (0 when absent).
135
+ - `TxnodPoolExhaustedError.retry_after_seconds: number` — value of the `Retry-After` header (0 when absent). Server derives it from the soonest `locked_until` across locked pool rows, falling back to the project's configured cooldown.
136
+ - `TxnodWebhookCapacityExhaustedError.retry_after_seconds: number` — always `0`. The server cannot tell when the operator will add a webhook shard; the actionable signal is the operator-side admin alert raised on every rejection, not partner-side retry timing. `.kind` carries the `error_code` literal; the underlying `chain` is also surfaced in the problem-details body.
137
+ - `TxnodTimestampError.skew_seconds: number` — `now - t`; negative means the webhook timestamp is in the future.
138
+ - `TxnodValidationError.raw.errors` — server-emitted per-field Zod issues.
139
+ - `TxnodTronNoActivatedAddressesError.walletId: string` — operator wallet id (ULID, guaranteed non-empty). The constructor fails fast if the server omits it. Use it to deep-link the operator into `/wallets/{walletId}` for in-dashboard activation.
140
+
141
+ ## Narrative grouping
142
+
143
+ Grouped by branching strategy in [`../05-errors.md`](../05-errors.md):
144
+
145
+ - Fatal — fix inputs / configuration
146
+ - Fatal — auth / identity
147
+ - Fatal — not found
148
+ - Idempotent — treat as success
149
+ - Transient — retry
150
+ - TON-chain runtime
151
+ - TonConnect operator-onboarding
152
+ - Sandbox lifecycle + simulation
153
+ - SDK-local synthesised (init-time guards)
154
+ - Dashboard-only (exported but not reachable from `TxnodClient`)
155
+ - Webhook-local (thrown by `verifyWebhookSignature` only)
156
+
157
+ ## Related
158
+
159
+ - [`../05-errors.md`](../05-errors.md) — branching patterns
160
+ - [`../01-authentication.md`](../01-authentication.md) — auth-family context
161
+ - [`../04-webhooks.md`](../04-webhooks.md) — webhook-local errors