@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.
- package/README.md +91 -7
- package/ai-exported/AGENTS.md +99 -0
- package/ai-exported/integration/README.md +41 -0
- package/ai-exported/integration/ai-rules.md +75 -0
- package/ai-exported/integration/architecture.md +519 -0
- package/ai-exported/integration/chain-specifics.md +189 -0
- package/ai-exported/integration/features/README.md +19 -0
- package/ai-exported/integration/features/auxiliary-services.md +189 -0
- package/ai-exported/integration/features/bridge.md +136 -0
- package/ai-exported/integration/features/dex.md +182 -0
- package/ai-exported/integration/features/icx-bnusd-baln.md +181 -0
- package/ai-exported/integration/features/money-market.md +198 -0
- package/ai-exported/integration/features/staking.md +166 -0
- package/ai-exported/integration/features/swap.md +207 -0
- package/ai-exported/integration/quickstart.md +213 -0
- package/ai-exported/integration/recipes/README.md +21 -0
- package/ai-exported/integration/recipes/backend-server-init.md +69 -0
- package/ai-exported/integration/recipes/chain-key-narrowing.md +65 -0
- package/ai-exported/integration/recipes/gas-estimation.md +33 -0
- package/ai-exported/integration/recipes/initialize-sodax.md +53 -0
- package/ai-exported/integration/recipes/raw-tx-flow.md +71 -0
- package/ai-exported/integration/recipes/result-and-errors.md +104 -0
- package/ai-exported/integration/recipes/signed-tx-flow.md +46 -0
- package/ai-exported/integration/recipes/testing.md +101 -0
- package/ai-exported/integration/reference/README.md +18 -0
- package/ai-exported/integration/reference/chain-keys.md +67 -0
- package/ai-exported/integration/reference/error-codes.md +165 -0
- package/ai-exported/integration/reference/glossary.md +32 -0
- package/ai-exported/integration/reference/public-api.md +138 -0
- package/ai-exported/integration/reference/wallet-providers.md +62 -0
- package/ai-exported/migration/README.md +58 -0
- package/ai-exported/migration/ai-rules.md +80 -0
- package/ai-exported/migration/breaking-changes/architecture.md +335 -0
- package/ai-exported/migration/breaking-changes/result-and-errors.md +363 -0
- package/ai-exported/migration/breaking-changes/type-system.md +321 -0
- package/ai-exported/migration/checklist.md +61 -0
- package/ai-exported/migration/features/README.md +35 -0
- package/ai-exported/migration/features/auxiliary-services.md +156 -0
- package/ai-exported/migration/features/bridge.md +125 -0
- package/ai-exported/migration/features/dex.md +143 -0
- package/ai-exported/migration/features/icx-bnusd-baln.md +151 -0
- package/ai-exported/migration/features/money-market.md +214 -0
- package/ai-exported/migration/features/staking.md +138 -0
- package/ai-exported/migration/features/swap.md +198 -0
- package/ai-exported/migration/recipes.md +288 -0
- package/ai-exported/migration/reference/README.md +18 -0
- package/ai-exported/migration/reference/deleted-exports.md +100 -0
- package/ai-exported/migration/reference/error-code-crosswalk.md +104 -0
- package/ai-exported/migration/reference/return-shapes.md +49 -0
- package/ai-exported/migration/reference/sodax-config.md +52 -0
- package/dist/index.cjs +32154 -31601
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8442 -6974
- package/dist/index.d.ts +8442 -6974
- package/dist/index.mjs +32642 -32130
- package/dist/index.mjs.map +1 -1
- 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).
|