Error handling conventions: This module returnsMigration part of the SDK provides abstractions to assist you with migrating tokens between ICON and the hub chain (Sonic). The service supports multiple migration types including ICX/wICX → SODA, bnUSD legacy → new bnUSD, BALN → SODA, and their reverse operations.Result<T, SodaxError<NarrowCode>>from every async public method. Discriminate onerror.code(a closed reason-only union) anderror.feature === 'migration'. See Error Handling below.
Using SDK Config and Constants
SDK includes predefined configurations of supported chains, tokens and other relevant information for the client to consume.*_CHAIN_ID constants to ChainKeys.* see packages/sdk/CHAIN_ID_MIGRATION.md.
Wallet Providers
All execution methods accept awalletProvider inside the action params object — no spoke provider classes need to be constructed by callers. The wallet provider type is chain-narrowed from the srcChainKey in the params.
Migration Types
The MigrationService supports multiple types of migrations:- ICX/wICX → SODA: Migrate ICX or wICX tokens from ICON to SODA tokens on the hub chain
- SODA → wICX: Revert SODA tokens from the hub chain back to wICX tokens on ICON
- bnUSD Legacy ↔ New bnUSD: Unified migration between legacy and new bnUSD tokens across supported chains
- BALN → SODA: Migrate BALN tokens to SODA tokens on the hub chain
Calling Convention
All exec methods onMigrationService follow the SpokeExecActionParams wrapper pattern. The wrapper carries the migration params, the wallet provider, and optional flags:
walletProvider when raw: true is a compile error; omitting it when raw: false is also a compile error.
Common Operations
Check Allowance
Before creating migration intents, you should check if the allowance is valid. For forward migrations (ICX/wICX, bnUSD from ICON, BALN), no allowance is required as these tokens do not require approval. Note: For Stellar-based operations, the allowance system works differently:- Source Chain (Stellar): The standard
isAllowanceValidmethod works as expected for EVM chains, but for Stellar as the source chain, this method checks and establishes trustlines instead. - Destination Chain (Stellar): When Stellar is specified as the destination chain, frontends/clients need to manually check trustlines using
StellarSpokeService.hasSufficientTrustlinebefore executing migration operations.
Approve Tokens
For reverse migrations, if the allowance check returns false, you need to approve the tokens before creating the revert migration intent. Some tokens take two transactions. A few ERC-20s of the 2017 TetherToken lineage — Ethereum USDT is the only one in the SODAX token list today — reject an allowance change from one non-zero value to another, soapprove sends approve(0) first and waits for it to be mined before the real approval.
The user signs twice; the returned value is still a single transaction hash, the last one’s.
Detection simulates the approval rather than consulting a token list, so a token listed later behaves
the same way.
Note: For Stellar-based operations, the approval system works differently:
- Source Chain (Stellar): The standard
approvemethod works as expected for EVM chains, but for Stellar as the source chain, this method establishes trustlines instead. - Destination Chain (Stellar): When Stellar is specified as the destination chain, frontends/clients need to manually establish trustlines using
StellarSpokeService.requestTrustlinebefore executing migration operations.
Stellar Trustline Requirements
For Stellar-based migration operations, you need to handle trustlines differently depending on whether Stellar is the source or destination chain. See Stellar Trustline Requirements for detailed information and code examples.ICX Migration (ICX/wICX → SODA)
Migrate ICX to SODA
Migrate ICX or wICX tokens to SODA tokens on the hub chain.Reverse ICX Migration (SODA → wICX)
Revert SODA to ICX
Revert SODA tokens back to wICX tokens on ICON. A SODA approval from the caller to their hub wallet must be set before calling this method (useisAllowanceValid to check and approve to set it).
bnUSD Migration (Legacy ↔ New bnUSD)
The bnUSD migration now uses a unified API that handles both forward (legacy → new) and reverse (new → legacy) migrations. The system automatically determines the migration direction based on the token addresses provided.bnUSD Constants and Helper Functions
The SDK provides several constants and helper functions to work with legacy and new bnUSD tokens across different chains:Migrate Legacy bnUSD to New bnUSD
Migrate legacy bnUSD tokens to new bnUSD tokens on any spoke chain (besides Icon — which has only legacy bnUSD). Note: When migrating to Stellar as the destination chain, ensure you have established the necessary trustlines usingStellarSpokeService.hasSufficientTrustline and StellarSpokeService.requestTrustline before executing the migration.
Reverse Migrate New bnUSD to Legacy bnUSD
Revert new bnUSD tokens back to legacy bnUSD tokens. Legacy bnUSD exists on Icon, Sui or Stellar chains. Note: When migrating to Stellar as the destination chain, ensure you have established the necessary trustlines usingStellarSpokeService.hasSufficientTrustline and StellarSpokeService.requestTrustline before executing the migration.
BALN Migration (BALN → SODA)
Migrate BALN to SODA
Migrate BALN tokens to SODA tokens on the hub chain. UseLockupPeriod enum values for the lockupPeriod field — longer lock-ups yield higher SODA multipliers (0.5×–1.5×).
BALN Lock Periods and Multipliers
BALN Lock Management
After migrating BALN, the resulting SODA (or xSoda) is held in locks managed bysodax.migration.balnSwapService. These methods act directly on the hub chain:
Complete Examples
ICX Migration Example
Reverse ICX Migration Example
bnUSD Migration Example
BALN Migration Example
Error Handling
All async public methods onMigrationService (and IcxMigrationService.getAvailableAmount) return Promise<Result<T, SodaxError<NarrowCode>>> where NarrowCode is a narrow per-method union of MigrationErrorCode. Discriminate on error.code, never on error.message. The original lower-level failure (a viem revert, a fetch error, a relay timeout) is preserved on error.cause; structured metadata (chain, action, direction, phase, relayCode) is on error.context.
Per-method error code unions
Note:
TX_VERIFICATION_FAILED only appears in the forward-orchestrator union because migratebnUSD is the only orchestrator that calls spoke.verifyTxHash. The other forward orchestrators technically can’t produce it, but the shared narrow union keeps callers working symmetrically across the three “migrate” methods.
Structured context
Every migration error carries an error.context payload. Fields vary by code:
Type guards
Per-method type guards are runtime-checked and compile-checked in lockstep with the union types. Use them incatch blocks to short-circuit when a foreign code escapes:
isMigrationError (broad — any migration error), isMigrateOrchestrationError (forward migrateIcxToSoda / migratebnUSD / migrateBaln), isRevertMigrationOrchestrationError (revertMigrateSodaToIcx), isMigrationCreateIntentError (shared by all 4 create*Intent methods), isMigrationApproveError, isMigrationAllowanceCheckError, isMigrationLookupError. Per-operation discrimination across the orchestrators is via error.context.action (one of 'migrateIcxToSoda' | 'migratebnUSD' | 'migrateBaln' | 'revertMigrateSodaToIcx').
Validation invariant
Precondition failures throw a typedVALIDATION_FAILED from inside the public method’s try/catch, surfacing as a typed Result.error rather than a generic prose Error. Consumers discriminate validation failures the same way as any other code.
Migration from the pre-v2 taxonomy
The published v1 4-code shape (EXECUTION_FAILED, CREATE_MIGRATION_INTENT_FAILED, REVERT_MIGRATION_FAILED, CREATE_REVERT_MIGRATION_INTENT_FAILED) is restored here with module-prefixed names and cause-preservation. Sub-modules (ICX, bnUSD, BALN) remain undifferentiated at the code level — fine-grained partitioning is delegated to context.action, faithful to v1 which also did not distinguish them.
Configuration
The MigrationService is wired internally by theSodax facade. Custom relay endpoints are passed via the Sodax constructor config:
relayerApiEndpoint:https://relay.soniclabs.comtimeout: 120000 ms (120 seconds) — overridable per call via thetimeoutfield in action params