diff --git a/contracts/test/EVMPrecompileTest.js b/contracts/test/EVMPrecompileTest.js index 88450768e6..1786ed566b 100755 --- a/contracts/test/EVMPrecompileTest.js +++ b/contracts/test/EVMPrecompileTest.js @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); const { expectRevert } = require('@openzeppelin/test-helpers'); -const { setupSigners, getAdmin, deployWasm, storeWasm, execute, isDocker, ABI, createTokenFactoryTokenAndMint, getSeiBalance, rawHttpDebugTraceWithCallTracer} = require("./lib"); +const { setupSigners, getAdmin, deployWasm, storeWasm, execute, isDocker, ABI, createTokenFactoryTokenAndMint, getSeiBalance, rawHttpDebugTraceWithCallTracer, proposeParamChange} = require("./lib"); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -125,8 +125,19 @@ describe("EVM Precompile Tester", function () { let govProposal; before(async function () { - const govProposalResponse = JSON.parse(await execute(`seid tx gov submit-proposal param-change ../contracts/test/param_change_proposal.json --from admin --fees 20000usei -b block -y -o json`)) - govProposal = govProposalResponse.logs[0].events[3].attributes[1].value; + // Use lib.js proposeParamChange (Autobahn-compatible: -b sync + poll + // gov state) instead of an inline `seid tx -b block` which hangs + // 60s under Autobahn. Mirrors ../contracts/test/param_change_proposal.json + // (kept on disk for any tooling that still consumes it). + govProposal = await proposeParamChange( + "Gov Param Change", + "Update quorum to 0.45", + [{ subspace: "gov", key: "tallyparams", value: { quorum: "0.45" } }], + "200000000usei", + "20000usei", + "admin", + false, // not expedited — matches the JSON file + ); const signer = accounts[0].signer const contractABIPath = '../../precompiles/gov/abi.json'; diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 83655dc40d..4e9681ab55 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -77,6 +77,24 @@ async function delay() { await sleep(1000) } +// Wait until the chain advances by `blocks` blocks. Used after `-b sync` +// submissions to give the chain time to include the tx — engine-agnostic +// substitute for a fixed sleep, since block times differ between CometBFT +// (~1s) and Autobahn (~100ms). Default is 2 blocks rather than 1 because +// the next block can be empty (the submitted tx is still in mempool and +// lands one block later); 2 blocks closes that race in nearly all cases +// without re-introducing a fixed sleep. +async function waitForBlocks(blocks=2, timeoutMs=15000) { + const start = await ethers.provider.getBlockNumber() + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const cur = await ethers.provider.getBlockNumber() + if (cur >= start + blocks) return + await sleep(50) + } + throw new Error(`block didn't advance by ${blocks} in ${timeoutMs}ms (start=${start})`) +} + async function getCosmosTx(provider, evmTxHash) { return await provider.send("sei_getCosmosTx", [evmTxHash]) } @@ -92,18 +110,25 @@ async function fundAddress(addr, amount="1000000000000000000000") { } async function evmSend(addr, fromKey, amount="10000000000000000000000000") { - const output = await execute(`seid tx evm send ${addr} ${amount} --from ${fromKey} -b block -y`); + // -b sync (returns on mempool acceptance) instead of -b block (subscribes + // to EventDataTx). Under Autobahn, executeBlock doesn't fire EventDataTx, + // so -b block hangs to its 60s timeout. -b sync is sufficient because + // callers don't read the deliver_tx event payload from this output. + const output = await execute(`seid tx evm send ${addr} ${amount} --from ${fromKey} -b sync -y`); + await waitForBlocks() return output.replace(/.*0x/, "0x").trim() } async function bankSend(toAddr, fromKey, amount="100000000000", denom="usei") { - const result = await execute(`seid tx bank send ${fromKey} ${toAddr} ${amount}${denom} -b block --fees 20000usei -y`); - await delay() + const result = await execute(`seid tx bank send ${fromKey} ${toAddr} ${amount}${denom} -b sync --fees 20000usei -y`); + await waitForBlocks() return result } async function fundSeiAddress(seiAddr, amount="100000000000", denom="usei", funder=adminKeyName) { - return await execute(`seid tx bank send ${funder} ${seiAddr} ${amount}${denom} -b block --fees 20000usei -y`); + const result = await execute(`seid tx bank send ${funder} ${seiAddr} ${amount}${denom} -b sync --fees 20000usei -y`); + await waitForBlocks() + return result } async function getSeiBalance(seiAddr, denom="usei") { @@ -155,8 +180,8 @@ async function getKeySeiAddress(name) { async function associateKey(keyName) { try { - await execute(`seid tx evm associate-address --from ${keyName} -b block`) - await delay() + await execute(`seid tx evm associate-address --from ${keyName} -b sync`) + await waitForBlocks() }catch(e){ console.log("skipping associate") } @@ -219,16 +244,36 @@ async function rawHttpDebugTraceWithCallTracer(txHash) { } async function createTokenFactoryTokenAndMint(name, amount, recipient, from=adminKeyName) { - const command = `seid tx tokenfactory create-denom ${name} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json` - const output = await execute(command); - const response = JSON.parse(output) - const token_denom = getEventAttribute(response, "create_denom", "new_token_denom") - const mint_command = `seid tx tokenfactory mint ${amount}${token_denom} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json` - await execute(mint_command); + // -b sync everywhere (no -b block hang under Autobahn). The tokenfactory + // denom format is factory//, so we construct + // the denom locally from the from-key's address rather than reading + // the create_denom event from the tx response — -b sync doesn't carry + // deliver_tx events. + const creator = await getKeySeiAddress(from); + const token_denom = `factory/${creator}/${name}`; + + const createOut = await execute(`seid tx tokenfactory create-denom ${name} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json`); + const createResp = JSON.parse(createOut); + if (createResp.code !== 0) { + throw new Error(`createTokenFactoryTokenAndMint: create-denom rejected: ${createResp.raw_log}`); + } + await waitForBlocks(); + + const mintOut = await execute(`seid tx tokenfactory mint ${amount}${token_denom} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json`); + const mintResp = JSON.parse(mintOut); + if (mintResp.code !== 0) { + throw new Error(`createTokenFactoryTokenAndMint: mint rejected: ${mintResp.raw_log}`); + } + await waitForBlocks(); + + const sendOut = await execute(`seid tx bank send ${from} ${recipient} ${amount}${token_denom} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json`); + const sendResp = JSON.parse(sendOut); + if (sendResp.code !== 0) { + throw new Error(`createTokenFactoryTokenAndMint: bank send rejected: ${sendResp.raw_log}`); + } + await waitForBlocks(); - const send_command = `seid tx bank send ${from} ${recipient} ${amount}${token_denom} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json` - await execute(send_command); - return token_denom + return token_denom; } async function getChainId() { @@ -402,19 +447,59 @@ async function proposeParamChange(title, description, changes, deposit="20000000 }; const proposalJson = JSON.stringify(proposal); const tempFile = `/tmp/param_change_${Date.now()}.json`; - + // Use base64 encoding to avoid quote escaping issues in Docker const base64Json = Buffer.from(proposalJson).toString('base64'); await execute(`echo ${base64Json} | base64 -d > ${tempFile}`); - - const command = `seid tx gov submit-proposal param-change ${tempFile} --from ${from} --fees ${fees} -y -o json --broadcast-mode=block`; + + // Snapshot max proposal id before submit so we can identify the new one + // by polling — see comment below for why we don't use the tx response. + const maxIdBefore = await maxProposalId(); + + // -b sync (returns on mempool acceptance) instead of -b block (subscribes + // to EventDataTx). Under Autobahn, executeBlock doesn't fire EventDataTx, + // so -b block hangs to its 60s timeout on every call. -b sync also means + // the tx response no longer carries the deliver_tx events — including the + // submit_proposal event we used to read proposal_id from — so we instead + // poll gov state until a new proposal appears. + const command = `seid tx gov submit-proposal param-change ${tempFile} --from ${from} --fees ${fees} -y -o json -b sync`; const output = await execute(command); await execute(`rm ${tempFile}`); const response = JSON.parse(output); if (response.code !== 0) { throw new Error(`Failed to submit proposal: ${response.raw_log}`); } - return getEventAttribute(response, "submit_proposal", "proposal_id"); + + // Poll for the new proposal to land. Tx hash lookup (`seid q tx `) + // would need a Cosmos-side tx indexer, which Autobahn doesn't run. + // 30s should comfortably cover the inclusion + commit window. + const deadline = Date.now() + 30000; + while (Date.now() < deadline) { + const cur = await maxProposalId(); + if (cur > maxIdBefore) return String(cur); + await sleep(250); + } + throw new Error(`proposal submitted (tx ${response.txhash}) but did not appear in gov state within 30s`); +} + +// Returns the highest existing proposal id, or 0 if there are no proposals. +async function maxProposalId() { + let out; + try { + // seid exits non-zero with "Error: no proposals found" on an empty + // gov set; treat that as id=0 and fall through to JSON parsing for + // the populated case. Avoid using a `|| echo` fallback inside the + // pipeline because the docker-exec wrapper double-quotes the inner + // command and a JSON literal with single quotes would terminate + // the outer quote region. + out = await execute(`seid q gov proposals --reverse --limit 1 -o json 2>/dev/null`); + } catch (e) { + return 0; + } + if (!out || !out.trim()) return 0; + const proposals = JSON.parse(out).proposals || []; + if (proposals.length === 0) return 0; + return Number(proposals[0].proposal_id || proposals[0].id); } async function proposeDisableWasm(title="Disable WASM", description="Disable cosmwasm store code and instantiate operations", deposit="200000000usei", fees="200000usei", from=adminKeyName) { @@ -498,9 +583,9 @@ async function ensureWasmDisabled(from=adminKeyName) { async function passProposal(proposalId, desposit="200000000usei", fees="200000usei", from=adminKeyName) { if(await isDocker()) { - await executeOnAllNodes(`seid tx gov vote ${proposalId} yes --from node_admin -b block -y --fees ${fees}`) + await executeOnAllNodes(`seid tx gov vote ${proposalId} yes --from node_admin -b sync -y --fees ${fees}`) } else { - await execute(`seid tx gov vote ${proposalId} yes --from ${from} -b block -y --fees ${fees}`) + await execute(`seid tx gov vote ${proposalId} yes --from ${from} -b sync -y --fees ${fees}`) } // Poll for proposal status with shorter delay for faster tests for(let i=0; i<200; i++) {