Lend / Borrow (Money Market)
Error handling conventions: This module uses the canonical
SodaxError<MoneyMarketErrorCode>shape (same family as the swap module). Discriminate onresult.error.code(e.g.'RELAY_TIMEOUT','EXECUTION_FAILED'); structured details live onresult.error.context(action,phase,relayCode,field). See the Error Handling section below for the full per-method code table and migration notes from the legacyerror.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 chaingetSupportedTokens()- Get all supported money market tokens per chaingetSupportedReserves()- Get all supported money market reserves (hub chain addresses)
Allowance & Approval
isAllowanceValid({ params })- Check if token approval/trustline is sufficientapprove({ 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 onlyborrow({ params, walletProvider, timeout? })- Borrow tokens (complete operation with relay)createBorrowIntent({ params, walletProvider?, raw?, skipSimulation? })- Create borrow intent onlywithdraw({ params, walletProvider, timeout? })- Withdraw tokens (complete operation with relay)createWithdrawIntent({ params, walletProvider?, raw?, skipSimulation? })- Create withdraw intent onlyrepay({ 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 addressesdata.getReservesData()- Get raw aggregated reserve datadata.getReservesHumanized()- Get humanized reserve datadata.getReserveData(asset)- Get specific reserve datadata.getReserveNormalizedIncome(asset)- Get normalized income for a specific asset (RAY precision)data.getUserReservesData(spokeChainKey, userAddress)- Get raw user reserve datadata.getUserReservesHumanized(spokeChainKey, userAddress)- Get humanized user reserve datadata.getEModes()- Get raw E-Mode datadata.getEModesHumanized()- Get humanized E-Mode datadata.formatReservesUSD(request)- Format reserves with USD conversionsdata.formatReserveUSD(request)- Format a single reserve with USD conversiondata.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, orMoneyMarketRepayParams). Every params type carries:srcChainKey: K— the source spoke chain (drives TypeScript narrowing ofwalletProvider)srcAddress: string— the caller's address on the source chaintoken: string— token address on the source chain (or destination chain for borrow/withdraw)amount: bigint— amount in token's native decimalsaction: 'supply' | 'borrow' | 'withdraw' | 'repay'dstChainKey?: SpokeChainKey— optional destination chain (defaults tosrcChainKey)dstAddress?: string— optional destination address (defaults tosrcAddress)
walletProvider: The wallet provider for the source chain. Required whenrawisfalse(or omitted); forbidden whenraw: true. The type is automatically narrowed to the correct interface for the givensrcChainKey(e.g.IEvmWalletProviderfor EVM chains).raw: (Optional, defaultfalse) Whentrue, returns unsigned transaction data instead of executing. Whentrue,walletProvidermust not be passed. Used increate*Intentandapprovemethods.skipSimulation: (Optional, defaultfalse) Skip transaction simulation before broadcast. Used increate*Intentmethods.timeout: (Optional, default:DEFAULT_RELAY_TX_TIMEOUT= 120 seconds) Timeout in milliseconds for relay operations. Used insupply,borrow,withdraw, andrepaymethods.
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
isAllowanceValidandapprovemethods 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 tokensrepay- Approves the asset manager contract to spend tokens
Sonic Spoke Provider (Hub Chain):
supply- Approves the user hub router to spend tokensrepay- 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:
supply: Executes the spoke-side deposit, relays to the hub, and waits for the relay to settle.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.
borrow: Executes the spoke-side message, relays to the hub, and waits for the relay to settle.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.
withdraw: Executes the spoke-side message, relays to the hub, and waits for the relay to settle.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.
repay: Executes the spoke-side deposit, relays to the hub, and waits for the relay to settle.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>
SodaxError<C>All MM-module errors are instances of SodaxError, exported from @sodax/sdk:
Rules:
Discriminate on
error.code— never onerror.message(which is human-readable, may change).error.causewalks the underlying error chain (loggers like Sentry/Pino/Datadog walk this automatically).error.contextcarries 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 incontextare coerced to strings, cause walked depth-3, no circular hazards).Use
isMoneyMarketError(e)(broad) or one of the narrow guardsisMoneyMarketOrchestrationError(e)/isMoneyMarketCreateIntentError(e)/isMoneyMarketApproveError(e)/isMoneyMarketAllowanceCheckError(e)/isMoneyMarketGasEstimationError(e)from@sodax/sdkinstead ofinstanceof SodaxErrorin 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.
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
context fieldsDiscrimination 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
error.message-based patternIf 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:
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
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.Handle
RELAY_TIMEOUTgracefully. The spoke tx succeeded; the relay just didn't deliver in time. Check on-chain status before retrying.Discriminate
RELAY_FAILEDviacontext.relayCode.'RELAY_POLLING_FAILED'(polling outage — packet status unknown) needs different UX from generic'UNKNOWN'.Use
error.causefor forensics. Every wrapped error preserves the original oncause. Loggers walk it automatically.Use
JSON.stringify(error)for logging. ThetoJSON()method handles bigint coercion + cause-chain truncation safely.Type-guard, don't
as-cast. Use the narrow guards (isMoneyMarketOrchestrationError,isMoneyMarketCreateIntentError, etc.) to narrow; anas MoneyMarketOrchestrationErrorcast after a genericisSodaxErrorcheck 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 stringsgetReserveData(asset)- Get specific reserve data for an assetgetReserveNormalizedIncome(asset)- Get normalized income for a specific asset (RAY precision)
User Data
getUserReservesData(spokeChainKey, userAddress)- Get raw user reserve datagetUserReservesHumanized(spokeChainKey, userAddress)- Get humanized user reserve data
E-Mode Data
getEModes()- Get raw E-Mode datagetEModesHumanized()- Get humanized E-Mode data
Data Formatting
Formatting Reserve Data
formatReservesUSD(request)- Format an array of reserves with USD conversionsformatReserveUSD(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