Migration
Error handling conventions: This module returns
Result<T, SodaxError<NarrowCode>>from every async public method. Discriminate onerror.code(a closed reason-only union) anderror.feature === 'migration'. See Error Handling below.
Migration 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.
Using SDK Config and Constants
SDK includes predefined configurations of supported chains, tokens and other relevant information for the client to consume.
import {
ChainKeys,
type HubChainKey,
type SpokeChainKey,
} from "@sodax/sdk"
// Supported migration chains
const hubChainKey: HubChainKey = ChainKeys.SONIC_MAINNET;
const iconChainKey: SpokeChainKey = ChainKeys.ICON_MAINNET;
// Migration tokens
const migrationTokens = ['ICX', 'bnUSD', 'BALN'] as const;Please refer to SDK ChainKeys for more. For a direct mapping from old *_CHAIN_ID constants to ChainKeys.* see packages/sdk/CHAIN_ID_MIGRATION.md.
Wallet Providers
All execution methods accept a walletProvider 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 on MigrationService follow the SpokeExecActionParams wrapper pattern. The wrapper carries the migration params, the wallet provider, and optional flags:
TypeScript enforces the pairing: 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.
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 (use isAllowanceValid 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 using StellarSpokeService.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 using StellarSpokeService.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. Use LockupPeriod enum values for the lockupPeriod field — longer lock-ups yield higher SODA multipliers (0.5×–1.5×).
BALN Lock Periods and Multipliers
No lock
LockupPeriod.NO_LOCKUP
0.5×
6 months
LockupPeriod.SIX_MONTHS
0.75×
12 months
LockupPeriod.TWELVE_MONTHS
1.0×
18 months
LockupPeriod.EIGHTEEN_MONTHS
1.25×
24 months
LockupPeriod.TWENTY_FOUR_MONTHS
1.5×
BALN Lock Management
After migrating BALN, the resulting SODA (or xSoda) is held in locks managed by sodax.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 on MigrationService (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
migratebnUSD / migrateIcxToSoda / migrateBaln (forward orchestrators)
VALIDATION_FAILED, INTENT_CREATION_FAILED, TX_VERIFICATION_FAILED, TX_SUBMIT_FAILED, RELAY_TIMEOUT, RELAY_FAILED, EXECUTION_FAILED, UNKNOWN
revertMigrateSodaToIcx (reverse orchestrator)
VALIDATION_FAILED, INTENT_CREATION_FAILED, TX_SUBMIT_FAILED, RELAY_TIMEOUT, RELAY_FAILED, EXECUTION_FAILED, UNKNOWN
createMigratebnUSDIntent / createMigrateIcxToSodaIntent / createMigrateBalnIntent (forward intent creators)
VALIDATION_FAILED, INTENT_CREATION_FAILED, UNKNOWN
createRevertSodaToIcxMigrationIntent (reverse intent creator)
VALIDATION_FAILED, INTENT_CREATION_FAILED, UNKNOWN
approve
VALIDATION_FAILED, APPROVE_FAILED, UNKNOWN
isAllowanceValid
VALIDATION_FAILED, ALLOWANCE_CHECK_FAILED, UNKNOWN
IcxMigrationService.getAvailableAmount
VALIDATION_FAILED, LOOKUP_FAILED, UNKNOWN
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
contextEvery migration error carries an error.context payload. Fields vary by code:
srcChainKey
all orchestrator + intent + approve + allowance codes
low-cardinality — suitable as a logger / Sentry tag
dstChainKey
migratebnUSD orchestrator + intent codes
bnUSD-only (the other orchestrators have a fixed destination)
action
all orchestrator + intent codes
one of 'migratebnUSD' | 'migrateIcxToSoda' | 'revertMigrateSodaToIcx' | 'migrateBaln'
direction
only on migratebnUSD errors
'forward' (legacy → new) or 'reverse' (new → legacy). The error code stays EXECUTION_FAILED regardless — this is purely a forensics hint
phase
most codes
'validate' | 'intentCreation' | 'verify' | 'submit' | 'relay' | 'destinationExecution' | 'approve' | 'allowanceCheck' | 'lookup'. 'destinationExecution' is set on RELAY_TIMEOUT / RELAY_FAILED / TX_SUBMIT_FAILED errors that originate from migratebnUSD's secondary waitUntilIntentExecuted watcher (vs. 'relay' for the primary relayTxAndWaitPacket call)
relayCode
RELAY_TIMEOUT / TX_SUBMIT_FAILED / RELAY_FAILED
mirrors the relay-layer RELAY_ERROR_CODES contract; carries 'RELAY_POLLING_FAILED' so polling outage is distinguishable from generic failure
field / reason
VALIDATION_FAILED
which precondition tripped
Type guards
Per-method type guards are runtime-checked and compile-checked in lockstep with the union types. Use them in catch blocks to short-circuit when a foreign code escapes:
Available guards: 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 typed VALIDATION_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.
EXECUTION_FAILED
EXECUTION_FAILED
Forward-orchestrator catch-all (migratebnUSD/migrateIcxToSoda/migrateBaln). Use context.action to discriminate.
CREATE_MIGRATION_INTENT_FAILED
INTENT_CREATION_FAILED
Forward intent-creation phase.
REVERT_MIGRATION_FAILED
EXECUTION_FAILED
Reverse-orchestrator catch-all (revertMigrateSodaToIcx).
CREATE_REVERT_MIGRATION_INTENT_FAILED
INTENT_CREATION_FAILED
Reverse intent-creation phase.
(none)
VALIDATION_FAILED
New: typed precondition failures (replaces prose Error throws from invariant).
(none)
TX_VERIFICATION_FAILED
New: spoke tx verification phase tag (only set by migratebnUSD, the only orchestrator that calls verifyTxHash).
(none)
TX_SUBMIT_FAILED / RELAY_TIMEOUT / RELAY_FAILED
New: typed relay-phase codes mapped from the shared RELAY_ERROR_CODES contract.
(none)
APPROVE_FAILED / ALLOWANCE_CHECK_FAILED / LOOKUP_FAILED
New: typed phase codes for approve / isAllowanceValid / IcxMigrationService.getAvailableAmount.
(none)
UNKNOWN
Reserved fallback for never-classified errors.
Configuration
The MigrationService is wired internally by the Sodax facade. Custom relay endpoints are passed via the Sodax constructor config:
Default configuration:
relayerApiEndpoint:https://relay.soniclabs.comtimeout: 120000 ms (120 seconds) — overridable per call via thetimeoutfield in action params
Last updated