truckMigration

Error handling conventions: This module returns Result<T, SodaxError<NarrowCode>> from every async public method. Discriminate on error.code (a closed reason-only union) and error.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 ChainKeysarrow-up-right 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:

  1. ICX/wICX → SODA: Migrate ICX or wICX tokens from ICON to SODA tokens on the hub chain

  2. SODA → wICX: Revert SODA tokens from the hub chain back to wICX tokens on ICON

  3. bnUSD Legacy ↔ New bnUSD: Unified migration between legacy and new bnUSD tokens across supported chains

  4. 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 isAllowanceValid method 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.hasSufficientTrustline before 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 approve method 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.requestTrustline before 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 Requirementsarrow-up-right 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

Lock Period
Enum Value
Multiplier

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

Method
Codes

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

Every migration error carries an error.context payload. Fields vary by code:

Field
Set on
Notes

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.

v1 code
v2 code
Notes

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.com

  • timeout: 120000 ms (120 seconds) — overridable per call via the timeout field in action params

Last updated