このページの翻訳は LLM によって生成されています。誤りに気づいた場合は GitHub で issue を開いてお知らせください。
Sai EVM連携ガイド
EVMの署名者とInterfaceコントラクトでSaiを操作します。JSONの wasmMsgBytes と資産の数量・アドレスを渡します。
0. はじめに
Section titled “0. はじめに”- Node.js ≥ 18。
- pnpm、npm、yarnのいずれか。
新しいプロジェクト
Section titled “新しいプロジェクト”mkdir sai-evm && cd sai-evmpnpm init -ypnpm add ethers bignumber.jspnpm add -D typescript ts-node @types/node dotenvnpx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext同じアプリでCosmos側も使うなら、
@nibiruchain/nibijs @cosmjs/cosmwasm-stargate @cosmjs/stargate cosmjs-typesを追加します。
設定を持つ .env を作成します。
# Testnet-2EVM_RPC=https://evm-rpc.testnet-2.nibiru.fi/
# Or Mainnet# EVM_RPC=https://evm-rpc.nibiru.fi/
PRIVATE_KEY=0x... # Your private key (keep this secret!)INTERFACE_ADDR=0x... # Contract address from saiEvmInterfaceFromChainType(chainType)Providerと署名者の初期化
Section titled “Providerと署名者の初期化”import "dotenv/config"import { ethers } from "ethers"
export function makeSigner() { const rpc = process.env.EVM_RPC! const pk = process.env.PRIVATE_KEY! const provider = new ethers.JsonRpcProvider(rpc) const signer = new ethers.Wallet(pk, provider) return { provider, signer }}インターフェースABI
Section titled “インターフェースABI”openTrade、executeSimpleFunctions、deposit、makeWithdrawRequest、redeem、executeVaultSimpleFunctions を公開するABIを読み込みます。
import PerpVaultEvmInterfaceAbi from "./PerpVaultEvmInterface.json" // Place your ABI file hereガス推定の補助関数
Section titled “ガス推定の補助関数”eth_estimateGas は無制限のCosmos SDKガスメーターで実行するため、実行時のERC-20サブ呼び出しにかかる63/64制限を反映せず、必要量より少なくなることがあります。十分な余裕と最低値を設定してください。料率はゼロなので、大きな上限に追加費用はなく、小さすぎると取引が失敗します。
const FALLBACK_GAS_LIMIT = 5_000_000nconst MIN_GAS_LIMIT = 2_500_000nconst FALLBACK_GAS_PRICE = ethers.parseUnits("1", "gwei")
const withGasBuffer = (g: bigint) => { const padded = g <= 0n ? 0n : g * 2n return padded > MIN_GAS_LIMIT ? padded : MIN_GAS_LIMIT}
async function estimateGasWithFallback( fn: () => Promise<bigint>, info?: unknown) { try { const est = await fn() return { gasLimit: withGasBuffer(est) } } catch (error) { console.warn("Gas estimation failed, using fallback", info) return { gasLimit: FALLBACK_GAS_LIMIT, gasPrice: FALLBACK_GAS_PRICE } }}
const toTxOverrides = ({ gasLimit, gasPrice,}: { gasLimit: bigint gasPrice?: bigint}) => (gasPrice === undefined ? { gasLimit } : { gasLimit, gasPrice })1. 担保・支払いトークン
Section titled “1. 担保・支払いトークン”小数桁の処理
Section titled “小数桁の処理”- BANKはCosmos側の小数6桁。
- ERC-20はトークンの桁数、通常6。
Testnet-2
Section titled “Testnet-2”| トークン | ERC-20アドレス |
|---|---|
| USDC | 0xAb68f1D1d91854383fd4Df9016E3040D03e8191a |
| stNIBI | 0xCae3d404AFB50016154a4B18091351065154E9bD |
メインネット
Section titled “メインネット”| トークン | ERC-20アドレス |
|---|---|
| USDC | 0x0829F361A05D993d5CEb035cA6DF3446b060970b |
| stNIBI | 0xcA0a9Fb5FBF692fa12fD13c0A900EC56Bb3f0a7b |
小数桁の変換
Section titled “小数桁の変換”BANKとERC-20の合計前に、両方をBANKの6桁単位にそろえます。
const toBankUnits = ( amt: bigint, sourceDecimals: number, bankDecimals: number = 6): bigint => { if (sourceDecimals === bankDecimals) return amt const diff = Math.abs(sourceDecimals - bankDecimals) const factor = 10n ** BigInt(diff) return sourceDecimals > bankDecimals ? amt / factor : amt * factor}2. 無期限先物の新規注文
Section titled “2. 無期限先物の新規注文”メソッドのシグネチャ
Section titled “メソッドのシグネチャ”contract.openTrade( wasmMsgBytes, // JSON-encoded WASM message with is_evm_origin: true collateralIndex, // Token index (e.g., 0 for first collateral) totalAmountBankUnits, // Total amount in BANK units useErc20Amount, // ERC-20 amount in token decimals overrides // Gas overrides)import { ethers } from "ethers"import BigNumber from "bignumber.js"import PerpVaultEvmInterfaceAbi from "./PerpVaultEvmInterface.json"import { makeSigner } from "./evm"
async function openTrade() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { open_trade: { market_index: "MarketIndex(0)", leverage: "5", long: true, collateral_index: "TokenIndex(0)", trade_type: "trade", open_price: "70000", tp: undefined, sl: undefined, slippage_p: "0.01", // CosmWasm Decimal fraction: 0.01 = 1% is_evm_origin: true, // IMPORTANT: Set to true for EVM origin }, }
const bankDecimals = 6 const erc20Decimals = 6 const displayAmount = new BigNumber("100") const bankAmount = ethers.parseUnits(displayAmount.toString(), bankDecimals) const erc20Amount = ethers.parseUnits(displayAmount.toString(), erc20Decimals)
const toBankUnits = ( amt: bigint, ercDec: number, bankDec: number ): bigint => { if (ercDec === bankDec) return amt const diff = Math.abs(ercDec - bankDec) const factor = 10n ** BigInt(diff) return ercDec > bankDec ? amt / factor : amt * factor }
const totalBank = bankAmount + toBankUnits(erc20Amount, erc20Decimals, bankDecimals) const wasmMsgBytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const gas = await estimateGasWithFallback(() => iface.openTrade.estimateGas(wasmMsgBytes, 0, totalBank, erc20Amount) )
const tx = await iface.openTrade( wasmMsgBytes, 0, totalBank, erc20Amount, toTxOverrides(gas) ) console.log("Transaction submitted:", tx.hash)
const receipt = await tx.wait() console.log("Mined in block:", receipt?.blockNumber)}3. 無期限先物の決済
Section titled “3. 無期限先物の決済”未決済ポジションを決済します。
async function closeTrade() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { close_trade: { trade_index: "UserTradeIndex(0)" } } const wasmMsgBytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const gas = await estimateGasWithFallback(() => iface.executeSimpleFunctions.estimateGas(wasmMsgBytes) )
const tx = await iface.executeSimpleFunctions(wasmMsgBytes, toTxOverrides(gas)) const receipt = await tx.wait() console.log("Trade closed in block:", receipt?.blockNumber)}4. 紹介コードの作成・適用
Section titled “4. 紹介コードの作成・適用”紹介コードの作成
Section titled “紹介コードの作成”async function createReferralCode() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { create_referrer_code: { code: "MYCODE" } } const bytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const gas = await estimateGasWithFallback(() => iface.executeSimpleFunctions.estimateGas(bytes) )
const tx = await iface.executeSimpleFunctions(bytes, toTxOverrides(gas)) await tx.wait() console.log("Referral code created")}紹介コードの適用
Section titled “紹介コードの適用”async function redeemReferralCode() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { redeem_referrer_code: { code: "PARTNER" } } const bytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const gas = await estimateGasWithFallback(() => iface.executeSimpleFunctions.estimateGas(bytes) )
const tx = await iface.executeSimpleFunctions(bytes, toTxOverrides(gas)) await tx.wait() console.log("Referral code redeemed")}5. ボールト入金
Section titled “5. ボールト入金”ボールトに担保を入金します。
async function depositToVault() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { deposit: {} } const bytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const bankDecimals = 6 const erc20Decimals = 6 const bankAmount = ethers.parseUnits("250", bankDecimals) const erc20Amount = ethers.parseUnits("250", erc20Decimals)
const toBankUnits = ( amt: bigint, ercDec: number, bankDec: number ): bigint => { if (ercDec === bankDec) return amt const diff = Math.abs(ercDec - bankDec) const factor = 10n ** BigInt(diff) return ercDec > bankDec ? amt / factor : amt * factor }
const totalBank = bankAmount + toBankUnits(erc20Amount, erc20Decimals, bankDecimals)
const vaultAddr = "<bech32 vault address>" const collateralErc20 = "<erc20 token address>"
const gas = await estimateGasWithFallback(() => iface.deposit.estimateGas( bytes, totalBank, erc20Amount, vaultAddr, collateralErc20, true ) )
const tx = await iface.deposit( bytes, totalBank, erc20Amount, vaultAddr, collateralErc20, true, toTxOverrides(gas) ) await tx.wait() console.log("Deposited to vault")}6. ボールト出金申請
Section titled “6. ボールト出金申請”ボールトから出金を申請します。
async function makeWithdrawRequest() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { make_withdraw_request: {} } const bytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const sharesBank = 1_000_000n // 1.0 shares in BANK units const sharesFromErc20 = 0n const totalShares = sharesBank + sharesFromErc20
const vaultAddr = "<vault address>"
const gas = await estimateGasWithFallback(() => iface.makeWithdrawRequest.estimateGas(vaultAddr, bytes, totalShares, sharesFromErc20) )
const tx = await iface.makeWithdrawRequest( vaultAddr, bytes, totalShares, sharesFromErc20, toTxOverrides(gas) ) await tx.wait() console.log("Withdraw request created")}7. ボールト償還
Section titled “7. ボールト償還”持分を償還します。
async function redeemVaultShares() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { redeem: { shares: "500000" } } // 0.5 shares const bytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const vaultAddr = "<vault address>" const sendToEvm = true
const gas = await estimateGasWithFallback(() => iface.redeem.estimateGas(bytes, vaultAddr, "500000", sendToEvm) )
const tx = await iface.redeem( bytes, vaultAddr, "500000", sendToEvm, toTxOverrides(gas) ) await tx.wait() console.log("Shares redeemed")}8. 出金申請取り消し
Section titled “8. 出金申請取り消し”待機中の申請を取り消します。
async function cancelWithdrawRequest() { const { signer } = makeSigner() const iface = new ethers.Contract( process.env.INTERFACE_ADDR!, PerpVaultEvmInterfaceAbi.abi, signer )
const msg = { cancel_withdraw_request: { unlock_epoch: 123 } } const bytes = ethers.toUtf8Bytes(JSON.stringify(msg))
const vaultAddr = "<vault address>"
const gas = await estimateGasWithFallback(() => iface.executeVaultSimpleFunctions.estimateGas(bytes, vaultAddr) )
const tx = await iface.executeVaultSimpleFunctions( bytes, vaultAddr, toTxOverrides(gas) ) await tx.wait() console.log("Withdraw request cancelled")}エラー処理と問題の解決
Section titled “エラー処理と問題の解決”よくある問題
Section titled “よくある問題”| 問題 | 解決方法 |
|---|---|
| ”No EVM signer” | makeSigner() の呼び出しと .env の PRIVATE_KEY を確認 |
| ガス推定失敗 | 自動的に 5,000,000 ガスと 1 gwei に切り替え |
| オンチェーンのリバート | archive RPCの debug_traceTransaction で最深部のエラーを確認 |
| 小数桁変換エラー | 加算前にBANK・ERC-20の桁を統一 |
無効な INTERFACE_ADDR | Testnet-2かメインネットか、ネットワークに合うか確認 |
- 必ずEVMの署名者を使ってください。Providerだけでは送信できません。
- ガスの余裕を設けて不足を防ぎます。
- 全取引メッセージの
is_evm_origin: trueを確認します。 - 合計前に小数桁を変換します。
PRIVATE_KEYを安全に保ち、Gitにコミットしてはいけません。