@sodax/sdk 1.5.6-beta → 2.0.0-rc.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 (57) hide show
  1. package/README.md +91 -7
  2. package/ai-exported/AGENTS.md +99 -0
  3. package/ai-exported/integration/README.md +41 -0
  4. package/ai-exported/integration/ai-rules.md +75 -0
  5. package/ai-exported/integration/architecture.md +519 -0
  6. package/ai-exported/integration/chain-specifics.md +189 -0
  7. package/ai-exported/integration/features/README.md +19 -0
  8. package/ai-exported/integration/features/auxiliary-services.md +189 -0
  9. package/ai-exported/integration/features/bridge.md +136 -0
  10. package/ai-exported/integration/features/dex.md +182 -0
  11. package/ai-exported/integration/features/icx-bnusd-baln.md +181 -0
  12. package/ai-exported/integration/features/money-market.md +198 -0
  13. package/ai-exported/integration/features/staking.md +166 -0
  14. package/ai-exported/integration/features/swap.md +207 -0
  15. package/ai-exported/integration/quickstart.md +213 -0
  16. package/ai-exported/integration/recipes/README.md +21 -0
  17. package/ai-exported/integration/recipes/backend-server-init.md +69 -0
  18. package/ai-exported/integration/recipes/chain-key-narrowing.md +65 -0
  19. package/ai-exported/integration/recipes/gas-estimation.md +33 -0
  20. package/ai-exported/integration/recipes/initialize-sodax.md +53 -0
  21. package/ai-exported/integration/recipes/raw-tx-flow.md +71 -0
  22. package/ai-exported/integration/recipes/result-and-errors.md +104 -0
  23. package/ai-exported/integration/recipes/signed-tx-flow.md +46 -0
  24. package/ai-exported/integration/recipes/testing.md +101 -0
  25. package/ai-exported/integration/reference/README.md +18 -0
  26. package/ai-exported/integration/reference/chain-keys.md +67 -0
  27. package/ai-exported/integration/reference/error-codes.md +165 -0
  28. package/ai-exported/integration/reference/glossary.md +32 -0
  29. package/ai-exported/integration/reference/public-api.md +138 -0
  30. package/ai-exported/integration/reference/wallet-providers.md +62 -0
  31. package/ai-exported/migration/README.md +58 -0
  32. package/ai-exported/migration/ai-rules.md +80 -0
  33. package/ai-exported/migration/breaking-changes/architecture.md +335 -0
  34. package/ai-exported/migration/breaking-changes/result-and-errors.md +363 -0
  35. package/ai-exported/migration/breaking-changes/type-system.md +321 -0
  36. package/ai-exported/migration/checklist.md +61 -0
  37. package/ai-exported/migration/features/README.md +35 -0
  38. package/ai-exported/migration/features/auxiliary-services.md +156 -0
  39. package/ai-exported/migration/features/bridge.md +125 -0
  40. package/ai-exported/migration/features/dex.md +143 -0
  41. package/ai-exported/migration/features/icx-bnusd-baln.md +151 -0
  42. package/ai-exported/migration/features/money-market.md +214 -0
  43. package/ai-exported/migration/features/staking.md +138 -0
  44. package/ai-exported/migration/features/swap.md +198 -0
  45. package/ai-exported/migration/recipes.md +288 -0
  46. package/ai-exported/migration/reference/README.md +18 -0
  47. package/ai-exported/migration/reference/deleted-exports.md +100 -0
  48. package/ai-exported/migration/reference/error-code-crosswalk.md +104 -0
  49. package/ai-exported/migration/reference/return-shapes.md +49 -0
  50. package/ai-exported/migration/reference/sodax-config.md +52 -0
  51. package/dist/index.cjs +32154 -31601
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.cts +8442 -6974
  54. package/dist/index.d.ts +8442 -6974
  55. package/dist/index.mjs +32642 -32130
  56. package/dist/index.mjs.map +1 -1
  57. package/package.json +19 -11
@@ -0,0 +1,519 @@
1
+ # Architecture — `@sodax/sdk` v2
2
+
3
+ Every v2 design concept the SDK rests on, in a single TOC-navigable file. Read end-to-end if you're new to v2; skim by section if you're solving a specific problem.
4
+
5
+ ## Section index
6
+
7
+ 1. [Hub-and-spoke model](#1-hub-and-spoke-model) — Sonic is the hub; 19 spoke chains route through it.
8
+ 2. [`SpokeService` router](#2-spokeservice-router) — single internal dispatcher; no per-chain provider classes.
9
+ 3. [`Sodax` facade and service graph](#3-sodax-facade-and-service-graph) — one instance owns every feature service.
10
+ 4. [`ConfigService`](#4-configservice) — dynamic config from backend with packaged-defaults fallback.
11
+ 5. [`ChainKeys` and chain-key narrowing](#5-chainkeys-and-chain-key-narrowing) — `GetChainType<K>`, `GetWalletProviderType<K>`.
12
+ 6. [`WalletProviderSlot<K, Raw>`](#6-walletproviderslotk-raw) — discriminated union for signed vs raw flows.
13
+ 7. [`Result<T, SodaxError<C>>`](#7-resultt-sodaxerrorc) — every async public method returns this.
14
+ 8. [`SodaxError<C>` and the 13-code vocabulary](#8-sodaxerrorc-and-the-13-code-vocabulary) — canonical error class.
15
+ 9. [Relay layer: `relayTxAndWaitPacket` and `mapRelayFailure`](#9-relay-layer-relaytxandwaitpacket-and-mapfailrelay) — cross-chain coordination.
16
+
17
+ ---
18
+
19
+ ## 1. Hub-and-spoke model
20
+
21
+ SODAX is a cross-chain DeFi platform built on a hub-and-spoke architecture. **Sonic is the hub chain.** Every cross-chain operation flows through it:
22
+
23
+ ```
24
+ spoke chain (e.g. Arbitrum)
25
+
26
+ │ spoke transaction (deposit / approve / send)
27
+
28
+ SpokeService (in @sodax/sdk)
29
+
30
+ │ submitTransaction (from intentRelay module)
31
+
32
+ relay layer
33
+
34
+ │ relayTxAndWaitPacket → packet 'executed' on hub
35
+
36
+ EvmHubProvider
37
+
38
+ │ hub-side contracts (vault, asset manager, wallet abstraction)
39
+
40
+ destination spoke (e.g. Stellar)
41
+ ```
42
+
43
+ For most consumers, this whole pipeline is one method call (`sodax.swaps.swap(...)`, `sodax.bridge.bridge(...)`, etc.). The result is a `Result<TxHashPair>` where `TxHashPair = { srcChainTxHash, dstChainTxHash }` — the spoke transaction hash on the source chain and the relayed hub transaction hash. The relay state in between is handled internally.
44
+
45
+ **You will hit "the relay" surface area** when:
46
+
47
+ - An operation fails partway through and the error is a relay code (`'TX_SUBMIT_FAILED'`, `'RELAY_TIMEOUT'`, `'RELAY_POLLING_FAILED'`) — see § 9.
48
+ - You build a custom orchestration on top of `relayTxAndWaitPacket` directly (rare; usually a feature service is the right abstraction).
49
+ - You need to recover assets stuck in a hub wallet — use `RecoveryService`.
50
+
51
+ ### Supported chains
52
+
53
+ 20 total. EVM (12): Sonic (hub), Ethereum, Arbitrum, Base, BSC, Optimism, Polygon, Avalanche, HyperEVM, Lightlink, Redbelly, Kaia. Non-EVM (8): Solana, Sui, Stellar, ICON, Injective, NEAR, Stacks, Bitcoin. See [`reference/`](reference/) § "Chain keys" for the full table with relay IDs and address-type mapping.
54
+
55
+ ---
56
+
57
+ ## 2. `SpokeService` router
58
+
59
+ The SDK does not require callers to construct per-chain provider classes. There is no `EvmSpokeProvider`, `SolanaSpokeProvider`, etc. for consumers to construct.
60
+
61
+ Instead, the SDK has **one** `SpokeService` instance (owned by `Sodax`) which holds one per-chain-family service internally:
62
+
63
+ ```
64
+ SpokeService
65
+ ├── EvmSpokeService (handles all 12 EVM chains)
66
+ ├── SonicSpokeService (special-cased for the hub)
67
+ ├── SolanaSpokeService
68
+ ├── SuiSpokeService
69
+ ├── StellarSpokeService
70
+ ├── IconSpokeService
71
+ ├── InjectiveSpokeService
72
+ ├── StacksSpokeService
73
+ ├── BitcoinSpokeService
74
+ └── NearSpokeService
75
+ ```
76
+
77
+ Public entry: `sodax.spoke.getSpokeService(chainKey)` (typed). Feature services route to the right family by calling this internally — consumer-side code never does.
78
+
79
+ ### How the router uses chain keys
80
+
81
+ The chain key on the request payload (e.g. `srcChainKey: ChainKeys.ETHEREUM_MAINNET`) does two things at once:
82
+
83
+ 1. **Type-level narrowing** — TypeScript preserves the literal in the generic `K`. From `K`, the type system derives:
84
+ - `GetChainType<K>` → chain family (`'EVM' | 'BITCOIN' | 'SOLANA' | …`)
85
+ - `GetWalletProviderType<K>` → chain-specific wallet provider interface (`IEvmWalletProvider`, …)
86
+ - `TxReturnType<K, Raw>` → chain-specific tx return shape
87
+ 2. **Runtime dispatch** — `getChainType(chainKey)` (the runtime helper) resolves the family at runtime, and `SpokeService` calls the right family service.
88
+
89
+ The chain key is the bridge between the type system and runtime routing.
90
+
91
+ ---
92
+
93
+ ## 3. `Sodax` facade and service graph
94
+
95
+ The `Sodax` class is the public entry point. It constructs and wires every service once at construction time, then reuses them across calls:
96
+
97
+ ```ts
98
+ const sodax = new Sodax(/* optional DeepPartial<SodaxConfig> */);
99
+ await sodax.config.initialize(); // fetch dynamic config; fall back to packaged defaults
100
+
101
+ // All feature services accessed off the instance:
102
+ await sodax.swaps.createIntent({ params, raw: false, walletProvider });
103
+ await sodax.moneyMarket.supply({ params, raw: false, walletProvider });
104
+ await sodax.bridge.bridge({ params, raw: false, walletProvider });
105
+ ```
106
+
107
+ ### Service graph
108
+
109
+ ```
110
+ Sodax
111
+ ├── swaps — SwapService (intent-based swaps via solver)
112
+ ├── moneyMarket — MoneyMarketService (cross-chain lending/borrowing)
113
+ ├── bridge — BridgeService (cross-chain token transfers)
114
+ ├── staking — StakingService (SODA/xSoda staking)
115
+ ├── dex — DexService (concentrated liquidity, AMM)
116
+ ├── migration — MigrationService (ICX/bnUSD/BALN migration)
117
+ ├── partners — PartnerService (partner fee claiming)
118
+ ├── recovery — RecoveryService (withdraw stuck hub-wallet assets)
119
+ ├── backendApi — BackendApiService (intent lookup, swap submission, config fetching)
120
+ ├── config — ConfigService (dynamic config; see § 4)
121
+ ├── hubProvider — HubProvider (hub contract interactions; concrete impl `EvmHubProvider`)
122
+ └── spoke — SpokeService (per-chain-family router; see § 2)
123
+ ```
124
+
125
+ All feature services receive `{ hubProvider, config, spoke }` via constructor injection. You don't instantiate them directly — accessing `sodax.<feature>` is the public API.
126
+
127
+ ### Constructor
128
+
129
+ ```ts
130
+ import { Sodax, type SodaxConfig, type DeepPartial } from '@sodax/sdk';
131
+
132
+ new Sodax(config?: DeepPartial<SodaxConfig>): Sodax;
133
+ ```
134
+
135
+ `SodaxConfig` carries (all optional via `DeepPartial`):
136
+
137
+ - `solver` — `{ intentsContract, solverApiEndpoint, protocolIntentsContract }` (endpoints).
138
+ - `swaps` — `SwapsConfig` (supported solver tokens per chain).
139
+ - `moneyMarket`, `bridge`, `staking`, `dex`, `migration`, `partner`, `recovery` — feature-specific config (contract addresses, etc.).
140
+ - `rpcConfig` — mapped type keyed by `ChainKey` values; `BitcoinRpcConfig` for `BITCOIN_MAINNET`, `StellarRpcConfig` for `STELLAR_MAINNET`, RPC URL strings for everything else.
141
+ - `hubConfig` — hub provider config (consumed by `EvmHubProvider`).
142
+ - `backendApi` — `{ url, api?: IConfigApi }` for custom backend / sandbox endpoints.
143
+
144
+ In production, the packaged defaults are sufficient — pass nothing and call `await sodax.config.initialize()` to load fresh data from the backend.
145
+
146
+ ---
147
+
148
+ ## 4. `ConfigService`
149
+
150
+ Replaces every static lookup table that v1 exported as a global (`hubAssets`, `moneyMarketSupportedTokens`, `solverSupportedTokens`, `SodaTokens`, etc.). Loads from the backend API on `initialize()`; falls back to packaged defaults from `@sodax/types` if the backend is unreachable.
151
+
152
+ ### Lifecycle
153
+
154
+ ```ts
155
+ const sodax = new Sodax();
156
+ await sodax.config.initialize(); // network call + cache; fall back on failure
157
+
158
+ // After init:
159
+ sodax.config.isValidSpokeChainKey(chainKey);
160
+ sodax.config.findSupportedTokenBySymbol(chainKey, 'USDC');
161
+ sodax.config.getSupportedTokensPerChain();
162
+ sodax.config.getOriginalAssetAddress(chainKey, hubAsset); // (chainId, hubAsset) → original spoke-side address
163
+ sodax.config.getMoneyMarketReserveAssets();
164
+ sodax.config.getMoneyMarketToken(chainKey, tokenAddress); // resolve a hub-asset address → XToken
165
+ sodax.config.getSupportedSwapTokensByChainId(chainKey); // solver-supported tokens for one chain
166
+ sodax.config.getSpokeChainKeyFromIntentRelayChainId(BigInt(...));
167
+ ```
168
+
169
+ Every feature service consumes `ConfigService` internally. The data flows through `XToken` (which now carries `vault` and `hubAsset` directly per token) and through service-method wrappers like `sodax.moneyMarket.getSupportedTokens()`.
170
+
171
+ ### Why dynamic
172
+
173
+ Chain configs (vault addresses, supported tokens, fee parameters) change between SDK releases. Dynamic loading means the SDK can pick up new chains and tokens without a version bump. The packaged defaults are a fallback for offline / sandbox / pre-release conditions.
174
+
175
+ ### Custom backend
176
+
177
+ Inject a custom `IConfigApi` for testing or sandbox via `SodaxConfig.backendApi.api`. The contract: every method on `IConfigApi` returns `Promise<Result<T>>`.
178
+
179
+ ---
180
+
181
+ ## 5. `ChainKeys` and chain-key narrowing
182
+
183
+ `ChainKeys` is a `const` object with one string property per chain. The values form the `ChainKey` union (full chain set, including hub) and `SpokeChainKey` (spoke chains only — no hub).
184
+
185
+ ```ts
186
+ import { ChainKeys, type ChainKey, type SpokeChainKey } from '@sodax/sdk';
187
+
188
+ ChainKeys.SONIC_MAINNET // 'sonic'
189
+ ChainKeys.ETHEREUM_MAINNET // 'ethereum'
190
+ ChainKeys.ARBITRUM_MAINNET // '0xa4b1.arbitrum'
191
+ ChainKeys.ICON_MAINNET // '0x1.icon'
192
+ ChainKeys.BITCOIN_MAINNET // 'bitcoin'
193
+ // …
194
+ ```
195
+
196
+ The full table with values + chain family + relay id is in [`reference/`](reference/) § "Chain keys".
197
+
198
+ ### Narrowing
199
+
200
+ When a literal `srcChainKey` flows into a generic method, TypeScript preserves it as a value type. From that one literal:
201
+
202
+ ```ts
203
+ type K = typeof ChainKeys.ETHEREUM_MAINNET; // '0xa4b1...' (the literal)
204
+
205
+ GetChainType<K> // 'EVM'
206
+ GetWalletProviderType<K> // IEvmWalletProvider
207
+ TxReturnType<K, false> // Hash (the EVM signed-tx return)
208
+ TxReturnType<K, true> // EvmRawTransaction
209
+ ```
210
+
211
+ This is what allows `sodax.swaps.createIntent({ params: { srcChainKey: ChainKeys.ETHEREUM_MAINNET, ... }, raw: false, walletProvider: <evm-provider> })` to enforce at compile time that `walletProvider` is `IEvmWalletProvider` and not a Solana or Bitcoin one — there's no runtime check; the type system does it.
212
+
213
+ ### Runtime helpers
214
+
215
+ ```ts
216
+ import { getChainType, isEvmChainKeyType, isSolanaChainKeyType, isBitcoinChainKeyType, /* … */ } from '@sodax/sdk';
217
+
218
+ getChainType(chainKey); // 'EVM' | 'BITCOIN' | 'SOLANA' | 'STELLAR' | 'SUI' | 'ICON' | 'INJECTIVE' | 'STACKS' | 'NEAR' | 'SONIC'
219
+ isEvmChainKeyType(chainKey); // boolean (with type guard)
220
+ ```
221
+
222
+ Use these for runtime branching — the typed helpers are friendlier than ad-hoc string equality and they don't go stale when new chains are added (they consult the central registry).
223
+
224
+ ---
225
+
226
+ ## 6. `WalletProviderSlot<K, Raw>`
227
+
228
+ The discriminated union that distinguishes signed-execution from raw-tx-building at compile time.
229
+
230
+ ```ts
231
+ type WalletProviderSlot<K extends ChainKey, Raw extends boolean> =
232
+ Raw extends true
233
+ ? { raw: true; walletProvider?: never }
234
+ : { raw: false; walletProvider: GetWalletProviderType<K> };
235
+ ```
236
+
237
+ ### Three rules
238
+
239
+ 1. **`raw: true`** — `walletProvider` is **forbidden** (`?: never` rejects any value). The method returns a raw, unsigned tx payload (`TxReturnType<K, true>` — `EvmRawTransaction`, `SolanaRawTransaction`, etc.).
240
+ 2. **`raw: false`** — `walletProvider` is **required** and chain-narrowed via `GetWalletProviderType<K>`. The method signs and broadcasts; returns a tx hash (`TxReturnType<K, false>`).
241
+ 3. **Mandatory discriminator** — without `raw: true` or `raw: false` in the literal, TypeScript can't pick a branch. Forgetting the discriminator surfaces as: `Object literal may only specify known properties, and 'walletProvider' does not exist in type ...`.
242
+
243
+ ### Usage in service methods
244
+
245
+ Every signed-execution method accepts `WalletProviderSlot<K, false>` (intersected into the action params type). Every raw-tx-building method accepts `WalletProviderSlot<K, true>` (sometimes both, via the `Raw extends boolean` generic).
246
+
247
+ ```ts
248
+ // Signed:
249
+ sodax.swaps.createIntent({ params, raw: false, walletProvider });
250
+
251
+ // Raw:
252
+ sodax.swaps.createIntent({ params, raw: true });
253
+
254
+ // Compile errors:
255
+ sodax.swaps.createIntent({ params, walletProvider }); // missing 'raw'
256
+ sodax.swaps.createIntent({ params, raw: true, walletProvider }); // walletProvider forbidden when raw: true
257
+ sodax.swaps.createIntent({ params, raw: false }); // walletProvider required when raw: false
258
+ ```
259
+
260
+ ### When to pick which
261
+
262
+ - **`raw: false`** — your app holds the wallet (Node script with private key, browser dApp with extension). Default for most flows.
263
+ - **`raw: true`** — your app builds the tx but a different system signs it (gnosis safe, hardware wallet across an isolation boundary, custom multi-sig). The returned payload is chain-specific; submit it via your own signing infra.
264
+
265
+ ### Read-only methods
266
+
267
+ Some read-only methods (`isAllowanceValid`, `getDeposit`) intersect with `WalletProviderSlot<K, Raw>` even though they don't actually consult the wallet provider. The underlying read doesn't need a wallet — but the method signature is unified with write methods. Use `{ params, raw: true }` for these; no wallet provider needed:
268
+
269
+ ```ts
270
+ const result = await sodax.dex.assetService.isAllowanceValid({ params, raw: true });
271
+ ```
272
+
273
+ ---
274
+
275
+ ## 7. `Result<T, SodaxError<C>>`
276
+
277
+ Every async public method returns this. There is no `throw` across a service boundary in v2.
278
+
279
+ ### Shape
280
+
281
+ ```ts
282
+ type Result<T, E = Error | unknown> =
283
+ | { ok: true; value: T }
284
+ | { ok: false; error: E };
285
+ ```
286
+
287
+ Defined in `@sodax/types`, re-exported from `@sodax/sdk`.
288
+
289
+ ### Branching
290
+
291
+ ```ts
292
+ const result = await sodax.swaps.createIntent({ params, raw: false, walletProvider });
293
+ if (!result.ok) {
294
+ // result.error: SodaxError<C> for the narrow code union of createIntent
295
+ return;
296
+ }
297
+ const { tx, intent, relayData } = result.value;
298
+ ```
299
+
300
+ ### Sub-Result propagation
301
+
302
+ Inside SDK code (and useful for consumer wrappers):
303
+
304
+ ```ts
305
+ async function myWorkflow(): Promise<Result<MyOutput, SodaxError<MyCodes>>> {
306
+ const sub = await this.subOperation();
307
+ if (!sub.ok) return sub; // forward as-is; narrower code unions are structurally assignable
308
+
309
+ // success path
310
+ return { ok: true, value: /* … */ };
311
+ }
312
+ ```
313
+
314
+ Narrower code unions (e.g. `'INTENT_CREATION_FAILED' | 'VALIDATION_FAILED'`) are structurally assignable to wider unions, so forwarding a sub-Result without re-wrapping typechecks.
315
+
316
+ ### No helpers like `toResult` / `tryCatch`
317
+
318
+ There's no `safeCall` wrapper. Explicit `try/catch` at each public method boundary is the deliberate convention — see `packages/sdk/CLAUDE.md` (internal) for the full pattern. Consumer-side code does the same: branch on `result.ok` and let success and failure paths diverge cleanly.
319
+
320
+ ### Pitfall
321
+
322
+ A `try { await sodax.<method>(...) } catch` block does **not** catch `Result` `{ ok: false }` — the SDK doesn't throw. The `catch` only fires for synchronous wrapper exceptions (e.g. missing `walletProvider`). Always branch on `result.ok`.
323
+
324
+ ---
325
+
326
+ ## 8. `SodaxError<C>` and the 13-code vocabulary
327
+
328
+ The canonical error class. Every SDK-emitted error is a `SodaxError<C>` parameterised by a code from a closed 13-element union.
329
+
330
+ ### Shape
331
+
332
+ ```ts
333
+ class SodaxError<C extends SodaxErrorCode = SodaxErrorCode> extends Error {
334
+ readonly code: C; // closed 13-code reason union
335
+ readonly feature: SodaxFeature; // 'swap' | 'moneyMarket' | 'bridge' | 'staking' | 'migration' | 'dex' | 'partner' | 'recovery'
336
+ readonly cause?: unknown;
337
+ readonly context?: SodaxErrorContext;
338
+
339
+ toJSON(): SodaxErrorJSON<C>; // canonical logger surface
340
+ }
341
+ ```
342
+
343
+ ### The 13 codes
344
+
345
+ | Code | Meaning |
346
+ |---|---|
347
+ | `VALIDATION_FAILED` | Pre-flight invariant tripped. |
348
+ | `INTENT_CREATION_FAILED` | Building the intent / payload failed. |
349
+ | `EXECUTION_FAILED` | Orchestrator-level catch-all for multi-step ops. |
350
+ | `TX_VERIFICATION_FAILED` | Spoke-side `verifyTxHash` returned false / threw. |
351
+ | `TX_SUBMIT_FAILED` | Spoke tx landed; relay POST submit failed. |
352
+ | `RELAY_TIMEOUT` | Destination packet didn't reach `executed` within timeout. |
353
+ | `RELAY_FAILED` | Relay polling outage / unrecognised relay error. |
354
+ | `APPROVE_FAILED` | Token approval call failed. |
355
+ | `ALLOWANCE_CHECK_FAILED` | Reading on-chain allowance failed. |
356
+ | `GAS_ESTIMATION_FAILED` | Gas estimation returned an error. |
357
+ | `LOOKUP_FAILED` | Read-only on-chain query / off-chain config fetch. |
358
+ | `EXTERNAL_API_ERROR` | Upstream API call failed (solver, backend). |
359
+ | `UNKNOWN` | Last-resort catch in an outer `try`. Should be rare. |
360
+
361
+ The full per-code semantics, common context fields, per-feature narrow unions, and retry guidance are in [`reference/`](reference/) § "Error codes".
362
+
363
+ ### `(feature, code)` discrimination
364
+
365
+ The pair `(error.feature, error.code)` is the canonical discriminator. Use it for both logging tags and switch statements:
366
+
367
+ ```ts
368
+ import { isSodaxError } from '@sodax/sdk';
369
+
370
+ if (!result.ok && isSodaxError(result.error)) {
371
+ if (result.error.feature === 'moneyMarket' && result.error.code === 'INTENT_CREATION_FAILED') {
372
+ /* show "couldn't build supply" */
373
+ }
374
+ if (result.error.code === 'RELAY_TIMEOUT') {
375
+ /* retry */
376
+ }
377
+ }
378
+ ```
379
+
380
+ ### Per-method narrow unions
381
+
382
+ Public methods declare narrow code unions via `Extract<SodaxErrorCode, ...>`:
383
+
384
+ ```ts
385
+ type CreateSupplyIntentErrorCode = Extract<
386
+ SodaxErrorCode,
387
+ 'VALIDATION_FAILED' | 'INTENT_CREATION_FAILED' | 'UNKNOWN'
388
+ >;
389
+ ```
390
+
391
+ Switch exhaustively over the narrow union when you know which method emitted the error. The full per-method catalogue is in [`reference/`](reference/) § "Per-method error codes".
392
+
393
+ ### Context fields
394
+
395
+ The `error.context` field carries per-error metadata. Reserved keys:
396
+
397
+ | Key | Type | Used by |
398
+ |---|---|---|
399
+ | `action` | string | Discriminates user-facing operation (e.g. `'supply'`, `'stake'`, `'migrateBaln'`). |
400
+ | `phase` | `SodaxPhase` | Orchestration phase (`'validate'`, `'intentCreation'`, `'verify'`, `'submit'`, `'relay'`, `'destinationExecution'`, `'execution'`, `'postExecution'`, `'approve'`, `'allowanceCheck'`, `'gasEstimation'`, `'lookup'`). |
401
+ | `srcChainKey`, `dstChainKey` | `ChainKey` strings | Chain-related errors. |
402
+ | `relayCode` | `'SUBMIT_TX_FAILED' \| 'RELAY_TIMEOUT' \| 'RELAY_POLLING_FAILED' \| 'UNKNOWN'` | Relay-layer errors (mirror of the lower-level relay code). |
403
+ | `api` | `'solver' \| 'backend'` | `EXTERNAL_API_ERROR` only. |
404
+ | `method` | string | `LOOKUP_FAILED` only. Names the failing read method. |
405
+ | `direction` | `'forward' \| 'reverse'` | Migration's `migratebnUSD` only. |
406
+ | `field`, `reason` | string | `VALIDATION_FAILED`. Names the precondition that tripped. |
407
+ | `[key: string]` | unknown | Open at the index signature for feature-specific metadata. |
408
+
409
+ ### `toJSON()` and logger integration
410
+
411
+ `JSON.stringify(error)` calls `toJSON()` automatically. The serializer:
412
+
413
+ - Coerces `bigint` to string anywhere in `context`.
414
+ - Walks `cause` chains up to depth 3.
415
+ - Stringifies `Date`, `Map`, `Set`, `Error`, and class instances safely.
416
+ - Bounds depth at 5 to prevent cycles.
417
+
418
+ Consumer-side:
419
+
420
+ ```ts
421
+ // Sentry
422
+ Sentry.captureException(err, {
423
+ tags: { feature: err.feature, code: err.code, action: err.context?.action },
424
+ });
425
+
426
+ // Pino
427
+ logger.error({ err }, 'sodax operation failed');
428
+ ```
429
+
430
+ ### `isSodaxError` (preferred over `instanceof`)
431
+
432
+ ```ts
433
+ import { isSodaxError, isFeatureError } from '@sodax/sdk';
434
+
435
+ if (isSodaxError(e)) {
436
+ // e: SodaxError<SodaxErrorCode>
437
+ }
438
+
439
+ const isSwapError = isFeatureError('swap');
440
+ if (isSwapError(e)) {
441
+ // e: SodaxError with feature: 'swap'
442
+ }
443
+ ```
444
+
445
+ Use these in cross-bundle code (apps with mixed ESM/CJS resolution, monorepos with multiple package copies). `instanceof SodaxError` returns `false` when `@sodax/sdk` is loaded twice in the same bundle — `isSodaxError` walks structural shape and works regardless.
446
+
447
+ ---
448
+
449
+ ## 9. Relay layer: `relayTxAndWaitPacket` and `mapRelayFailure`
450
+
451
+ Cross-chain coordination is exposed as two top-level functions (re-exported from `@sodax/sdk`'s barrel):
452
+
453
+ - `submitTransaction({ relayerApiEndpoint, srcChainKey, txHash, payload })` — POSTs the spoke transaction to the relay submit endpoint and resolves the relay's first-stage acknowledgement.
454
+ - `relayTxAndWaitPacket({ relayerApiEndpoint, srcChainKey, dstChainKey, txHash, payload, timeout? })` — runs `submitTransaction` and then polls until the destination packet reaches `executed`.
455
+
456
+ These functions are **not** exposed on the `Sodax` instance. Consumers don't call them directly — every feature service (`swaps.swap`, `bridge.bridge`, `staking.stake`, …) wraps the spoke→hub leg internally. If you genuinely need custom relay orchestration (rare), import `relayTxAndWaitPacket` / `submitTransaction` from `@sodax/sdk` and pass the same `relayerApiEndpoint` your `Sodax` instance uses.
457
+
458
+ ### Relay-layer error contract
459
+
460
+ The relay layer keeps a stable string vocabulary of its own (separate from the 13 `SodaxErrorCode`s):
461
+
462
+ ```ts
463
+ type RelayCode =
464
+ | 'SUBMIT_TX_FAILED' // POST to relay submit endpoint failed
465
+ | 'RELAY_TIMEOUT' // Poll loop exhausted timeout
466
+ | 'RELAY_POLLING_FAILED' // Relay endpoint outage / unrecognised response
467
+ | 'UNKNOWN'; // Anything else
468
+ ```
469
+
470
+ These codes appear on `error.context.relayCode` of the `SodaxError` that surfaces to consumers.
471
+
472
+ ### `mapRelayFailure`
473
+
474
+ The single shared mapper from a relay-layer error to a `SodaxError`. Every feature service uses it internally — exported for custom orchestration:
475
+
476
+ ```ts
477
+ import { mapRelayFailure, relayTxAndWaitPacket } from '@sodax/sdk';
478
+
479
+ try {
480
+ await relayTxAndWaitPacket({ /* relayerApiEndpoint, srcChainKey, dstChainKey, txHash, payload, timeout? */ });
481
+ } catch (e) {
482
+ const sodaxError = mapRelayFailure(e, {
483
+ feature: 'swap',
484
+ action: 'createIntent',
485
+ srcChainKey,
486
+ dstChainKey,
487
+ // phase: 'destinationExecution', // optional override; used by migration's bnUSD secondary watcher
488
+ });
489
+ return { ok: false, error: sodaxError };
490
+ }
491
+ ```
492
+
493
+ Maps to one of: `'TX_SUBMIT_FAILED'`, `'RELAY_TIMEOUT'`, `'RELAY_FAILED'`, or `'EXECUTION_FAILED'`.
494
+
495
+ ### When to use the relay layer directly
496
+
497
+ Almost never. The right abstraction is a feature service — `sodax.swaps.swap(...)`, `sodax.bridge.bridge(...)`, `sodax.staking.stake(...)` — which internally builds the spoke tx, calls `relayTxAndWaitPacket`, runs hub-side post-execution, and returns the unified `Result<TxHashPair>`.
498
+
499
+ You drop down to the relay layer only when:
500
+
501
+ - You're building custom orchestration not represented by a feature service.
502
+ - You're testing relay behavior (E2E test that intentionally drops the destination tx).
503
+ - You're writing a custom feature on top of the SDK primitives.
504
+
505
+ ### Cross-references
506
+
507
+ - `RecoveryService` for pulling stuck hub-wallet assets back to a spoke chain: see [`features/auxiliary-services.md`](features/auxiliary-services.md).
508
+ - Per-feature error codes related to relay (e.g. `'TX_SUBMIT_FAILED'`, `'RELAY_TIMEOUT'`): [`reference/`](reference/) § "Error codes".
509
+
510
+ ---
511
+
512
+ ## Cross-references
513
+
514
+ - Quickstart (install + initialize): [`quickstart.md`](quickstart.md).
515
+ - Lookup tables (chain keys, error codes, public API surface): [`reference/`](reference/).
516
+ - Recipes (init, result handling, raw vs signed, narrowing, testing): [`recipes/`](recipes/).
517
+ - Per-feature usage: [`features/`](features/).
518
+ - Non-EVM chain quirks: [`chain-specifics.md`](chain-specifics.md).
519
+ - v1 → v2 porting context: [`../migration/README.md`](../migration/README.md).