For the complete documentation index, see llms.txt. This page is also available as Markdown.

Lend / Borrow (Money Market)

Error handling conventions: This module uses the canonical SodaxError<MoneyMarketErrorCode> shape (same family as the swap module). Discriminate on result.error.code (e.g. 'RELAY_TIMEOUT', 'EXECUTION_FAILED'); structured details live on result.error.context (action, phase, relayCode, field). See the Error Handling section below for the full per-method code table and migration notes from the legacy error.message-based pattern.

Money Market part of SDK provides abstractions to assist you with interacting with the cross-chain Money Market Smart Contracts.

All money market operations are accessed through the moneyMarket property of a Sodax instance:

import { Sodax, ChainKeys } from '@sodax/sdk';

const sodax = new Sodax();

// All money market methods are available through sodax.moneyMarket
const supplyResult = await sodax.moneyMarket.supply({
  params: {
    srcChainKey: ChainKeys.BSC_MAINNET,
    srcAddress: '0x...',
    token: '0x...',
    amount: 1000n,
    action: 'supply',
  },
  walletProvider: evmWalletProvider,
});

Using SDK Config and Constants

SDK includes predefined configurations of supported chains, tokens and other relevant information for the client to consume. All configurations are accessible through the config property of the Sodax instance (sodax.config), or through service-specific properties for convenience.

IMPORTANT: If you want dynamic (backend API based - contains latest tokens) configuration, make sure to initialize the instance before usage:

By default, configuration from the specific SDK version you are using is used.

Chain constants are available under the ChainKeys namespace (e.g. ChainKeys.BSC_MAINNET, ChainKeys.SONIC_MAINNET). The old *_CHAIN_ID constants have been replaced — see packages/sdk/CHAIN_ID_MIGRATION.md for the full rename mapping.

Available Methods

All money market methods are accessible through sodax.moneyMarket:

Token & Reserve Configuration

  • getSupportedTokensByChainId(chainKey) - Get supported money market tokens for a specific chain

  • getSupportedTokens() - Get all supported money market tokens per chain

  • getSupportedReserves() - Get all supported money market reserves (hub chain addresses)

Allowance & Approval

  • isAllowanceValid({ params }) - Check if token approval/trustline is sufficient

  • approve({ params, walletProvider, raw? }) - Approve tokens or establish Stellar trustline

Money Market Operations

  • supply({ params, walletProvider, timeout? }) - Supply tokens (complete operation with relay)

  • createSupplyIntent({ params, walletProvider?, raw?, skipSimulation? }) - Create supply intent only

  • borrow({ params, walletProvider, timeout? }) - Borrow tokens (complete operation with relay)

  • createBorrowIntent({ params, walletProvider?, raw?, skipSimulation? }) - Create borrow intent only

  • withdraw({ params, walletProvider, timeout? }) - Withdraw tokens (complete operation with relay)

  • createWithdrawIntent({ params, walletProvider?, raw?, skipSimulation? }) - Create withdraw intent only

  • repay({ params, walletProvider, timeout? }) - Repay tokens (complete operation with relay)

  • createRepayIntent({ params, walletProvider?, raw?, skipSimulation? }) - Create repay intent only

Gas Estimation

  • estimateGas(params) - Estimate gas for an encoded transaction on a given spoke chain

Data Retrieval & Formatting

  • data.getReservesList() - Get list of all reserve addresses

  • data.getReservesData() - Get raw aggregated reserve data

  • data.getReservesHumanized() - Get humanized reserve data

  • data.getReserveData(asset) - Get specific reserve data

  • data.getReserveNormalizedIncome(asset) - Get normalized income for a specific asset (RAY precision)

  • data.getUserReservesData(spokeChainKey, userAddress) - Get raw user reserve data

  • data.getUserReservesHumanized(spokeChainKey, userAddress) - Get humanized user reserve data

  • data.getEModes() - Get raw E-Mode data

  • data.getEModesHumanized() - Get humanized E-Mode data

  • data.formatReservesUSD(request) - Format reserves with USD conversions

  • data.formatReserveUSD(request) - Format a single reserve with USD conversion

  • data.formatUserSummary(request) - Format user portfolio summary with USD conversions

Function Parameters Structure

All money market exec methods use a single SpokeExecActionParams-shaped object:

  • params: The money market operation parameters (MoneyMarketSupplyParams, MoneyMarketBorrowParams, MoneyMarketWithdrawParams, or MoneyMarketRepayParams). Every params type carries:

    • srcChainKey: K — the source spoke chain (drives TypeScript narrowing of walletProvider)

    • srcAddress: string — the caller's address on the source chain

    • token: string — token address on the source chain (or destination chain for borrow/withdraw)

    • amount: bigint — amount in token's native decimals

    • action: 'supply' | 'borrow' | 'withdraw' | 'repay'

    • dstChainKey?: SpokeChainKey — optional destination chain (defaults to srcChainKey)

    • dstAddress?: string — optional destination address (defaults to srcAddress)

  • walletProvider: The wallet provider for the source chain. Required when raw is false (or omitted); forbidden when raw: true. The type is automatically narrowed to the correct interface for the given srcChainKey (e.g. IEvmWalletProvider for EVM chains).

  • raw: (Optional, default false) When true, returns unsigned transaction data instead of executing. When true, walletProvider must not be passed. Used in create*Intent and approve methods.

  • skipSimulation: (Optional, default false) Skip transaction simulation before broadcast. Used in create*Intent methods.

  • timeout: (Optional, default: DEFAULT_RELAY_TX_TIMEOUT = 120 seconds) Timeout in milliseconds for relay operations. Used in supply, borrow, withdraw, and repay methods.

Allowance and Approval

Before making a money market action (supply, repay), you need to ensure the money market contract has sufficient allowance to spend your tokens. The SDK provides methods to check and set allowances for different types of spoke providers.

Note: For Stellar-based operations, the allowance and approval system works differently:

  • Source Chain (Stellar): The standard isAllowanceValid and approve methods check and establish trustlines automatically.

  • Destination Chain (Stellar): When Stellar is specified as the destination chain, the SDK checks both the sender's and recipient's trustlines via isAllowanceValid.

Withdraw and borrow: No on-chain approval is required for these actions. isAllowanceValid always returns true for them (though it validates the token is supported on the destination chain).

Checking Allowance

The isAllowanceValid method checks if the current allowance is sufficient for the specified action:

Setting Allowance

The approve method sets the allowance for the specified action. The spender address is resolved internally based on the chain:

  • EVM Spoke Chains: The spender is the spoke asset manager contract

  • Sonic (Hub) Chain: The spender is the user's hub router contract

  • Stellar: Creates/updates the required trustline

To obtain unsigned approval calldata without broadcasting:

Supported Actions by Provider Type

The allowance and approval system supports different actions depending on the spoke chain type:

EVM Spoke Providers:

  • supply - Approves the asset manager contract to spend tokens

  • repay - Approves the asset manager contract to spend tokens

Sonic Spoke Provider (Hub Chain):

  • supply - Approves the user hub router to spend tokens

  • repay - Approves the user hub router to spend tokens

Stellar:

  • supply / repay / withdraw / borrow — Checks and establishes trustlines

Borrow and withdraw on EVM/hub chains do not require approval.

Stellar Trustline Requirements

For Stellar-based money market 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.

Complete Example

Here's a complete example showing the allowance check and approval flow:

Estimate Gas for Raw Transactions

The estimateGas method estimates gas for an already-encoded transaction on a given spoke chain. Use this after obtaining a raw transaction from a create*Intent or approve call.

Supply Tokens

Supply tokens to the money market pool. There are two methods available:

  1. supply: Executes the spoke-side deposit, relays to the hub, and waits for the relay to settle.

  2. createSupplyIntent: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay. Useful when you need manual relay control.

Borrow Tokens

Borrow tokens from the money market pool. Borrowed tokens can be delivered to a different spoke chain by specifying dstChainKey and dstAddress.

  1. borrow: Executes the spoke-side message, relays to the hub, and waits for the relay to settle.

  2. createBorrowIntent: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay.

Withdraw Tokens

Withdraw previously supplied tokens from the money market pool. Withdrawn tokens can be delivered to a different spoke chain by specifying dstChainKey and dstAddress.

  1. withdraw: Executes the spoke-side message, relays to the hub, and waits for the relay to settle.

  2. createWithdrawIntent: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay.

Repay Tokens

Repay a borrowed position in the money market pool.

  1. repay: Executes the spoke-side deposit, relays to the hub, and waits for the relay to settle.

  2. createRepayIntent: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay.

Error Handling

The Money Market module's user-facing methods return Promise<Result<T, SodaxError<NarrowCode>>>. Discriminate on result.error.code (a string literal) — never on result.error.message. This is the same canonical shape used by the swap module.

The canonical error: SodaxError<C>

All MM-module errors are instances of SodaxError, exported from @sodax/sdk:

Rules:

  • Discriminate on error.code — never on error.message (which is human-readable, may change).

  • error.cause walks the underlying error chain (loggers like Sentry/Pino/Datadog walk this automatically).

  • error.context carries structured metadata: srcChainKey, dstChainKey, action, phase, plus per-code extras (relayCode, field, …).

  • error.toJSON() is the canonical logger surface; JSON.stringify(error) invokes it automatically and produces a logger-safe payload (bigints in context are coerced to strings, cause walked depth-3, no circular hazards).

  • Use isMoneyMarketError(e) (broad) or one of the narrow guards isMoneyMarketOrchestrationError(e) / isMoneyMarketCreateIntentError(e) / isMoneyMarketApproveError(e) / isMoneyMarketAllowanceCheckError(e) / isMoneyMarketGasEstimationError(e) from @sodax/sdk instead of instanceof SodaxError in dapp/app code (bundle-safe).

Per-method error type unions

The 4 orchestrators (supply/borrow/withdraw/repay) share one type — MoneyMarketOrchestrationError. They are not partitioned at the type level; instead, discriminate operations at runtime via error.context.action. Similarly, the 4 create*Intent methods share MoneyMarketCreateIntentError.

Method
Error type
Codes

supply / borrow / withdraw / repay

MoneyMarketOrchestrationError

VALIDATION_FAILED, INTENT_CREATION_FAILED, TX_VERIFICATION_FAILED, TX_SUBMIT_FAILED, RELAY_TIMEOUT, RELAY_FAILED, EXECUTION_FAILED, UNKNOWN

createSupplyIntent / createBorrowIntent / createWithdrawIntent / createRepayIntent

MoneyMarketCreateIntentError

VALIDATION_FAILED, INTENT_CREATION_FAILED, UNKNOWN

approve

MoneyMarketApproveError

VALIDATION_FAILED, APPROVE_FAILED, UNKNOWN

isAllowanceValid

MoneyMarketAllowanceCheckError

VALIDATION_FAILED, ALLOWANCE_CHECK_FAILED, UNKNOWN

estimateGas

MoneyMarketGasEstimationError

VALIDATION_FAILED, GAS_ESTIMATION_FAILED, UNKNOWN

Use error.context.action ('supply' | 'borrow' | 'withdraw' | 'repay') to discriminate which orchestrator surfaced the error.

Standard context fields

Discrimination example

Handling create-intent errors

create*Intent methods only cover the spoke-side transaction. Their narrow union excludes relay/verify codes:

Handling allowance + approval errors

Migration from the legacy error.message-based pattern

If you were on the previous CODE-string-on-error.message pattern (or the older MoneyMarketError<Code> typed shape that the public docs at https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market#error-handling document), here are the mappings:

Before
After

error.message === 'SUBMIT_TX_FAILED'

error.code === 'TX_SUBMIT_FAILED'

error.message === 'RELAY_TIMEOUT'

error.code === 'RELAY_TIMEOUT'

error.message === 'CREATE_SUPPLY_INTENT_FAILED'

error.code === 'INTENT_CREATION_FAILED'

error.message === 'CREATE_BORROW_INTENT_FAILED' etc.

error.code === 'INTENT_CREATION_FAILED' etc.

error.message === 'SUPPLY_UNKNOWN_ERROR' etc.

error.code === 'EXECUTION_FAILED' etc. (with cause)

isMoneyMarketSubmitTxFailedError(e)

e.code === 'TX_SUBMIT_FAILED' (after isMoneyMarketOrchestrationError(e) and e.context?.action === 'supply')

Prose error.message for invariants

error.code === 'VALIDATION_FAILED'; the prose stays on error.message

error.data.payload (historical)

Not preserved. Capture input params before calling if you need them for retry; this is the one departure from the historical published guidance.

Best practices

  1. Always handle TX_SUBMIT_FAILED. Critical — the spoke tx landed but the relay submission failed. Funds may be in flight; persist the user's input and retry.

  2. Handle RELAY_TIMEOUT gracefully. The spoke tx succeeded; the relay just didn't deliver in time. Check on-chain status before retrying.

  3. Discriminate RELAY_FAILED via context.relayCode. 'RELAY_POLLING_FAILED' (polling outage — packet status unknown) needs different UX from generic 'UNKNOWN'.

  4. Use error.cause for forensics. Every wrapped error preserves the original on cause. Loggers walk it automatically.

  5. Use JSON.stringify(error) for logging. The toJSON() method handles bigint coercion + cause-chain truncation safely.

  6. Type-guard, don't as-cast. Use the narrow guards (isMoneyMarketOrchestrationError, isMoneyMarketCreateIntentError, etc.) to narrow; an as MoneyMarketOrchestrationError cast after a generic isSodaxError check would silently widen the contract.

Data Retrieval and Formatting

The Money Market SDK provides comprehensive data retrieval and formatting capabilities through the MoneyMarketDataService, accessible as sodax.moneyMarket.data. This service allows you to fetch reserve data, user data, and format them into human-readable values with USD conversions.

Available Data Methods

Reserve Data

  • getReservesList(unfiltered?) - Get list of all reserve addresses (bnUSD debt reserve filtered by default)

  • getReservesData() - Get raw aggregated reserve data (bigint fields)

  • getReservesHumanized() - Get humanized reserve data with decimal strings

  • getReserveData(asset) - Get specific reserve data for an asset

  • getReserveNormalizedIncome(asset) - Get normalized income for a specific asset (RAY precision)

User Data

  • getUserReservesData(spokeChainKey, userAddress) - Get raw user reserve data

  • getUserReservesHumanized(spokeChainKey, userAddress) - Get humanized user reserve data

E-Mode Data

  • getEModes() - Get raw E-Mode data

  • getEModesHumanized() - Get humanized E-Mode data

Data Formatting

Formatting Reserve Data

  • formatReservesUSD(request) - Format an array of reserves with USD conversions

  • formatReserveUSD(request) - Format a single reserve with USD conversion

Formatting User Data

  • formatUserSummary(request) - Format user portfolio summary with USD conversions

NOTE: If you need more customized formatting, see math-utils.

Complete Example: Fetching and Formatting Data

Step-by-Step Data Retrieval Process

1. Fetch Raw Data

2. Build Formatting Requests

3. Format Data

Data Structure Examples

Formatted Reserve Data

The formattedReserves array entries extend the humanized reserve shape with USD-denominated fields computed by formatReservesUSD:

Formatted User Summary

The userSummary object contains the user's portfolio information:

Utility Functions

The SDK also provides utility functions for formatting specific values:

Last updated