Skip to content

feat(truapi)!: prepare and execute EVM contract calls - #1873

Merged
pgherveou merged 24 commits into
mainfrom
pg/evm-contract-caller
Nov 4, 2026
Merged

pgherveou merged 24 commits into
mainfrom
pg/evm-contract-caller

Conversation

@pgherveou

@pgherveou pgherveou commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Contract writes are simulated as the signer into an opaque PreparedEvmTransaction. execute_and_watch consumes that request, maps the account when needed, chooses a fresh nonce and mortal era, and waits for successful finalized dispatch using the prepared limits. Execution rejects requests for another chain or a changed signer account before any side effects.

EvmContractCaller::read shares the private Revive codec through RuntimeApi, preserving the transport's block context and typed contract reverts. Pinned dotNS reads remain consistent with their storage reads. The public contract interface is read, dry_run and execute_and_watch; wire types, limits and mapping helpers stay private.

Breaking Rust API: contract callers use chain::{EvmContractCaller, EvmContractError, PreparedEvmTransaction} instead of the public dotNS Revive codec helpers. Address conversion is available as chain::account_to_h160. The wire protocol and JavaScript API are unchanged.

Validation:

  • Workspace tests with all features and targets: 2,765 passed, 34 ignored.
  • Twelve prepared-transaction regressions cover the simulated origin, unchanged limits, fresh nonce/mortality after mapping, changed signer and wrong chain, mapping failures, and finalized dispatch outcomes.
  • Pinned read regressions cover head movement and revert classification.
  • All-target builds, pinned-nightly clippy with -D warnings, WASM check, rustdoc with -D warnings, and formatting passed.
  • Codegen and generated CLI script types verified.
  • Workspace doctests: 4 passed, 3 ignored.

The two workspace packages that emit uniffi-bindgen are built separately to avoid their existing output-name collision. Cargo also reports existing dependency future-compatibility notices for primitive-types and proc-macro-error2.

Bind simulation to its signer, chain, calldata and limits through
submission. Keep Revive wire types and mapping internal.

BREAKING CHANGE: Rust contract callers use EvmContractCaller and its
prepared request instead of the public dotNS Revive codec helpers.
@pgherveou
pgherveou requested review from a team as code owners November 3, 2026 20:19
@github-actions github-actions Bot added size:XXL 1000 lines or more; split it, or say in the description why it cannot be status:waiting-on-review Ready for a reviewer labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Bundle size report

Compared with main at e92024d.

Raw Gzip Brotli
truapi-host 8.45 MiB (+22.4 KiB, +0.3%) 5.85 MiB (+8.8 KiB, +0.1%) 5.52 MiB (+6.3 KiB, +0.1%)
truapi-provider 4.04 MiB 1.27 MiB 961.4 KiB
truapi 7.89 MiB 1.10 MiB 916.8 KiB
trinity-stack 634.2 KiB 247.5 KiB 204.8 KiB
Total 21.01 MiB (+22.4 KiB, +0.1%) 8.47 MiB (+8.8 KiB, +0.1%) 7.55 MiB (+6.3 KiB, +0.1%)

WebAssembly modules

Raw Gzip Brotli
truapi-host/wasm/web/truapi_server_bg.wasm 3.34 MiB (+22.4 KiB, +0.7%) 1.16 MiB (+8.8 KiB, +0.7%) 883.8 KiB (+6.3 KiB, +0.7%)
truapi-host/wasm/web/truapi_verifiable_bg.wasm 4.89 MiB 4.64 MiB 4.61 MiB
truapi-provider/truapi_provider_bg.wasm 4.00 MiB 1.27 MiB 953.6 KiB
trinity-stack/wasm/trinity_stack_bg.wasm 564.4 KiB 230.5 KiB 190.0 KiB
Changed files (1)
Raw Gzip Brotli
truapi-host/wasm/web/truapi_server_bg.wasm 3.34 MiB (+22.4 KiB, +0.7%) 1.16 MiB (+8.8 KiB, +0.7%) 883.8 KiB (+6.3 KiB, +0.7%)

Commit: bacccab

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

CI Status: 24 required jobs green, 22 passed and 2 skipped by path filter.

All job results
job result
android-app success
android-bindings success
bundle-size success
changes success
changeset-guard success
cli-package success
codegen success
e2e skipped
explorer success
headless-install success
host-wasm success
ios-app success
ios-bindings success
licenses success
playground success
provider-android-bindings success
release-guard success
rust success
ts-client success
ts-debugger success
ts-host success
ts-trinity-stack success
wasm-provider success
workflow-lint skipped

Signing credentials: valid as of 2026-11-04

Commit bacccabf · run log

@pgherveou
pgherveou marked this pull request as draft November 4, 2026 06:10
@github-actions github-actions Bot removed the status:waiting-on-review Ready for a reviewer label Nov 4, 2026
Revive.AutoMap is true on every configured Asset Hub, so map_account is a no-op there and the runtime maps accounts when they are created.
PreparedEvmTransaction borrows the EvmContractCaller that simulated it and executes itself, so it carries no copy of the account or genesis to check and cannot reach another chain.
The trait shared its name with the protocol struct truapi::latest::RuntimeApi. The chain module doc now covers contract calls, whose transport selects the chain and block.
Weight needs no Clone or Copy, PreparedEvmTransaction and ReviveCallArgs no Debug. The Revive call arguments name every field at their construction sites.
A send error, a closed watch, Invalid, Dropped and Error now return NotFinalized with the MortalExtrinsic, so the caller can follow it until its era ends instead of signing a second write. A failed block number or outcome read after InFinalizedBlock returns OutcomeUnreadable with the finalized block hash, replacing Chain and OutcomeMissing there. The classification follows the durable tracker, which hands every one of these cases to recovery.
DispatchOutcome::Failed keeps the ExtrinsicFailed dispatch error, decoded with the block's metadata through subxt's DispatchError as bulletin_rpc does, and EvmContractError::DispatchFailed carries it as detail. An on-chain revert now reads Revive::ContractReverted, apart from OutOfGas and StorageDepositLimitExhausted. DispatchOutcome loses Copy; the durable tracker and ladder ignore the error, so no stored value changes. The README no longer claims EvmContractError separates on-chain reverts from failed execution.
revive.rs loses its inline tests: the caller tests check the same limits and call layout on the submitted extrinsic. Simulation and Weight drop the derives only those tests used; Weight keeps Default in test builds for the identity fixture. The dropped-watch test also asserts the dropped event (invalid and error stay pinned in chain/tests.rs), the missing-extrinsic row joins the unreadable-outcome test, and the failed row, the dispatch-event message row, the two failed dry_run tests and the malformed row go. The runtime rejection keeps one test. Named counters replace the positional tuples. In dotns_lookup the head-move test and its scripted provider go: ScriptedAssetHub asserts the follow and pinned block on every storage read and call, the identity test covers the revert, and a failing transport checks that EvmContractCaller::read never reports a runtime API failure as a revert.
ReviveApi_call maps an unmapped origin before simulating, so dry_run cannot see that a signer is unmapped on a runtime without AutoMap. Say so in dry_run's doc and the README, and say the prepared request borrows the caller and the signer, which is not the caller's own.
OutcomeUnreadable also covers a finalized block whose body lacks the extrinsic, so its doc and the README say to read the outcome at block or, when the block does not contain it, follow the extrinsic until its era ends. NotFinalized covers a failed send and says resubmitting the same bytes is safe while signing a new extrinsic is not; WatchEvent::Invalid says the extrinsic may still be included, as Dropped and Error do. Both Display messages name the extrinsic hash. An ExtrinsicFailed error that subxt cannot decode keeps the event's fields as 0x-hex in the detail. finalized_outcome takes the EncodedExtrinsic, so it has no adjacent hash parameters to swap with the block.
The unfinalized-write test gains a row where the node answers transactionWatch_v1_submitAndWatch with a JSON-RPC error, which must return NotFinalized with WatchEvent::Error and the submitted extrinsic. The unreadable-outcome test pins each row's cause, so the missing-extrinsic row fails on any other OutcomeUnreadable, and compares the whole MortalExtrinsic, era included.
…lusion

The scripted node now reports every extrinsic in a best block before its terminal event. The dropped row then fails if execution completes at best-block inclusion.
A DispatchError::Trie event, which subxt's DispatchError lacks, must still fail the write as DispatchFailed with the undecodable detail.
A dry run also ends in a DispatchError when the contract traps or exhausts a limit. EvmContractCaller::read runs on whatever chain its transport selects; only writes are bound to the caller's chain.
Nothing in chain uses it since the mapping step is gone; dotNS resolution and the identity test are its only callers.
NotFinalized carried a 72-byte MortalExtrinsic inline, which made EvmContractError 120 bytes, so any error wrapping it exceeds clippy's result_large_err limit. Boxed, EvmContractError is 88 bytes, the same pattern as the boxed RuntimeFailure in OutcomeUnreadable.
DotnsViewError renamed Reverted and flattened every other EvmContractError into a string, and every caller matched only Reverted. DotnsTransport::view and DotnsCallError::View now carry EvmContractError, and the chain-follow failure in DotnsLookup::pinned_to_best_block, which is not a contract call, becomes DotnsError::Follow. DotnsCallError and DotnsError drop PartialEq, which EvmContractError cannot derive. One table test of EvmContractCaller::read over a stub runtime replaces the two revert-classification tests and also covers Malformed output.
The method returns once a finalized block dispatched the write; it streams no progress.
@pgherveou
pgherveou marked this pull request as ready for review November 4, 2026 14:03
@github-actions github-actions Bot added the status:waiting-on-review Ready for a reviewer label Nov 4, 2026

@TarikGul TarikGul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@pgherveou
pgherveou enabled auto-merge November 4, 2026 14:14
@pgherveou
pgherveou added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 922fca5 Nov 4, 2026
49 checks passed
@pgherveou
pgherveou deleted the pg/evm-contract-caller branch November 4, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1000 lines or more; split it, or say in the description why it cannot be status:waiting-on-review Ready for a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants