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
Section titled “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
Section titled “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
Section titled “Mainnet”| Route | Symbol | Display Name | bankDenom | evmAddr | Default ERC‑20 Decimals |
|---|---|---|---|---|---|
usd | USDC | USDC | erc20/0x0829F361A05D993d5CEb035cA6DF3446b060970b | 0x0829F361A05D993d5CEb035cA6DF3446b060970b | 6 |
stnibi | stNIBI | Liquid Staked Nibiru (Wrapped) | tf/nibi1udqqx30cw8nwjxtl4l28ym9hhrp933zlq8dqxfjzcdhvl8y24zcqpzmh8m/ampNIBI | 0xcA0a9Fb5FBF692fa12fD13c0A900EC56Bb3f0a7b | 6 |
Testnet‑2
Section titled “Testnet‑2”| Route | Symbol | Display Name | bankDenom | evmAddr | Default ERC‑20 Decimals |
|---|---|---|---|---|---|
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
Section titled “Shared Concepts & Types”-
Signers
- WASM signer
{ fromBech32Addr, runner: NibiruTxClient } - EVM signer
{ fromHexAddr, runner?: ContractRunner, signer?: Signer }(must havesignerto send tx)
- WASM signer
-
TradeType Derivation
-
Market → "trade" -
Limit/Stopis derived by comparinglimitPriceUsdtoopen_price:- Long:
limit > open_price→stop, elselimit - Short:
limit < open_price→stop, elselimit
- Long:
-
-
Gas Strategy (EVM)
- Estimate with
contract.method.estimateGas(...)and pad by ×1.1 - Fallbacks:
gasLimit = 5_000_000,gasPrice = 1 gweiif estimation fails
- Estimate with
-
ExecuteResult (normalized)
- EVM receipts are converted to a CosmJS‑like
ExecuteResultwithheight,transactionHash,gasUsed/Wanted, andevents(topics/data echoed per log).
- EVM receipts are converted to a CosmJS‑like
-
Explorer Links
- UI examples compute
blockUrlandtxUrlfromheightandhash.
- UI examples compute
EVM Integration (via Interface Contract)
Section titled “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
Section titled “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": "0.01", "is_evm_origin": true }}slippage_p is a CosmWasm Decimal fraction, so "0.01" means 1%.
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
Section titled “2 Close Perp Trade”Method: executeSimpleFunctions(wasmMsgBytes)
wasmMsg:
{ "close_trade": { "trade_index": "UserTradeIndex(<N>)" } }3. Referral: Create Code
Section titled “3. Referral: Create Code”Method: executeSimpleFunctions(wasmMsgBytes)
wasmMsg:
{ "create_referrer_code": { "code": "<your_code>" } }4. Referral: Redeem Code
Section titled “4. Referral: Redeem Code”Method: executeSimpleFunctions(wasmMsgBytes)
wasmMsg:
{ "redeem_referrer_code": { "code": "<partner_code>" } }5. Vault: Deposit
Section titled “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
Section titled “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
Section titled “7. Vault: Redeem”Method:
contract.redeem( wasmMsgBytes, // { "redeem": { "shares": "..." } } vaultAddress, shares, // BANK units sendToEvm, // boolean overrides)8. Vault: Cancel Withdraw Request
Section titled “8. Vault: Cancel Withdraw Request”Method: executeVaultSimpleFunctions(wasmMsgBytes, vaultAddress)
wasmMsg:
{ "cancel_withdraw_request": { "unlock_epoch": <number> } }WASM (Cosmos) Integration
Section titled “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
Section titled “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": "0.01", "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
Section titled “2. Close Perp Trade”Client: execute(sender, saiContracts.perp, { close_trade: { trade_index: "UserTradeIndex(N)" } }, fee)
3. Referral: Create Code
Section titled “3. Referral: Create Code”Client: execute(sender, saiContracts.perp, { create_referrer_code: { code } }, fee)
4. Referral: Redeem Code
Section titled “4. Referral: Redeem Code”Client: execute(sender, saiContracts.perp, { redeem_referrer_code: { code } }, fee)
5. Vault: Deposit
Section titled “5. Vault: Deposit”Client: execute(sender, vaultSelection.address, { deposit: {} }, fee, memo, [funds])
Funds:
[{ "denom": "<collateral.bankDenom>", "amount": "<BANK units>" }]6. Vault: Make Withdraw Request
Section titled “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
Section titled “7. Vault: Redeem”Client: execute(sender, vaultAddress, { redeem: { shares: "..." } }, fee) (no funds)
8. Vault: Cancel Withdraw Request
Section titled “8. Vault: Cancel Withdraw Request”Client: execute(sender, vaultAddress, { cancel_withdraw_request: { unlock_epoch } }, fee)
Amounts & Decimals (Gotchas)
Section titled “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
Section titled “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
Section titled “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)
Section titled “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)
Section titled “Open Trade (WASM)”await wasm.execute( bech32, saiContracts.perp, { open_trade: { /* ... */, is_evm_origin: false } }, "auto", undefined, [ { denom: collateral.bankDenom, amount: scaledAmount } ])Vault Deposit (EVM)
Section titled “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)
Section titled “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)
Section titled “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
Section titled “Appendix: Utility Notes”estimateGasWithFallbackdoubles the estimate, floors it at2_500_000, and falls back to5_000_000 / 1 gweiif estimation fails. The estimate is not a reliable lower bound for wasm precompile calls, and gas is free at a zero fee price, so the padding is deliberately generous.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.