From 079906eeb486357cc90626dc41bbd26656f1f5d9 Mon Sep 17 00:00:00 2001 From: Luiz Date: Tue, 2 Jun 2026 12:25:50 -0300 Subject: [PATCH] fix(runtime): support view/staticCall on native ethers contracts + prime genesis on reachable TRE --- src/runtime/ethers-bridge.js | 44 ++++++++++++++++++++++++++++++++++++ src/tre/lifecycle.js | 7 ++++++ 2 files changed, 51 insertions(+) diff --git a/src/runtime/ethers-bridge.js b/src/runtime/ethers-bridge.js index 26035bc..e66df7a 100644 --- a/src/runtime/ethers-bridge.js +++ b/src/runtime/ethers-bridge.js @@ -239,6 +239,50 @@ const stubProvider = { } return '0x' + '0'.repeat(64); }, + // ethers v6 routes `view`/`pure` Contract methods (and explicit + // `.staticCall`) through `runner.call(tx)`. The facade path + // (deployContract / getContractAt) intercepts these itself, but a native + // `new ethers.Contract(addr, abi, provider)` — e.g. the test helper + // `forceDeployCode` layered over `setCode` — calls straight through to here. + // Forward the raw calldata to `/wallet/triggerconstantcontract` and return + // the raw result hex; ethers performs the ABI decode. Mirrors estimateGas. + async call(tx) { + const tw = this._tw(); + if (!tw) return '0x'; + const toAddr = lookupTvmActualBase58(tx.to) || signersMod.toBase58(tx.to); + const fromAddr = tx.from ? lookupTvmActualBase58(tx.from) || signersMod.toBase58(tx.from) : toAddr; + const data = typeof tx.data === 'string' && tx.data.startsWith('0x') ? tx.data.slice(2) : tx.data || ''; + const body = { + owner_address: TronWeb.address.toHex(fromAddr), + contract_address: TronWeb.address.toHex(toAddr), + data, + call_value: tx.value ? Number(tx.value) : 0, + visible: false, + }; + const url = tw.fullNode.host.replace(/\/$/, '') + '/wallet/triggerconstantcontract'; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + .then((r) => r.json()) + .catch(() => null); + // A successful constant call reports `result.result === true`; a revert + // sets `result.code`/`result.message` and puts the revert bytes in + // `constant_result[0]` (same detection as staticCallWithRevertData). + const reverted = res && res.result && (res.result.code || res.result.message); + const raw = res && res.constant_result && res.constant_result[0] ? '0x' + res.constant_result[0] : '0x'; + if (reverted) { + // Surface revert data on a CALL_EXCEPTION so ethers / chai-matchers can + // decode it (revertedWith / revertedWithCustomError) instead of + // mis-decoding a 4-byte error selector as return data. + const err = new Error('execution reverted (TVM constant call)'); + err.code = 'CALL_EXCEPTION'; + err.data = raw; + throw err; + } + return raw; + }, }; // -- Address conversion --------------------------------------------- diff --git a/src/tre/lifecycle.js b/src/tre/lifecycle.js index 4d7cc17..991437b 100644 --- a/src/tre/lifecycle.js +++ b/src/tre/lifecycle.js @@ -139,6 +139,13 @@ async function waitForReady(url, deadlineMs, log) { async function ensureUp(cfg, networkUrl, log = () => {}) { if (await isReachable(networkUrl)) { log(` TRE already reachable at ${networkUrl} (skipping spawn)`); + // We didn't spawn it, but a parallel-test runner (own container + TRE_URL) + // or a manual `tre:up` may have left the chain at genesis (block 0), where + // java-tron's proto3 omits the zero `number` field and the first + // CreateSmartContract deploy crashes in getCurrentRefBlockParams. Prime it + // the same way as the spawn path. Best-effort + idempotent: on an + // already-advanced chain this just mines one extra (harmless) block. + await primeGenesis(networkUrl, log); return { spawned: false, name: null, url: networkUrl }; }