@sodax/sdk 2.0.0-rc.3 → 2.0.0-rc.4

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 +8 -60
  2. package/dist/index.cjs +28 -39
  3. package/dist/index.d.cts +21 -108
  4. package/dist/index.d.ts +21 -108
  5. package/dist/index.mjs +34 -43
  6. package/package.json +22 -22
  7. package/ai-exported/AGENTS.md +0 -99
  8. package/ai-exported/integration/README.md +0 -41
  9. package/ai-exported/integration/ai-rules.md +0 -75
  10. package/ai-exported/integration/architecture.md +0 -533
  11. package/ai-exported/integration/chain-specifics.md +0 -189
  12. package/ai-exported/integration/features/README.md +0 -19
  13. package/ai-exported/integration/features/auxiliary-services.md +0 -189
  14. package/ai-exported/integration/features/bridge.md +0 -136
  15. package/ai-exported/integration/features/dex.md +0 -182
  16. package/ai-exported/integration/features/icx-bnusd-baln.md +0 -181
  17. package/ai-exported/integration/features/money-market.md +0 -198
  18. package/ai-exported/integration/features/staking.md +0 -166
  19. package/ai-exported/integration/features/swap.md +0 -207
  20. package/ai-exported/integration/quickstart.md +0 -213
  21. package/ai-exported/integration/recipes/README.md +0 -21
  22. package/ai-exported/integration/recipes/backend-server-init.md +0 -69
  23. package/ai-exported/integration/recipes/chain-key-narrowing.md +0 -65
  24. package/ai-exported/integration/recipes/gas-estimation.md +0 -33
  25. package/ai-exported/integration/recipes/initialize-sodax.md +0 -78
  26. package/ai-exported/integration/recipes/raw-tx-flow.md +0 -71
  27. package/ai-exported/integration/recipes/result-and-errors.md +0 -104
  28. package/ai-exported/integration/recipes/signed-tx-flow.md +0 -46
  29. package/ai-exported/integration/recipes/testing.md +0 -101
  30. package/ai-exported/integration/reference/README.md +0 -18
  31. package/ai-exported/integration/reference/chain-keys.md +0 -67
  32. package/ai-exported/integration/reference/error-codes.md +0 -165
  33. package/ai-exported/integration/reference/glossary.md +0 -32
  34. package/ai-exported/integration/reference/public-api.md +0 -138
  35. package/ai-exported/integration/reference/wallet-providers.md +0 -62
  36. package/ai-exported/migration/README.md +0 -58
  37. package/ai-exported/migration/ai-rules.md +0 -80
  38. package/ai-exported/migration/breaking-changes/architecture.md +0 -344
  39. package/ai-exported/migration/breaking-changes/result-and-errors.md +0 -363
  40. package/ai-exported/migration/breaking-changes/type-system.md +0 -341
  41. package/ai-exported/migration/checklist.md +0 -67
  42. package/ai-exported/migration/features/README.md +0 -35
  43. package/ai-exported/migration/features/auxiliary-services.md +0 -156
  44. package/ai-exported/migration/features/bridge.md +0 -128
  45. package/ai-exported/migration/features/dex.md +0 -143
  46. package/ai-exported/migration/features/icx-bnusd-baln.md +0 -151
  47. package/ai-exported/migration/features/money-market.md +0 -214
  48. package/ai-exported/migration/features/staking.md +0 -138
  49. package/ai-exported/migration/features/swap.md +0 -198
  50. package/ai-exported/migration/recipes.md +0 -350
  51. package/ai-exported/migration/reference/README.md +0 -18
  52. package/ai-exported/migration/reference/deleted-exports.md +0 -127
  53. package/ai-exported/migration/reference/error-code-crosswalk.md +0 -104
  54. package/ai-exported/migration/reference/return-shapes.md +0 -49
  55. package/ai-exported/migration/reference/sodax-config.md +0 -145
  56. package/dist/index.cjs.map +0 -1
  57. package/dist/index.mjs.map +0 -1
@@ -1,533 +0,0 @@
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` has exactly **10 fields** (all required at the type level, but `DeepPartial` makes every leaf optional):
136
-
137
- - `fee: PartnerFee | undefined` — global partner fee, applied unless a feature-level config overrides.
138
- - `chains: Record<SpokeChainKey, SpokeChainConfig>` — per-spoke-chain config. Each entry carries `rpcUrl`, polling config, and chain-family-specific extras (`BitcoinSpokeChainConfig`, `StellarSpokeChainConfig`, etc.).
139
- - `swaps: SwapsConfig` — supported solver tokens per chain.
140
- - `moneyMarket: MoneyMarketConfig` — money market contracts + supported tokens.
141
- - `bridge: BridgeConfig` — bridge `{ partnerFee }` override.
142
- - `dex: DexConfig` — DEX pool/asset config.
143
- - `hub: HubConfig` — hub-chain (Sonic) full address map + RPC URL + polling config.
144
- - `api: ApiConfig` — backend API endpoint (`{ baseURL, timeout, headers }`).
145
- - `solver: SolverConfig` — `{ intentsContract, solverApiEndpoint, protocolIntentsContract }`.
146
- - `relay: RelayConfig` — intent relay endpoint + chain-id map.
147
-
148
- > **Not config slots** — `staking`, `migration`, `partner`/`partners`, `recovery` are services on the `Sodax` instance (`sodax.staking`, etc.) but they are **not** configurable via `SodaxConfig`. They run on packaged defaults; per-call params handle customization.
149
-
150
- In production, the packaged defaults are sufficient — pass nothing and call `await sodax.config.initialize()` to load fresh data from the backend.
151
-
152
- ---
153
-
154
- ## 4. `ConfigService`
155
-
156
- 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.
157
-
158
- ### Lifecycle
159
-
160
- ```ts
161
- const sodax = new Sodax();
162
- await sodax.config.initialize(); // network call + cache; fall back on failure
163
-
164
- // After init:
165
- sodax.config.isValidSpokeChainKey(chainKey);
166
- sodax.config.findSupportedTokenBySymbol(chainKey, 'USDC');
167
- sodax.config.getSupportedTokensPerChain();
168
- sodax.config.getOriginalAssetAddress(chainKey, hubAsset); // (chainId, hubAsset) → original spoke-side address
169
- sodax.config.getMoneyMarketReserveAssets();
170
- sodax.config.getMoneyMarketToken(chainKey, tokenAddress); // resolve a hub-asset address → XToken
171
- sodax.config.getSupportedSwapTokensByChainId(chainKey); // solver-supported tokens for one chain
172
- sodax.config.getSpokeChainKeyFromIntentRelayChainId(BigInt(...));
173
- ```
174
-
175
- 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()`.
176
-
177
- ### Why dynamic
178
-
179
- 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.
180
-
181
- ### Custom backend
182
-
183
- Point at a custom backend URL via `SodaxConfig.api.baseURL`:
184
-
185
- ```ts
186
- const sodax = new Sodax({
187
- api: { baseURL: 'https://sandbox-api.example.com' },
188
- });
189
- ```
190
-
191
- `SodaxConfig.api` is `ApiConfig` (`{ baseURL, timeout, headers }`) — pass any subset via `DeepPartial`. v2 does not provide a typed slot to inject a custom `IConfigApi` implementation at construction; if you need to mock the backend for tests, point `baseURL` at a local mock server, or construct your own `BackendApiService`-compatible mock and inject it where you control the `Sodax` instance (e.g. dependency-injected in your app layer).
192
-
193
- ---
194
-
195
- ## 5. `ChainKeys` and chain-key narrowing
196
-
197
- `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).
198
-
199
- ```ts
200
- import { ChainKeys, type ChainKey, type SpokeChainKey } from '@sodax/sdk';
201
-
202
- ChainKeys.SONIC_MAINNET // 'sonic'
203
- ChainKeys.ETHEREUM_MAINNET // 'ethereum'
204
- ChainKeys.ARBITRUM_MAINNET // '0xa4b1.arbitrum'
205
- ChainKeys.ICON_MAINNET // '0x1.icon'
206
- ChainKeys.BITCOIN_MAINNET // 'bitcoin'
207
- // …
208
- ```
209
-
210
- The full table with values + chain family + relay id is in [`reference/`](reference/) § "Chain keys".
211
-
212
- ### Narrowing
213
-
214
- When a literal `srcChainKey` flows into a generic method, TypeScript preserves it as a value type. From that one literal:
215
-
216
- ```ts
217
- type K = typeof ChainKeys.ETHEREUM_MAINNET; // '0xa4b1...' (the literal)
218
-
219
- GetChainType<K> // 'EVM'
220
- GetWalletProviderType<K> // IEvmWalletProvider
221
- TxReturnType<K, false> // Hash (the EVM signed-tx return)
222
- TxReturnType<K, true> // EvmRawTransaction
223
- ```
224
-
225
- 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.
226
-
227
- ### Runtime helpers
228
-
229
- ```ts
230
- import { getChainType, isEvmChainKeyType, isSolanaChainKeyType, isBitcoinChainKeyType, /* … */ } from '@sodax/sdk';
231
-
232
- getChainType(chainKey); // 'EVM' | 'BITCOIN' | 'SOLANA' | 'STELLAR' | 'SUI' | 'ICON' | 'INJECTIVE' | 'STACKS' | 'NEAR' | 'SONIC'
233
- isEvmChainKeyType(chainKey); // boolean (with type guard)
234
- ```
235
-
236
- 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).
237
-
238
- ---
239
-
240
- ## 6. `WalletProviderSlot<K, Raw>`
241
-
242
- The discriminated union that distinguishes signed-execution from raw-tx-building at compile time.
243
-
244
- ```ts
245
- type WalletProviderSlot<K extends ChainKey, Raw extends boolean> =
246
- Raw extends true
247
- ? { raw: true; walletProvider?: never }
248
- : { raw: false; walletProvider: GetWalletProviderType<K> };
249
- ```
250
-
251
- ### Three rules
252
-
253
- 1. **`raw: true`** — `walletProvider` is **forbidden** (`?: never` rejects any value). The method returns a raw, unsigned tx payload (`TxReturnType<K, true>` — `EvmRawTransaction`, `SolanaRawTransaction`, etc.).
254
- 2. **`raw: false`** — `walletProvider` is **required** and chain-narrowed via `GetWalletProviderType<K>`. The method signs and broadcasts; returns a tx hash (`TxReturnType<K, false>`).
255
- 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 ...`.
256
-
257
- ### Usage in service methods
258
-
259
- 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).
260
-
261
- ```ts
262
- // Signed:
263
- sodax.swaps.createIntent({ params, raw: false, walletProvider });
264
-
265
- // Raw:
266
- sodax.swaps.createIntent({ params, raw: true });
267
-
268
- // Compile errors:
269
- sodax.swaps.createIntent({ params, walletProvider }); // missing 'raw'
270
- sodax.swaps.createIntent({ params, raw: true, walletProvider }); // walletProvider forbidden when raw: true
271
- sodax.swaps.createIntent({ params, raw: false }); // walletProvider required when raw: false
272
- ```
273
-
274
- ### When to pick which
275
-
276
- - **`raw: false`** — your app holds the wallet (Node script with private key, browser dApp with extension). Default for most flows.
277
- - **`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.
278
-
279
- ### Read-only methods
280
-
281
- 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:
282
-
283
- ```ts
284
- const result = await sodax.dex.assetService.isAllowanceValid({ params, raw: true });
285
- ```
286
-
287
- ---
288
-
289
- ## 7. `Result<T, SodaxError<C>>`
290
-
291
- Every async public method returns this. There is no `throw` across a service boundary in v2.
292
-
293
- ### Shape
294
-
295
- ```ts
296
- type Result<T, E = Error | unknown> =
297
- | { ok: true; value: T }
298
- | { ok: false; error: E };
299
- ```
300
-
301
- Defined in `@sodax/types`, re-exported from `@sodax/sdk`.
302
-
303
- ### Branching
304
-
305
- ```ts
306
- const result = await sodax.swaps.createIntent({ params, raw: false, walletProvider });
307
- if (!result.ok) {
308
- // result.error: SodaxError<C> for the narrow code union of createIntent
309
- return;
310
- }
311
- const { tx, intent, relayData } = result.value;
312
- ```
313
-
314
- ### Sub-Result propagation
315
-
316
- Inside SDK code (and useful for consumer wrappers):
317
-
318
- ```ts
319
- async function myWorkflow(): Promise<Result<MyOutput, SodaxError<MyCodes>>> {
320
- const sub = await this.subOperation();
321
- if (!sub.ok) return sub; // forward as-is; narrower code unions are structurally assignable
322
-
323
- // success path
324
- return { ok: true, value: /* … */ };
325
- }
326
- ```
327
-
328
- 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.
329
-
330
- ### No helpers like `toResult` / `tryCatch`
331
-
332
- 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.
333
-
334
- ### Pitfall
335
-
336
- 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`.
337
-
338
- ---
339
-
340
- ## 8. `SodaxError<C>` and the 13-code vocabulary
341
-
342
- The canonical error class. Every SDK-emitted error is a `SodaxError<C>` parameterised by a code from a closed 13-element union.
343
-
344
- ### Shape
345
-
346
- ```ts
347
- class SodaxError<C extends SodaxErrorCode = SodaxErrorCode> extends Error {
348
- readonly code: C; // closed 13-code reason union
349
- readonly feature: SodaxFeature; // 'swap' | 'moneyMarket' | 'bridge' | 'staking' | 'migration' | 'dex' | 'partner' | 'recovery'
350
- readonly cause?: unknown;
351
- readonly context?: SodaxErrorContext;
352
-
353
- toJSON(): SodaxErrorJSON<C>; // canonical logger surface
354
- }
355
- ```
356
-
357
- ### The 13 codes
358
-
359
- | Code | Meaning |
360
- |---|---|
361
- | `VALIDATION_FAILED` | Pre-flight invariant tripped. |
362
- | `INTENT_CREATION_FAILED` | Building the intent / payload failed. |
363
- | `EXECUTION_FAILED` | Orchestrator-level catch-all for multi-step ops. |
364
- | `TX_VERIFICATION_FAILED` | Spoke-side `verifyTxHash` returned false / threw. |
365
- | `TX_SUBMIT_FAILED` | Spoke tx landed; relay POST submit failed. |
366
- | `RELAY_TIMEOUT` | Destination packet didn't reach `executed` within timeout. |
367
- | `RELAY_FAILED` | Relay polling outage / unrecognised relay error. |
368
- | `APPROVE_FAILED` | Token approval call failed. |
369
- | `ALLOWANCE_CHECK_FAILED` | Reading on-chain allowance failed. |
370
- | `GAS_ESTIMATION_FAILED` | Gas estimation returned an error. |
371
- | `LOOKUP_FAILED` | Read-only on-chain query / off-chain config fetch. |
372
- | `EXTERNAL_API_ERROR` | Upstream API call failed (solver, backend). |
373
- | `UNKNOWN` | Last-resort catch in an outer `try`. Should be rare. |
374
-
375
- The full per-code semantics, common context fields, per-feature narrow unions, and retry guidance are in [`reference/`](reference/) § "Error codes".
376
-
377
- ### `(feature, code)` discrimination
378
-
379
- The pair `(error.feature, error.code)` is the canonical discriminator. Use it for both logging tags and switch statements:
380
-
381
- ```ts
382
- import { isSodaxError } from '@sodax/sdk';
383
-
384
- if (!result.ok && isSodaxError(result.error)) {
385
- if (result.error.feature === 'moneyMarket' && result.error.code === 'INTENT_CREATION_FAILED') {
386
- /* show "couldn't build supply" */
387
- }
388
- if (result.error.code === 'RELAY_TIMEOUT') {
389
- /* retry */
390
- }
391
- }
392
- ```
393
-
394
- ### Per-method narrow unions
395
-
396
- Public methods declare narrow code unions via `Extract<SodaxErrorCode, ...>`:
397
-
398
- ```ts
399
- type CreateSupplyIntentErrorCode = Extract<
400
- SodaxErrorCode,
401
- 'VALIDATION_FAILED' | 'INTENT_CREATION_FAILED' | 'UNKNOWN'
402
- >;
403
- ```
404
-
405
- 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".
406
-
407
- ### Context fields
408
-
409
- The `error.context` field carries per-error metadata. Reserved keys:
410
-
411
- | Key | Type | Used by |
412
- |---|---|---|
413
- | `action` | string | Discriminates user-facing operation (e.g. `'supply'`, `'stake'`, `'migrateBaln'`). |
414
- | `phase` | `SodaxPhase` | Orchestration phase (`'validate'`, `'intentCreation'`, `'verify'`, `'submit'`, `'relay'`, `'destinationExecution'`, `'execution'`, `'postExecution'`, `'approve'`, `'allowanceCheck'`, `'gasEstimation'`, `'lookup'`). |
415
- | `srcChainKey`, `dstChainKey` | `ChainKey` strings | Chain-related errors. |
416
- | `relayCode` | `'SUBMIT_TX_FAILED' \| 'RELAY_TIMEOUT' \| 'RELAY_POLLING_FAILED' \| 'UNKNOWN'` | Relay-layer errors (mirror of the lower-level relay code). |
417
- | `api` | `'solver' \| 'backend'` | `EXTERNAL_API_ERROR` only. |
418
- | `method` | string | `LOOKUP_FAILED` only. Names the failing read method. |
419
- | `direction` | `'forward' \| 'reverse'` | Migration's `migratebnUSD` only. |
420
- | `field`, `reason` | string | `VALIDATION_FAILED`. Names the precondition that tripped. |
421
- | `[key: string]` | unknown | Open at the index signature for feature-specific metadata. |
422
-
423
- ### `toJSON()` and logger integration
424
-
425
- `JSON.stringify(error)` calls `toJSON()` automatically. The serializer:
426
-
427
- - Coerces `bigint` to string anywhere in `context`.
428
- - Walks `cause` chains up to depth 3.
429
- - Stringifies `Date`, `Map`, `Set`, `Error`, and class instances safely.
430
- - Bounds depth at 5 to prevent cycles.
431
-
432
- Consumer-side:
433
-
434
- ```ts
435
- // Sentry
436
- Sentry.captureException(err, {
437
- tags: { feature: err.feature, code: err.code, action: err.context?.action },
438
- });
439
-
440
- // Pino
441
- logger.error({ err }, 'sodax operation failed');
442
- ```
443
-
444
- ### `isSodaxError` (preferred over `instanceof`)
445
-
446
- ```ts
447
- import { isSodaxError, isFeatureError } from '@sodax/sdk';
448
-
449
- if (isSodaxError(e)) {
450
- // e: SodaxError<SodaxErrorCode>
451
- }
452
-
453
- const isSwapError = isFeatureError('swap');
454
- if (isSwapError(e)) {
455
- // e: SodaxError with feature: 'swap'
456
- }
457
- ```
458
-
459
- 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.
460
-
461
- ---
462
-
463
- ## 9. Relay layer: `relayTxAndWaitPacket` and `mapRelayFailure`
464
-
465
- Cross-chain coordination is exposed as two top-level functions (re-exported from `@sodax/sdk`'s barrel):
466
-
467
- - `submitTransaction({ relayerApiEndpoint, srcChainKey, txHash, payload })` — POSTs the spoke transaction to the relay submit endpoint and resolves the relay's first-stage acknowledgement.
468
- - `relayTxAndWaitPacket({ relayerApiEndpoint, srcChainKey, dstChainKey, txHash, payload, timeout? })` — runs `submitTransaction` and then polls until the destination packet reaches `executed`.
469
-
470
- 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.
471
-
472
- ### Relay-layer error contract
473
-
474
- The relay layer keeps a stable string vocabulary of its own (separate from the 13 `SodaxErrorCode`s):
475
-
476
- ```ts
477
- type RelayCode =
478
- | 'SUBMIT_TX_FAILED' // POST to relay submit endpoint failed
479
- | 'RELAY_TIMEOUT' // Poll loop exhausted timeout
480
- | 'RELAY_POLLING_FAILED' // Relay endpoint outage / unrecognised response
481
- | 'UNKNOWN'; // Anything else
482
- ```
483
-
484
- These codes appear on `error.context.relayCode` of the `SodaxError` that surfaces to consumers.
485
-
486
- ### `mapRelayFailure`
487
-
488
- The single shared mapper from a relay-layer error to a `SodaxError`. Every feature service uses it internally — exported for custom orchestration:
489
-
490
- ```ts
491
- import { mapRelayFailure, relayTxAndWaitPacket } from '@sodax/sdk';
492
-
493
- try {
494
- await relayTxAndWaitPacket({ /* relayerApiEndpoint, srcChainKey, dstChainKey, txHash, payload, timeout? */ });
495
- } catch (e) {
496
- const sodaxError = mapRelayFailure(e, {
497
- feature: 'swap',
498
- action: 'createIntent',
499
- srcChainKey,
500
- dstChainKey,
501
- // phase: 'destinationExecution', // optional override; used by migration's bnUSD secondary watcher
502
- });
503
- return { ok: false, error: sodaxError };
504
- }
505
- ```
506
-
507
- Maps to one of: `'TX_SUBMIT_FAILED'`, `'RELAY_TIMEOUT'`, `'RELAY_FAILED'`, or `'EXECUTION_FAILED'`.
508
-
509
- ### When to use the relay layer directly
510
-
511
- 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>`.
512
-
513
- You drop down to the relay layer only when:
514
-
515
- - You're building custom orchestration not represented by a feature service.
516
- - You're testing relay behavior (E2E test that intentionally drops the destination tx).
517
- - You're writing a custom feature on top of the SDK primitives.
518
-
519
- ### Cross-references
520
-
521
- - `RecoveryService` for pulling stuck hub-wallet assets back to a spoke chain: see [`features/auxiliary-services.md`](features/auxiliary-services.md).
522
- - Per-feature error codes related to relay (e.g. `'TX_SUBMIT_FAILED'`, `'RELAY_TIMEOUT'`): [`reference/`](reference/) § "Error codes".
523
-
524
- ---
525
-
526
- ## Cross-references
527
-
528
- - Quickstart (install + initialize): [`quickstart.md`](quickstart.md).
529
- - Lookup tables (chain keys, error codes, public API surface): [`reference/`](reference/).
530
- - Recipes (init, result handling, raw vs signed, narrowing, testing): [`recipes/`](recipes/).
531
- - Per-feature usage: [`features/`](features/).
532
- - Non-EVM chain quirks: [`chain-specifics.md`](chain-specifics.md).
533
- - v1 → v2 porting context: [`../migration/README.md`](../migration/README.md).