From 37c2e0403053266ffcd4749dacbf19e6ceed5c97 Mon Sep 17 00:00:00 2001 From: Wen Date: Fri, 1 May 2026 12:09:40 -0700 Subject: [PATCH 01/14] test(evm): use -b sync + sleep in lib.js cosmos-tx helpers for Autobahn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bankSend, fundSeiAddress, executeWasm, associateWasm all used -b block (broadcast-mode block), which subscribes to tendermint's EventDataTx for the submitted hash and waits up to 60s for it to fire. Under Autobahn the executeBlock path doesn't invoke FireEvents, so EventDataTx is never published and -b block hangs to the timeout, blocking every test that funds an account or runs a wasm message in beforeEach / before-all hooks. Switch to -b sync (returns on mempool acceptance) plus a fixed 2s sleep to let inclusion happen. Portable across both engines: under CometBFT this is slightly slower than the event-driven detection -b block had, but the difference is in the noise of the surrounding hardhat-ethers polling. Under Autobahn it unblocks the test paths entirely. Verified locally: EVMPrecompileTest's Bank, Addr, Distribution, Staking, Oracle precompile sections (which depend on getAdmin → fundSeiAddress) go from 0/all hung in before-all to 9/9 passing. Gov + Wasm precompile sections still fail because they have direct -b block calls inside the test files themselves; those need separate substitutions. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 73456b9918..033633eca2 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -102,13 +102,15 @@ async function evmSend(addr, fromKey, amount="10000000000000000000000000") { } 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 sleep(2000) 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 sleep(2000) + return result } async function getSeiBalance(seiAddr, denom="usei") { @@ -633,14 +635,16 @@ async function queryWasm(contractAddress, operation, args={}){ async function executeWasm(contractAddress, msg, coins = "0usei") { const jsonString = JSON.stringify(msg).replace(/"/g, '\\"'); // Properly escape JSON string - const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; + const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json`; const output = await execute(command); + await sleep(2000) return JSON.parse(output); } async function associateWasm(contractAddress) { - const command = `seid tx evm associate-contract-address ${contractAddress} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; + const command = `seid tx evm associate-contract-address ${contractAddress} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json`; const output = await execute(command); + await sleep(2000) return JSON.parse(output); } From 9d4305795a132d2192177ea2d60cdb7a5d9cf38b Mon Sep 17 00:00:00 2001 From: Wen Date: Fri, 1 May 2026 13:38:18 -0700 Subject: [PATCH 02/14] test(evm): also flip associateKey and passProposal vote to -b sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same migration as the prior commit, extended to two more sites that don't read post-execution event data: - associateKey: result is unused (wrapped in try/catch swallowing all errors). The -b block timeout was a 60s wait under Autobahn before the catch fired. - passProposal gov vote: caller polls proposal status afterward; the vote-tx return value is unused. Helpers that parse post-execution events from the response (storeWasm, proposeParamChange, registerPointerForERC*, etc.) stay on -b block in this PR — they need a separate "submit then poll for tx result" change. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 033633eca2..f4fc1a625c 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -162,8 +162,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 sleep(2000) }catch(e){ console.log("skipping associate") } @@ -524,9 +524,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++) { From b28bfcd23cfb5d9b3cbf0cb127885c175a42c695 Mon Sep 17 00:00:00 2001 From: Wen Date: Fri, 1 May 2026 13:59:38 -0700 Subject: [PATCH 03/14] test(evm): revert executeWasm/associateWasm to -b block CI on this PR caught a regression: CW1155toERC1155PointerTest's "should not transfer an NFT if not owned" reads res.code from executeWasm and expects it to be non-zero (the wasm execution should fail when the sender doesn't own the tokens). Under -b block, res.code is the DeliverTx code (post-execution); under -b sync it's the CheckTx code (mempool acceptance). For a tx that passes basic checks but fails in DeliverTx, -b sync returns 0 and the test wrongly thinks the tx succeeded. executeWasm and associateWasm have callers that read post-execution fields (code, events, logs) from the response, so they can't migrate to the -b sync pattern without also adding a "submit then poll for tx result" step. Revert these two; leave the truly response-agnostic helpers (bankSend, fundSeiAddress, associateKey, passProposal vote) on -b sync. The complex helpers are deferred to the same follow-up PR that addresses storeWasm, proposeParamChange, and the other event-parsing callers. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index f4fc1a625c..6333c1234c 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -635,16 +635,14 @@ async function queryWasm(contractAddress, operation, args={}){ async function executeWasm(contractAddress, msg, coins = "0usei") { const jsonString = JSON.stringify(msg).replace(/"/g, '\\"'); // Properly escape JSON string - const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json`; + const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --amount ${coins} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; const output = await execute(command); - await sleep(2000) return JSON.parse(output); } async function associateWasm(contractAddress) { - const command = `seid tx evm associate-contract-address ${contractAddress} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json`; + const command = `seid tx evm associate-contract-address ${contractAddress} --from ${adminKeyName} --gas=5000000 --fees=1000000usei -y --broadcast-mode block -o json`; const output = await execute(command); - await sleep(2000) return JSON.parse(output); } From b8ee5cf99f83f3bb53dfdcda4898e830e457c806 Mon Sep 17 00:00:00 2001 From: Wen Date: Fri, 1 May 2026 14:23:08 -0700 Subject: [PATCH 04/14] test(evm): replace fixed sleep(2000) with waitForBlocks(1) helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer flagged the magic 2s sleep after -b sync submissions: under CometBFT (block ~1s) it's marginal; under Autobahn (block ~100ms) it's 20x longer than needed. Both are arbitrary. Replace with waitForBlocks(1): poll eth_blockNumber every 50ms until the chain advances by one block, with a 15s timeout. Engine-agnostic substitute that returns as soon as the next block lands. Net wall-clock impact (Bank/Addr/Distribution precompile sections): - CometBFT: 19s → 15s - Autobahn: 39s → 10s Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 6333c1234c..1a30a5eb16 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -82,6 +82,21 @@ async function delay() { await sleep(1000) } +// Wait until the chain advances by `blocks` blocks. Used after `-b sync` +// submissions to give the next block time to include the tx — engine-agnostic +// substitute for a fixed sleep, since block times differ between CometBFT +// (~1s) and Autobahn (~100ms). +async function waitForBlocks(blocks=1, 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]) } @@ -103,13 +118,13 @@ async function evmSend(addr, fromKey, amount="10000000000000000000000000") { async function bankSend(toAddr, fromKey, amount="100000000000", denom="usei") { const result = await execute(`seid tx bank send ${fromKey} ${toAddr} ${amount}${denom} -b sync --fees 20000usei -y`); - await sleep(2000) + await waitForBlocks(1) return result } async function fundSeiAddress(seiAddr, amount="100000000000", denom="usei", funder=adminKeyName) { const result = await execute(`seid tx bank send ${funder} ${seiAddr} ${amount}${denom} -b sync --fees 20000usei -y`); - await sleep(2000) + await waitForBlocks(1) return result } @@ -163,7 +178,7 @@ async function getKeySeiAddress(name) { async function associateKey(keyName) { try { await execute(`seid tx evm associate-address --from ${keyName} -b sync`) - await sleep(2000) + await waitForBlocks(1) }catch(e){ console.log("skipping associate") } From a225d0e289b55af8b6b0541d25a10a6dd98e5b1f Mon Sep 17 00:00:00 2001 From: Wen Date: Fri, 1 May 2026 14:48:30 -0700 Subject: [PATCH 05/14] test(evm): bump waitForBlocks default to 2 to close empty-block race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on this PR caught a sequence mismatch in SeiSoloTest's "Claim CW20 Tester / before all": fundSeiAddress submits with -b sync and waitForBlocks(1) returns when the next block lands. That next block can be empty (the submitted tx is still in mempool and lands one block later), so the chain hasn't advanced the funder's account sequence yet. The follow-up storeWasm queries the funder's account, gets the stale sequence, signs with it, and is rejected when the funder tx finally lands a block later and bumps the sequence. Default to 2 blocks instead of 1: closes the empty-next-block race without reintroducing a fixed sleep. Callers no longer pass an explicit block count — the helper takes care of the safety margin. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 1a30a5eb16..5f864d69c5 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -83,10 +83,13 @@ async function delay() { } // Wait until the chain advances by `blocks` blocks. Used after `-b sync` -// submissions to give the next block time to include the tx — engine-agnostic +// 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). -async function waitForBlocks(blocks=1, timeoutMs=15000) { +// (~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) { @@ -118,13 +121,13 @@ async function evmSend(addr, fromKey, amount="10000000000000000000000000") { async function bankSend(toAddr, fromKey, amount="100000000000", denom="usei") { const result = await execute(`seid tx bank send ${fromKey} ${toAddr} ${amount}${denom} -b sync --fees 20000usei -y`); - await waitForBlocks(1) + await waitForBlocks() return result } async function fundSeiAddress(seiAddr, amount="100000000000", denom="usei", funder=adminKeyName) { const result = await execute(`seid tx bank send ${funder} ${seiAddr} ${amount}${denom} -b sync --fees 20000usei -y`); - await waitForBlocks(1) + await waitForBlocks() return result } @@ -178,7 +181,7 @@ async function getKeySeiAddress(name) { async function associateKey(keyName) { try { await execute(`seid tx evm associate-address --from ${keyName} -b sync`) - await waitForBlocks(1) + await waitForBlocks() }catch(e){ console.log("skipping associate") } From 94354ea4345eddd81dbe974531908c0685a6422f Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 15:38:51 -0700 Subject: [PATCH 06/14] test(evm): convert evmSend and proposeParamChange off -b block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more lib.js helpers were still using --broadcast-mode=block. Under Autobahn the executeBlock path doesn't fire EventDataTx, so -b block hangs to its 60s timeout on every call. evmSend (used by fundAddress, called from setupSigners and many test file before-hooks): convert to -b sync + waitForBlocks. Callers read only the txhash from the output, which is present in either format, so no other adjustment needed. This unblocks EVMPrecompileTest and EVMGigaTest's `transfer to non-existent contract with data` (latter verified passing in 126ms after the fix). proposeParamChange: convert to -b sync. The tx response under -b sync no longer carries deliver_tx events, so we can't read proposal_id from the submit_proposal event. Poll gov state instead (max-id-before vs max-id-after) — autobahn doesn't run a Cosmos-side tx indexer so seid q tx isn't an option. New maxProposalId helper handles the "no proposals exist yet" case explicitly via try/catch; using a shell `|| echo '{...}'` fallback breaks because the docker-exec wrapper double-quotes the inner command. Verified by EVMCompatabilityTest test #3 (`should reproduce mismatch by changing param`) passing in 47s after the fix (was timing out at 120s with -b block). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 55 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 5f864d69c5..2d2fa5b60c 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -115,7 +115,12 @@ 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() } @@ -446,19 +451,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) { From fbed3cfa734388ff1dc370a73f625c7a0d0a09b9 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 16:54:51 -0700 Subject: [PATCH 07/14] test(evm): convert createTokenFactoryTokenAndMint off -b block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last lib.js helper still using --broadcast-mode=block. Same Autobahn incompatibility as evmSend / proposeParamChange: executeBlock doesn't fire EventDataTx, so -b block hangs to its 60s timeout on every call, which blocked ERC20toNativePointerTest's before-all hook. The original code read the new denom from the create_denom event in the tx response. Under -b sync we don't get deliver_tx events, but the tokenfactory denom is deterministic — `factory//` where creator is the bech32 of the --from key. Construct it locally from getKeySeiAddress instead. Verified: ERC20toNativePointerTest passes 14/14 in 17s after the fix (previously 0/1, hung 60s+ on the before-all hook). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 2d2fa5b60c..82acf65961 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -268,16 +268,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() { From dc8c0d2f722fc921864dd1d4bbdbab98b4507140 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 17:53:41 -0700 Subject: [PATCH 08/14] test(evm): convert EVMPrecompileTest Gov before-hook off -b block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gov Precompile section's before-hook had its own inline 'seid tx gov submit-proposal param-change ... -b block' which hangs 60s under Autobahn. Replace with the lib.js proposeParamChange helper (now -b sync + poll gov state). EVMPrecompileTest goes from 9/11 to 10/11 — only the Wasm Precompile section still fails (out of scope; wasm being decommed). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/EVMPrecompileTest.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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'; From 1c8b8ee391cdd5c8440c21cd12c6a34253ef7ac7 Mon Sep 17 00:00:00 2001 From: Wen Date: Tue, 12 May 2026 19:14:49 -0700 Subject: [PATCH 09/14] test(evm): drop migration-history framing from lib.js comments Strip comments that described the old -b block behavior or contrasted -b sync against it. Reader of the current code doesn't need to know what was replaced; comments now describe pure invariants (empty-next-block race, deterministic denom format, diff-against-snapshot proposal identification, seid's empty-gov-set exit code). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/EVMPrecompileTest.js | 6 ++--- contracts/test/lib.js | 39 +++++------------------------ 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/contracts/test/EVMPrecompileTest.js b/contracts/test/EVMPrecompileTest.js index 1786ed566b..31f355d8be 100755 --- a/contracts/test/EVMPrecompileTest.js +++ b/contracts/test/EVMPrecompileTest.js @@ -125,10 +125,8 @@ describe("EVM Precompile Tester", function () { let govProposal; before(async function () { - // 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). + // Mirrors ../contracts/test/param_change_proposal.json, which is + // still consumed by integration_test/gov_module/gov_proposal_test.yaml. govProposal = await proposeParamChange( "Gov Param Change", "Update quorum to 0.45", diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 82acf65961..b57f2f9770 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -82,13 +82,8 @@ 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. +// Default 2 because the very next block after submit can be empty +// (tx still in mempool, lands one block later). async function waitForBlocks(blocks=2, timeoutMs=15000) { const start = await ethers.provider.getBlockNumber() const deadline = Date.now() + timeoutMs @@ -115,10 +110,6 @@ async function fundAddress(addr, amount="1000000000000000000000") { } async function evmSend(addr, fromKey, amount="10000000000000000000000000") { - // -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() @@ -268,11 +259,7 @@ async function rawHttpDebugTraceWithCallTracer(txHash) { } async function createTokenFactoryTokenAndMint(name, amount, recipient, from=adminKeyName) { - // -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. + // Tokenfactory denom is deterministic: factory//. const creator = await getKeySeiAddress(from); const token_denom = `factory/${creator}/${name}`; @@ -476,16 +463,9 @@ async function proposeParamChange(title, description, changes, deposit="20000000 const base64Json = Buffer.from(proposalJson).toString('base64'); await execute(`echo ${base64Json} | base64 -d > ${tempFile}`); - // 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. + // Identify the new proposal by diffing gov state before vs after submit. 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}`); @@ -494,9 +474,6 @@ async function proposeParamChange(title, description, changes, deposit="20000000 throw new Error(`Failed to submit proposal: ${response.raw_log}`); } - // 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(); @@ -510,12 +487,8 @@ async function proposeParamChange(title, description, changes, deposit="20000000 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. + // seid exits non-zero ("Error: no proposals found") on an empty + // gov set; the try/catch treats that as id=0. out = await execute(`seid q gov proposals --reverse --limit 1 -o json 2>/dev/null`); } catch (e) { return 0; From 63ab2116a2d99cdad110f9bbb9cb8eb49cb60c69 Mon Sep 17 00:00:00 2001 From: Wen Date: Tue, 12 May 2026 19:33:08 -0700 Subject: [PATCH 10/14] test(evm): load gov proposal fields from param_change_proposal.json Read title/description/changes/is_expedited from the JSON file directly rather than duplicating them inline. deposit/fees stay inline (they're not part of the proposal spec; they're CLI-level args). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/EVMPrecompileTest.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/contracts/test/EVMPrecompileTest.js b/contracts/test/EVMPrecompileTest.js index 31f355d8be..1ea20ba2d3 100755 --- a/contracts/test/EVMPrecompileTest.js +++ b/contracts/test/EVMPrecompileTest.js @@ -125,16 +125,15 @@ describe("EVM Precompile Tester", function () { let govProposal; before(async function () { - // Mirrors ../contracts/test/param_change_proposal.json, which is - // still consumed by integration_test/gov_module/gov_proposal_test.yaml. + const proposalSpec = require('./param_change_proposal.json'); govProposal = await proposeParamChange( - "Gov Param Change", - "Update quorum to 0.45", - [{ subspace: "gov", key: "tallyparams", value: { quorum: "0.45" } }], + proposalSpec.title, + proposalSpec.description, + proposalSpec.changes, "200000000usei", "20000usei", "admin", - false, // not expedited — matches the JSON file + proposalSpec.is_expedited, ); const signer = accounts[0].signer From b7ae4408aba54c6cc27c5e773f1de23814e34f26 Mon Sep 17 00:00:00 2001 From: Wen Date: Tue, 12 May 2026 19:35:59 -0700 Subject: [PATCH 11/14] test(evm): verify polled proposal title matches submitted in proposeParamChange After polling identifies the new proposal id, fetch the proposal and assert its title matches what we submitted. Closes the sharp edge where a concurrent submission (or DeliverTx silent failure followed by an unrelated submission) would otherwise cause us to return someone else's id. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index b57f2f9770..13ab9a341c 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -477,7 +477,14 @@ async function proposeParamChange(title, description, changes, deposit="20000000 const deadline = Date.now() + 30000; while (Date.now() < deadline) { const cur = await maxProposalId(); - if (cur > maxIdBefore) return String(cur); + if (cur > maxIdBefore) { + const detail = JSON.parse(await execute(`seid q gov proposal ${cur} -o json`)); + const observedTitle = detail.content?.title ?? detail.title; + if (observedTitle !== title) { + throw new Error(`proposal ${cur} title "${observedTitle}" does not match submitted "${title}" — concurrent submission?`); + } + return String(cur); + } await sleep(250); } throw new Error(`proposal submitted (tx ${response.txhash}) but did not appear in gov state within 30s`); From fd0d6e747f28bbe34d85897938b87f14edeb8799 Mon Sep 17 00:00:00 2001 From: Wen Date: Tue, 12 May 2026 19:44:32 -0700 Subject: [PATCH 12/14] test(evm): minimize diff in createTokenFactoryTokenAndMint Keep original command/mint_command/send_command variable names and inline-execute pattern. Only changes vs main are: derive token_denom locally instead of from the create_denom event, swap -b block for -b sync, and waitForBlocks between submissions. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 13ab9a341c..53d53f3c52 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -259,32 +259,19 @@ 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 sync -o json` + await execute(command); // Tokenfactory denom is deterministic: factory//. - 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 token_denom = `factory/${await getKeySeiAddress(from)}/${name}` + await waitForBlocks() + const mint_command = `seid tx tokenfactory mint ${amount}${token_denom} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json` + await execute(mint_command); + await waitForBlocks() - return token_denom; + const send_command = `seid tx bank send ${from} ${recipient} ${amount}${token_denom} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json` + await execute(send_command); + await waitForBlocks() + return token_denom } async function getChainId() { From f01e6d90f2799a57910a507dff10bd422382adcd Mon Sep 17 00:00:00 2001 From: Wen Date: Wed, 13 May 2026 15:24:40 -0700 Subject: [PATCH 13/14] test(evm): address Bugbot review on PR #3363 - createTokenFactoryTokenAndMint: check response.code on each sync submit; previously a CheckTx rejection would silently leave the function with a non-existent denom. Now matches proposeParamChange's pattern. - Drop redundant `await delay()` after helpers that already do waitForBlocks internally (fundAddress, getNativeAccount, setupSigners loop). The delay was carrying inclusion-wait duties; waitForBlocks inside the helper supersedes it. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 53d53f3c52..c057e33e5b 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -104,9 +104,7 @@ async function getEvmTx(provider, cosmosTxHash) { } async function fundAddress(addr, amount="1000000000000000000000") { - const result = await evmSend(addr, adminKeyName, amount) - await delay() - return result + return await evmSend(addr, adminKeyName, amount) } async function evmSend(addr, fromKey, amount="10000000000000000000000000") { @@ -157,7 +155,6 @@ async function getNativeAccount(keyName) { await associateKey(adminKeyName) const seiAddress = await getKeySeiAddress(keyName) await fundSeiAddress(seiAddress) - await delay() const evmAddress = await getEvmAddress(seiAddress) return { seiAddress, @@ -260,16 +257,19 @@ 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 sync -o json` - await execute(command); + const response = JSON.parse(await execute(command)) + if (response.code !== 0) throw new Error(`create-denom rejected: ${response.raw_log}`) // Tokenfactory denom is deterministic: factory//. const token_denom = `factory/${await getKeySeiAddress(from)}/${name}` await waitForBlocks() const mint_command = `seid tx tokenfactory mint ${amount}${token_denom} --from ${from} --gas=5000000 --fees=1000000usei -y --broadcast-mode sync -o json` - await execute(mint_command); + const mintResp = JSON.parse(await execute(mint_command)) + if (mintResp.code !== 0) throw new Error(`mint rejected: ${mintResp.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 sync -o json` - await execute(send_command); + const sendResp = JSON.parse(await execute(send_command)) + if (sendResp.code !== 0) throw new Error(`bank send rejected: ${sendResp.raw_log}`) await waitForBlocks() return token_denom } @@ -656,10 +656,8 @@ async function setupSigners(signers) { let seiAddress = await associateSigner(signer); if (seiAddress) { await fundSeiAddress(seiAddress); - await delay() } else { await fundAddress(evmAddress); - await delay() const resp = await signer.sendTransaction({ to: evmAddress, value: 0 From bd63691efbc610f1adde6d31ebd6758e8cc3657b Mon Sep 17 00:00:00 2001 From: Wen Date: Wed, 13 May 2026 15:35:56 -0700 Subject: [PATCH 14/14] test(evm): scan full new-id range in proposeParamChange polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If multiple proposals land in one polling interval (250ms), the loop must scan every id in (maxIdBefore, cur] to find ours by title — not just the max. Otherwise we'd throw on a title mismatch against a concurrent submission's proposal even when ours did land. Also keep polling (rather than erroring) if none of the new ids match our title; the 30s deadline catches the genuine "ours never landed" case. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/test/lib.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/test/lib.js b/contracts/test/lib.js index c057e33e5b..d291a49fa6 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -451,7 +451,7 @@ async function proposeParamChange(title, description, changes, deposit="20000000 await execute(`echo ${base64Json} | base64 -d > ${tempFile}`); // Identify the new proposal by diffing gov state before vs after submit. - const maxIdBefore = await maxProposalId(); + let maxIdBefore = await maxProposalId(); const command = `seid tx gov submit-proposal param-change ${tempFile} --from ${from} --fees ${fees} -y -o json -b sync`; const output = await execute(command); @@ -464,14 +464,12 @@ async function proposeParamChange(title, description, changes, deposit="20000000 const deadline = Date.now() + 30000; while (Date.now() < deadline) { const cur = await maxProposalId(); - if (cur > maxIdBefore) { - const detail = JSON.parse(await execute(`seid q gov proposal ${cur} -o json`)); + for (let id = maxIdBefore + 1; id <= cur; id++) { + const detail = JSON.parse(await execute(`seid q gov proposal ${id} -o json`)); const observedTitle = detail.content?.title ?? detail.title; - if (observedTitle !== title) { - throw new Error(`proposal ${cur} title "${observedTitle}" does not match submitted "${title}" — concurrent submission?`); - } - return String(cur); + if (observedTitle === title) return String(id); } + maxIdBefore = cur; await sleep(250); } throw new Error(`proposal submitted (tx ${response.txhash}) but did not appear in gov state within 30s`);