# Migration

> **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](#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.

```typescript
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](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/constants/index.ts) 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.

```typescript
import { EvmWalletProvider, IconWalletProvider } from '@sodax/wallet-sdk-core';

// ICON wallet provider (for ICX / BALN / legacy bnUSD migrations originating on ICON)
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

// Sonic wallet provider (for reverse ICX migration originating on Sonic)
const sonicWalletProvider = new EvmWalletProvider({ privateKey: '...', rpcUrl: '...' });
```

## 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
// Signed execution (raw: false, walletProvider required)
const result = await sodax.migration.migrateIcxToSoda({
  params: { srcChainKey: ChainKeys.ICON_MAINNET, /* ... */ },
  walletProvider: iconWalletProvider,
  timeout: 30000,       // optional, ms; default 120000
  skipSimulation: false, // optional
});

// Raw transaction (raw: true, walletProvider forbidden)
const rawResult = await sodax.migration.createMigrateIcxToSodaIntent({
  params: { srcChainKey: ChainKeys.ICON_MAINNET, /* ... */ },
  raw: true,
});
```

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.

```typescript
const sodax = new Sodax();

// For forward ICX/BALN migration (no allowance required — returns true immediately for ICON source)
const migrationParams: IcxMigrateParams = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...',
  address: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // wICX address
  amount: BigInt(1000000000000000000), // 1 ICX (18 decimals)
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on hub chain
};

const isAllowed = await sodax.migration.isAllowanceValid(migrationParams, 'migrate');

if (!isAllowed.ok) {
  console.error('Failed to check allowance:', isAllowed.error);
} else {
  console.log('Allowance is valid:', isAllowed.value);
}

// For reverse ICX migration (SODA tokens require allowance check — source chain is Sonic)
const revertParams: IcxCreateRevertMigrationParams = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...', // Sonic address
  amount: BigInt(1000000000000000000), // 1 SODA token (18 decimals)
  dstAddress: 'hx1234567890123456789012345678901234567890', // ICON address to receive wICX
};

const isAllowedRevert = await sodax.migration.isAllowanceValid(revertParams, 'revert');

if (!isAllowedRevert.ok) {
  console.error('Failed to check allowance:', isAllowedRevert.error);
} else if (!isAllowedRevert.value) {
  console.log('Approval needed for SODA tokens');
} else {
  console.log('Allowance is valid');
}
```

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

```typescript
const sodax = new Sodax();

const revertParams: IcxCreateRevertMigrationParams = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...',
  amount: BigInt(1000000000000000000),
  dstAddress: 'hx1234567890123456789012345678901234567890',
};

// Approve SODA tokens for reverse migration
const approveResult = await sodax.migration.approve(
  {
    params: revertParams,
    walletProvider: sonicWalletProvider,
  },
  'revert',
);

if (approveResult.ok) {
  console.log('Approval transaction hash:', approveResult.value);
  // Wait for approval transaction to be mined
  const approveTxResult = await sonicWalletProvider.waitForTransactionReceipt(approveResult.value);
  console.log('Approval transaction confirmed:', approveTxResult);
} else {
  console.error('Failed to approve tokens:', approveResult.error);
}
```

### 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](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STELLAR_TRUSTLINE.md#migration) 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.

```typescript
const sodax = new Sodax();

const migrationParams: IcxMigrateParams = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...', // ICON wallet address
  address: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // wICX address
  amount: BigInt(1000000000000000000), // 1 ICX (18 decimals)
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on hub chain
};

// Migrate ICX to SODA
const result = await sodax.migration.migrateIcxToSoda({
  params: migrationParams,
  walletProvider: iconWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('ICX migration successful!');
  console.log('Spoke transaction hash:', srcChainTxHash);
  console.log('Hub transaction hash:', dstChainTxHash);
} else {
  console.error('ICX migration failed:', result.error);
}
```

## 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).

```typescript
const sodax = new Sodax();

const revertParams: IcxCreateRevertMigrationParams = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...', // Sonic wallet address
  amount: BigInt(1000000000000000000), // 1 SODA token (18 decimals)
  dstAddress: 'hx1234567890123456789012345678901234567890', // ICON address to receive wICX
};

// Revert SODA to ICX
const result = await sodax.migration.revertMigrateSodaToIcx({
  params: revertParams,
  walletProvider: sonicWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('SODA to ICX revert successful!');
  console.log('Sonic transaction hash:', srcChainTxHash);
  console.log('Hub packet receipt:', dstChainTxHash);
} else {
  console.error('SODA to ICX revert failed:', result.error);
}
```

## 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:

```typescript
import {
  bnUSDLegacySpokeChainIds,
  newbnUSDSpokeChainIds,
  bnUSDLegacyTokens,
  isLegacybnUSDChainId,
  isNewbnUSDChainId,
  isLegacybnUSDToken,
  isNewbnUSDToken,
  getAllLegacybnUSDTokens,
} from '@sodax/sdk';

// Get all chains that support legacy bnUSD
console.log('Legacy bnUSD chains:', bnUSDLegacySpokeChainIds);
// Output: ['0x1.icon', 'sui', 'stellar']

// Get all chains that support new bnUSD
console.log('New bnUSD chains:', newbnUSDSpokeChainIds);
// Output: ['sonic', 'arbitrum', 'base', 'polygon', ...] (all chains except Icon)

// Check if a chain supports legacy bnUSD
const isLegacyChain = isLegacybnUSDChainId(ChainKeys.ICON_MAINNET); // true
const isNewChain = isNewbnUSDChainId(ChainKeys.SONIC_MAINNET); // true

// Check if a token address is legacy bnUSD
const isLegacyToken = isLegacybnUSDToken('cx88fd7df7ddff82f7cc735c871dc519838cb235bb'); // true
const isNewToken = isNewbnUSDToken('0xE801CA34E19aBCbFeA12025378D19c4FBE250131'); // true
```

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

```typescript
const sodax = new Sodax();

const migrationParams: UnifiedBnUSDMigrateParams<typeof ChainKeys.ICON_MAINNET> = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...', // ICON wallet address
  dstChainKey: ChainKeys.SONIC_MAINNET,
  srcbnUSD: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // Legacy bnUSD address on ICON
  dstbnUSD: '0xE801CA34E19aBCbFeA12025378D19c4FBE250131', // New bnUSD address on Sonic
  amount: BigInt(1000000000000000000), // 1 bnUSD (18 decimals)
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on Sonic
};

// Migrate legacy bnUSD to new bnUSD
const result = await sodax.migration.migratebnUSD({
  params: migrationParams,
  walletProvider: iconWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('bnUSD migration successful!');
  console.log('Spoke transaction hash:', srcChainTxHash);
  console.log('Hub transaction hash:', dstChainTxHash);
} else {
  console.error('bnUSD migration failed:', result.error);
}
```

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

```typescript
const sodax = new Sodax();

const revertParams: UnifiedBnUSDMigrateParams<typeof ChainKeys.SONIC_MAINNET> = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...', // Sonic wallet address
  dstChainKey: ChainKeys.ICON_MAINNET,
  srcbnUSD: '0xE801CA34E19aBCbFeA12025378D19c4FBE250131', // New bnUSD address on Sonic
  dstbnUSD: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // Legacy bnUSD address on ICON
  amount: BigInt(1000000000000000000), // 1 new bnUSD (18 decimals)
  dstAddress: 'hx1234567890123456789012345678901234567890', // Recipient address on ICON
};

// Check allowance for reverse migration
const isAllowed = await sodax.migration.isAllowanceValid(revertParams, 'revert');

if (!isAllowed.ok) {
  console.error('Failed to check allowance:', isAllowed.error);
} else if (!isAllowed.value) {
  // Approve if needed
  const approveResult = await sodax.migration.approve(
    {
      params: revertParams,
      walletProvider: sonicWalletProvider,
    },
    'revert',
  );

  if (approveResult.ok) {
    console.log('Approval transaction hash:', approveResult.value);
    // Wait for approval transaction to be mined
    await sonicWalletProvider.waitForTransactionReceipt(approveResult.value);
  } else {
    console.error('Failed to approve tokens:', approveResult.error);
    return;
  }
}

// Reverse migrate new bnUSD to legacy bnUSD
const result = await sodax.migration.migratebnUSD({
  params: revertParams,
  walletProvider: sonicWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('bnUSD reverse migration successful!');
  console.log('Sonic transaction hash:', srcChainTxHash);
  console.log('Hub packet receipt:', dstChainTxHash);
} else {
  console.error('bnUSD reverse migration failed:', result.error);
}
```

## 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×).

```typescript
import { LockupPeriod } from '@sodax/sdk';

const sodax = new Sodax();

const migrationParams: BalnMigrateParams = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...', // ICON wallet address
  amount: BigInt(1000000000000000000), // 1 BALN (18 decimals)
  lockupPeriod: LockupPeriod.SIX_MONTHS, // 0.75x multiplier; use LockupPeriod enum
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on hub chain
  stake: true, // Whether to immediately stake the received SODA tokens
};

// Migrate BALN to SODA
const result = await sodax.migration.migrateBaln({
  params: migrationParams,
  walletProvider: iconWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('BALN migration successful!');
  console.log('Spoke transaction hash:', srcChainTxHash);
  console.log('Hub transaction hash:', dstChainTxHash);
} else {
  console.error('BALN migration failed:', result.error);
}
```

### 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:

```typescript
const { balnSwapService } = sodax.migration;

const lockId = 1n;

// Claim unlocked SODA tokens from a completed lock
await balnSwapService.claim(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Initiate unstaking of xSoda tokens from a lock
await balnSwapService.unstake(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Claim tokens after the unstaking waiting period expires
await balnSwapService.claimUnstaked(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Cancel a pending unstake request
await balnSwapService.cancelUnstake(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Stake SODA tokens held in a lock into the xSoda vault
await balnSwapService.stake(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Read all locks for a user
const locks = await balnSwapService.getDetailedUserLocks(publicClient, userAddress);
```

## Complete Examples

### ICX Migration Example

```typescript
import { Sodax, ChainKeys, type IcxMigrateParams } from '@sodax/sdk';
import { IconWalletProvider } from '@sodax/wallet-sdk-core';
import type { Address } from 'viem';

const sodax = new Sodax();
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function migrateIcx(amount: bigint, recipient: Address): Promise<void> {
  const params: IcxMigrateParams = {
    srcChainKey: ChainKeys.ICON_MAINNET,
    srcAddress: await iconWalletProvider.getWalletAddress(),
    address: '0x0000000000000000000000000000000000000000', // wICX or native ICX token address
    amount,
    dstAddress: recipient,
  };

  const result = await sodax.migration.migrateIcxToSoda({
    params,
    walletProvider: iconWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[migrateIcx] Migration successful!');
    console.log('[migrateIcx] Spoke transaction hash:', srcChainTxHash);
    console.log('[migrateIcx] Hub transaction hash:', dstChainTxHash);
  } else {
    console.error('[migrateIcx] Migration failed:', result.error);
  }
}

// Usage
await migrateIcx(BigInt(1000000000000000000), '0x1234567890123456789012345678901234567890');
```

### Reverse ICX Migration Example

```typescript
import { Sodax, ChainKeys, type IcxCreateRevertMigrationParams } from '@sodax/sdk';
import { EvmWalletProvider } from '@sodax/wallet-sdk-core';
import type { IconEoaAddress } from '@sodax/sdk';

const sodax = new Sodax();
const sonicWalletProvider = new EvmWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function reverseMigrateIcx(amount: bigint, to: IconEoaAddress): Promise<void> {
  const srcAddress = await sonicWalletProvider.getWalletAddress();

  const params: IcxCreateRevertMigrationParams = {
    srcChainKey: ChainKeys.SONIC_MAINNET,
    srcAddress,
    amount,
    dstAddress: to,
  };

  // Check allowance
  const isAllowed = await sodax.migration.isAllowanceValid(params, 'revert');

  if (!isAllowed.ok) {
    console.error('[reverseMigrateIcx] Allowance check failed:', isAllowed.error);
    return;
  }

  if (!isAllowed.value) {
    // Approve if needed
    const approveResult = await sodax.migration.approve(
      { params, walletProvider: sonicWalletProvider },
      'revert',
    );
    if (approveResult.ok) {
      console.log('[reverseMigrateIcx] Approval hash:', approveResult.value);
      await sonicWalletProvider.waitForTransactionReceipt(approveResult.value);
    } else {
      console.error('[reverseMigrateIcx] Approval failed:', approveResult.error);
      return;
    }
  }

  // Create and submit revert migration intent
  const result = await sodax.migration.revertMigrateSodaToIcx({
    params,
    walletProvider: sonicWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[reverseMigrateIcx] Revert migration successful!');
    console.log('[reverseMigrateIcx] Sonic transaction hash:', srcChainTxHash);
    console.log('[reverseMigrateIcx] Hub packet receipt:', dstChainTxHash);
  } else {
    console.error('[reverseMigrateIcx] Revert migration failed:', result.error);
  }
}

// Usage
await reverseMigrateIcx(BigInt(1000000000000000000), 'hx1234567890123456789012345678901234567890');
```

### bnUSD Migration Example

```typescript
import { Sodax, ChainKeys, type UnifiedBnUSDMigrateParams, type SpokeChainKey } from '@sodax/sdk';
import { IconWalletProvider } from '@sodax/wallet-sdk-core';
import type { Address } from 'viem';

const sodax = new Sodax();
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function migrateBnUSD(
  amount: bigint,
  recipient: Address,
  legacybnUSD: string,
  newbnUSD: string,
  dstChainKey: SpokeChainKey,
): Promise<void> {
  const params: UnifiedBnUSDMigrateParams<typeof ChainKeys.ICON_MAINNET> = {
    srcChainKey: ChainKeys.ICON_MAINNET,
    srcAddress: await iconWalletProvider.getWalletAddress(),
    dstChainKey,
    srcbnUSD: legacybnUSD,
    dstbnUSD: newbnUSD,
    amount,
    dstAddress: recipient,
  };

  const result = await sodax.migration.migratebnUSD({
    params,
    walletProvider: iconWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[migrateBnUSD] Migration successful!');
    console.log('[migrateBnUSD] Spoke transaction hash:', srcChainTxHash);
    console.log('[migrateBnUSD] Hub transaction hash:', dstChainTxHash);
  } else {
    console.error('[migrateBnUSD] Migration failed:', result.error);
  }
}

// Usage — forward migration (legacy to new)
await migrateBnUSD(
  BigInt(1000000000000000000),
  '0x1234567890123456789012345678901234567890',
  'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // Legacy bnUSD on ICON
  '0xE801CA34E19aBCbFeA12025378D19c4FBE250131', // New bnUSD on Sonic
  ChainKeys.SONIC_MAINNET,
);
```

### BALN Migration Example

```typescript
import { Sodax, ChainKeys, LockupPeriod, type BalnMigrateParams } from '@sodax/sdk';
import { IconWalletProvider } from '@sodax/wallet-sdk-core';
import type { Address } from 'viem';

const sodax = new Sodax();
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function migrateBaln(amount: bigint, recipient: Address): Promise<void> {
  const params: BalnMigrateParams = {
    srcChainKey: ChainKeys.ICON_MAINNET,
    srcAddress: await iconWalletProvider.getWalletAddress(),
    amount,
    lockupPeriod: LockupPeriod.SIX_MONTHS, // 0.75x multiplier
    dstAddress: recipient,
    stake: true,
  };

  const result = await sodax.migration.migrateBaln({
    params,
    walletProvider: iconWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[migrateBaln] Migration successful!');
    console.log('[migrateBaln] Spoke transaction hash:', srcChainTxHash);
    console.log('[migrateBaln] Hub transaction hash:', dstChainTxHash);
  } else {
    console.error('[migrateBaln] Migration failed:', result.error);
  }
}

// Usage
await migrateBaln(BigInt(1000000000000000000), '0x1234567890123456789012345678901234567890');
```

## 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`.

```typescript
import { isMigrateOrchestrationError, type MigrateOrchestrationError } from '@sodax/sdk';

const result = await sodax.migration.migrateIcxToSoda({ /* params */ });
if (!result.ok) {
  // result.error is typed as `MigrateOrchestrationError = SodaxError<MigrateOrchestrationErrorCode>`
  switch (result.error.code) {
    case 'VALIDATION_FAILED':       // precondition tripped (see context.field)
    case 'INTENT_CREATION_FAILED':  // spoke-side intent creation failed
    case 'TX_VERIFICATION_FAILED':           // spoke tx not verifiable on-chain (only migratebnUSD calls verifyTxHash)
    case 'TX_SUBMIT_FAILED':        // relay submit failed
    case 'RELAY_TIMEOUT':           // relay packet did not arrive within timeout
    case 'RELAY_FAILED':            // relay polling failure / unknown relay error
    case 'EXECUTION_FAILED':                  // generic forward-orchestrator catch-all (see error.cause)
    case 'UNKNOWN':
      handleMigrationError(result.error);
      break;
  }
}
```

### 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:

```typescript
import { isMigrateOrchestrationError, isMigrationError } from '@sodax/sdk';

try {
  // ... call sodax.migration.migratebnUSD ...
} catch (e) {
  if (isMigrateOrchestrationError(e)) console.error('typed forward-migration error:', e.code, e.context);
  else if (isMigrationError(e)) console.error('migration error from another method:', e.code);
  else throw e; // not a migration error — bubble up
}
```

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.

```typescript
import { migrationInvariant } from '@sodax/sdk';

migrationInvariant(amount > 0n, 'Amount must be greater than 0', { field: 'amount' });
```

### 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:

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax({
  relay: {
    relayerApiEndpoint: 'https://custom-relay-api.example.com',
  },
});

await sodax.config.initialize(); // optional: fetch dynamic chain config from backend
```

Default configuration:

* `relayerApiEndpoint`: `https://relay.soniclabs.com`
* `timeout`: 120000 ms (120 seconds) — overridable per call via the `timeout` field in action params


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/migration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
