WASM & EVM Integration
Sai Contracts – WASM & EVM Integration
A practical, copy‑paste friendly reference for integrating with Sai’s perpetuals and SLP vaults from Cosmos (WASM) and EVM wallets. It documents all calls shown in the code you shared, with params, expected preconditions, and example snippets.
Environments: Mainnet and Testnet-2 are both supported. Token routes and contract addresses differ by network; see the Collateral / Payment Tokens section.
Quick Glossary
BANK: Cosmos-side coins (e.g.,
tf/...denoms).ERC-20: EVM-side tokens (e.g.,
0x...).Interface (EVM): A single EVM contract that accepts a JSON-encoded WASM message and forwards it cross‑stack to Sai’s modules.
Perp: The perpetuals contract/router on the Cosmos side (referenced via
saiContracts.perp).Vault: SLP vault contracts (Cosmos bech32 addresses) that accept deposits/withdrawals/redeems.
Collateral / Payment Tokens
These are the routes supported in the shared code (usd, stnibi). Values are used for funds on WASM calls and for ERC‑20 params on EVM interface calls.
Mainnet
usd
USDC
USDC
erc20/0x0829F361A05D993d5CEb035cA6DF3446b060970b
0x0829F361A05D993d5CEb035cA6DF3446b060970b
6
stnibi
stNIBI
Liquid Staked Nibiru (Wrapped)
tf/nibi1udqqx30cw8nwjxtl4l28ym9hhrp933zlq8dqxfjzcdhvl8y24zcqpzmh8m/ampNIBI
0xcA0a9Fb5FBF692fa12fD13c0A900EC56Bb3f0a7b
6
Testnet‑2
usd
USDC
USDC
tf/nibi1pc2mmwcqhvzn9vsm0umpu40yzl6gfy6nucwn7g/usdc
0xAb68f1D1d91854383fd4Df9016E3040D03e8191a
6
stnibi
stNIBI
Liquid Staked Nibiru (Wrapped)
tf/nibi1pc2mmwcqhvzn9vsm0umpu40yzl6gfy6nucwn7g/stnibi
0xCae3d404AFB50016154a4B18091351065154E9bD
6
Tip: When calling WASM, you spend BANK units (scaled by token decimals, 6 by default). On EVM, you pass both a BANK portion and an ERC‑20 portion; these are combined for total amounts.
Shared Concepts & Types
Signers
WASM signer
{ fromBech32Addr, runner: NibiruTxClient }EVM signer
{ fromHexAddr, runner?: ContractRunner, signer?: Signer }(must havesignerto send tx)
TradeType Derivation
Market → "trade"Limit/Stopis derived by comparinglimitPriceUsdtoopen_price:Long:
limit > open_price→stop, elselimitShort:
limit < open_price→stop, elselimit
Gas Strategy (EVM)
Estimate with
contract.method.estimateGas(...)and pad by ×1.1Fallbacks:
gasLimit = 5_000_000,gasPrice = 1 gweiif estimation fails
ExecuteResult (normalized)
EVM receipts are converted to a CosmJS‑like
ExecuteResultwithheight,transactionHash,gasUsed/Wanted, andevents(topics/data echoed per log).
Explorer Links
UI examples compute
blockUrlandtxUrlfromheightandhash.
EVM Integration (via Interface Contract)
All EVM flows call a single Interface contract (obtained via saiEvmInterfaceFromChainType(chainType)). Each call sends a JSON-encoded WASM message (wasmMsgBytes) together with token amounts/addresses. Always ensure an EVM signer is available.
1. Open Perp Trade
Method: openTrade(wasmMsgBytes, collateralIndex, totalAmountBankUnits, useErc20Amount)
wasmMsg (JSON):
{
"open_trade": {
"market_index": "MarketIndex(N)",
"leverage": "string",
"long": true,
"collateral_index": "TokenIndex(N)",
"trade_type": "trade|limit|stop",
"open_price": "<market price or limit price>",
"tp": "optional string",
"sl": "optional string",
"slippage_p": "1",
"is_evm_origin": true
}
}Amounting:
Compute BANK portion in 6‑dec units.
Compute ERC‑20 portion in token decimals (default 6; use
collateral.evmDefaultDecimalsif present).totalAmount=bankAmount + evmAmount(on‑chain Interface handles bridging/combining).
Preconditions:
orderType === "Market"requiresopen_price.collateralmust be present;amtTrademust be > 0.If using smart allocation, split requested amount across BANK/ERC‑20 per wallet balances and preference.
2 Close Perp Trade
Method: executeSimpleFunctions(wasmMsgBytes)
wasmMsg:
{ "close_trade": { "trade_index": "UserTradeIndex(<N>)" } }3. Referral: Create Code
Method: executeSimpleFunctions(wasmMsgBytes)
wasmMsg:
{ "create_referrer_code": { "code": "<your_code>" } }4. Referral: Redeem Code
Method: executeSimpleFunctions(wasmMsgBytes)
wasmMsg:
{ "redeem_referrer_code": { "code": "<partner_code>" } }5. Vault: Deposit
Method:
contract.deposit(
wasmMsgBytes, // { "deposit": {} }
depositAmountTotalBankUnits, // BANK units (includes ERC‑20 converted -> BANK)
useErc20Amount, // ERC‑20 units (token decimals)
vaultAddress, // bech32 vault contract
collateralErc20, // ERC‑20 address
true, // sendToEvm on triggers
overrides
)Notes:
Convert ERC‑20 portion to BANK units before summing into
depositAmountTotalif needed (decimals may differ).
6. Vault: Make Withdraw Request
Method:
contract.makeWithdrawRequest(
vaultAddress,
wasmMsgBytes, // { "make_withdraw_request": {} }
totalShares, // BANK units to lock (sharesBANK + sharesFromERC20)
useErc20Amount, // shares to bridge from ERC‑20 (BANK-scaled already)
overrides
)7. Vault: Redeem
Method:
contract.redeem(
wasmMsgBytes, // { "redeem": { "shares": "..." } }
vaultAddress,
shares, // BANK units
sendToEvm, // boolean
overrides
)8. Vault: Cancel Withdraw Request
Method: executeVaultSimpleFunctions(wasmMsgBytes, vaultAddress)
wasmMsg:
{ "cancel_withdraw_request": { "unlock_epoch": <number> } }WASM (Cosmos) Integration
WASM calls use the NibiruTxClient (signer.runner.wasmClient). Funds are passed as Cosmos Coin { denom, amount } where amount is BANK units (1e6‑scaled by default).
1. Open Perp Trade
Client: wasmClient.execute(sender, saiContracts.perp, msg, fee, memo, funds)
Msg:
{
"open_trade": {
"market_index": "MarketIndex(N)",
"leverage": "string",
"long": true,
"collateral_index": "TokenIndex(N)",
"trade_type": "trade|limit|stop",
"open_price": "<market price or limit price>",
"slippage_p": "1",
"tp": "optional string",
"sl": "optional string",
"is_evm_origin": false
}
}Funds:
[{ "denom": "<collateral.bankDenom>", "amount": "<BANK units>" }]Preconditions:
For
Market,open_priceis required.For
Limit/Stop,limitPriceUsdis required.
Optional ERC‑20 → BANK bridging
If the current BANK balance is insufficient, prepend a
MsgConvertEvmToCointo convert ERC‑20 to BANK for the deficit.
2. Close Perp Trade
Client: execute(sender, saiContracts.perp, { close_trade: { trade_index: "UserTradeIndex(N)" } }, fee)
3. Referral: Create Code
Client: execute(sender, saiContracts.perp, { create_referrer_code: { code } }, fee)
4. Referral: Redeem Code
Client: execute(sender, saiContracts.perp, { redeem_referrer_code: { code } }, fee)
5. Vault: Deposit
Client: execute(sender, vaultSelection.address, { deposit: {} }, fee, memo, [funds])
Funds:
[{ "denom": "<collateral.bankDenom>", "amount": "<BANK units>" }]6. Vault: Make Withdraw Request
Share Denom Discovery: Query once before executing:
{ "get_vault_share_denom": {} }Client:
Execute with funds in share denom:
[{ "denom": "<shareDenom>", "amount": "<shares BANK units>" }]Msg: { "make_withdraw_request": {} }
7. Vault: Redeem
Client: execute(sender, vaultAddress, { redeem: { shares: "..." } }, fee) (no funds)
8. Vault: Cancel Withdraw Request
Client: execute(sender, vaultAddress, { cancel_withdraw_request: { unlock_epoch } }, fee)
Amounts & Decimals (Gotchas)
Default decimals = 6 for BANK. Always scale UI amounts:
display × 10^decimals.When mixing BANK and ERC‑20 on EVM deposit/withdraw, convert across decimals before summing.
Validate that amounts are finite, non‑negative, non‑zero after scaling.
Error Handling & UX Patterns
Signer presence: EVM calls require
signer(not just a runner). WASM requiresrunner.Toast/Notifications: Use pending/success/failure to surface status. On success, show
heightandhashwith explorer links.Debugging (EVM): If a tx reverts, use archive RPC’s
debug_traceTransactionto extract the deepestcalls[-1].error.
Minimal Pseudocode Examples
Below are slimmed examples you can adapt; they assume your app has already selected
chainType,saiContracts,interfaceAddress,collateral, etc.
Open Trade (EVM)
const msg = { open_trade: { /* ...see schema above... */ is_evm_origin: true } }
const wasmMsgBytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const gas = await estimateGasWithFallback(
() => contract.openTrade.estimateGas(wasmMsgBytes, collateralIndex, totalAmount, useErc20),
{ msg }
)
const tx = await contract.openTrade(wasmMsgBytes, collateralIndex, totalAmount, useErc20, toTxOverrides(gas))
const res = await tx.wait() // -> map to ExecuteResult if desiredOpen Trade (WASM)
await wasm.execute(
bech32,
saiContracts.perp,
{ open_trade: { /* ... */, is_evm_origin: false } },
"auto",
undefined,
[ { denom: collateral.bankDenom, amount: scaledAmount } ]
)Vault Deposit (EVM)
const msg = { deposit: {} }
const wasmMsgBytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const bankTotal = bankAmount + toBankUnits(erc20Amount, erc20Dec, 6)
const tx = await contract.deposit(wasmMsgBytes, bankTotal, erc20Amount, vaultAddr, collateral.evmAddr, true, overrides)
await tx.wait()Make Withdraw Request (WASM)
const shareDenom = await wasm.queryContractSmart(vaultAddr, { get_vault_share_denom: {} })
await wasm.execute(
bech32,
vaultAddr,
{ make_withdraw_request: {} },
"auto",
undefined,
[ { denom: shareDenom, amount: shares } ]
)Preconditions Checklist (per call)
Open Trade: amount > 0; Market→
open_pricepresent; Limit/Stop→limitPriceUsdpresent;collateral.indexparseable; signer ready.Close Trade:
userTradeIndexset; signer ready.Create/Redeem Referral:
codenon‑empty; signer ready.Vault Deposit: amounts properly scaled; convert ERC‑20 portion to BANK for totals on EVM; signer ready.
Withdraw Request: shares in BANK units; (WASM) share denom fetched; signer ready.
Redeem: shares in BANK units;
sendToEvmflag set (EVM path); signer ready.Cancel Withdraw Request:
unlock_epochset; signer ready.
Appendix: Utility Notes
estimateGasWithFallbackpads gas by 10% and falls back to5_000_000 / 1 gweiif estimation fails.receiptToExecuteResultnormalizes EVM receipts to a CosmJS‑like shape.MsgConvertEvmToCoin(WASM path) can optionally be appended if BANK funds are insufficient.Smart Allocation (optional): split requested amount across BANK / ERC‑20 based on balances and user preference; then scale per‑side and stitch back together.
Last updated