Repository navigation
feat(truapi)!: prepare and execute EVM contract calls - #1873
Merged
Merged
Conversation
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.
Contributor
Bundle size reportCompared with
WebAssembly modules
Changed files (1)
Commit: bacccab |
Contributor
|
CI Status: 24 required jobs green, 22 passed and 2 skipped by path filter. All job results
Signing credentials: valid as of 2026-11-04 Commit |
pgherveou
marked this pull request as draft
November 4, 2026 06:10
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
marked this pull request as ready for review
November 4, 2026 14:03
pgherveou
enabled auto-merge
November 4, 2026 14:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contract writes are simulated as the signer into an opaque
PreparedEvmTransaction.execute_and_watchconsumes 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::readshares the private Revive codec throughRuntimeApi, preserving the transport's block context and typed contract reverts. Pinned dotNS reads remain consistent with their storage reads. The public contract interface isread,dry_runandexecute_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 aschain::account_to_h160. The wire protocol and JavaScript API are unchanged.Validation:
-D warnings, WASM check, rustdoc with-D warnings, and formatting passed.The two workspace packages that emit
uniffi-bindgenare built separately to avoid their existing output-name collision. Cargo also reports existing dependency future-compatibility notices forprimitive-typesandproc-macro-error2.