diff --git a/Cargo.lock b/Cargo.lock index 602891892a2b..8c3119464b96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -1150,6 +1165,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compression" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd 0.13.0", + "zstd-safe 7.0.0", +] + [[package]] name = "async-executor" version = "1.5.1" @@ -2582,6 +2613,27 @@ dependencies = [ "tuplex", ] +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bs58" version = "0.5.1" @@ -7418,6 +7470,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" +[[package]] +name = "http-range-header" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" + [[package]] name = "httparse" version = "1.8.0" @@ -7897,6 +7955,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "iri-string" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0f0a572e8ffe56e2ff4f769f32ffe919282c3916799f8b68688b6030063bea" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.9" @@ -9680,6 +9748,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -12469,6 +12547,41 @@ dependencies = [ "subxt-signer", ] +[[package]] +name = "pallet-revive-eth-rpc" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.13", + "env_logger 0.11.3", + "futures", + "hex", + "hex-literal", + "hyper 1.3.1", + "jsonrpsee 0.24.3", + "log", + "pallet-revive", + "pallet-revive-fixtures", + "parity-scale-codec", + "rlp 0.6.1", + "sc-rpc", + "scale-info", + "secp256k1", + "serde_json", + "sp-core 28.0.0", + "sp-crypto-hashing 0.1.0", + "sp-runtime 31.0.1", + "sp-weights 27.0.0", + "substrate-cli-test-utils", + "subxt", + "subxt-signer", + "thiserror", + "tokio", + "tower", + "tower-http 0.5.2", + "tracing-subscriber 0.3.18", +] + [[package]] name = "pallet-revive-fixtures" version = "0.1.0" @@ -15392,6 +15505,7 @@ dependencies = [ "pallet-referenda", "pallet-remark", "pallet-revive", + "pallet-revive-eth-rpc", "pallet-revive-fixtures", "pallet-revive-mock-network", "pallet-revive-proc-macro", @@ -24198,9 +24312,9 @@ dependencies = [ [[package]] name = "subxt-core" -version = "0.37.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f41eb2e2eea6ed45649508cc735f92c27f1fcfb15229e75f8270ea73177345" +checksum = "3af3b36405538a36b424d229dc908d1396ceb0994c90825ce928709eac1a159a" dependencies = [ "base58", "blake2 0.10.6", @@ -24726,9 +24840,9 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] @@ -24755,9 +24869,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", @@ -25143,7 +25257,7 @@ dependencies = [ "futures-util", "http 0.2.9", "http-body 0.4.5", - "http-range-header", + "http-range-header 0.3.1", "mime", "pin-project-lite", "tower-layer", @@ -25157,14 +25271,29 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ + "async-compression", + "base64 0.21.7", "bitflags 2.6.0", "bytes", + "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "http-body-util", + "http-range-header 0.4.1", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", + "tower", "tower-layer", "tower-service", + "tracing", + "uuid", ] [[package]] @@ -25581,6 +25710,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -27421,6 +27559,15 @@ dependencies = [ "zstd-safe 6.0.6", ] +[[package]] +name = "zstd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +dependencies = [ + "zstd-safe 7.0.0", +] + [[package]] name = "zstd-safe" version = "5.0.2+zstd.1.5.2" @@ -27441,6 +27588,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.8+zstd.1.5.5" diff --git a/Cargo.toml b/Cargo.toml index 6ba91de3c09b..e61aab2adae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -402,6 +402,7 @@ members = [ "substrate/frame/revive/fixtures", "substrate/frame/revive/mock-network", "substrate/frame/revive/proc-macro", + "substrate/frame/revive/rpc", "substrate/frame/revive/uapi", "substrate/frame/root-offences", "substrate/frame/root-testing", @@ -967,6 +968,7 @@ pallet-recovery = { path = "substrate/frame/recovery", default-features = false pallet-referenda = { path = "substrate/frame/referenda", default-features = false } pallet-remark = { default-features = false, path = "substrate/frame/remark" } pallet-revive = { path = "substrate/frame/revive", default-features = false } +pallet-revive-eth-rpc = { path = "substrate/frame/revive/rpc", default-features = false } pallet-revive-fixtures = { path = "substrate/frame/revive/fixtures", default-features = false } pallet-revive-mock-network = { default-features = false, path = "substrate/frame/revive/mock-network" } pallet-revive-proc-macro = { path = "substrate/frame/revive/proc-macro", default-features = false } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 9a60de77a584..5fbf0d33d576 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -58,7 +58,7 @@ use frame_system::{ }; use pallet_asset_conversion_tx_payment::SwapAssetAdapter; use pallet_nfts::{DestroyWitness, PalletFeatures}; -use pallet_revive::evm::runtime::EthExtra; +use pallet_revive::{evm::runtime::EthExtra, AddressMapper}; use parachains_common::{ impls::DealWithFees, message_queue::*, AccountId, AssetIdForTrustBackedAssets, AuraId, Balance, BlockNumber, CollectionId, Hash, Header, ItemId, Nonce, Signature, AVERAGE_ON_INITIALIZE_RATIO, @@ -2069,8 +2069,17 @@ impl_runtime_apis! { } } - impl pallet_revive::ReviveApi for Runtime + impl pallet_revive::ReviveApi for Runtime { + fn balance(address: H160) -> Balance { + let account = ::AddressMapper::to_account_id(&address); + Balances::usable_balance(account) + } + + fn nonce(address: H160) -> Nonce { + let account = ::AddressMapper::to_account_id(&address); + System::account_nonce(account) + } fn eth_transact( from: H160, dest: Option, diff --git a/prdoc/pr_6147.prdoc b/prdoc/pr_6147.prdoc new file mode 100644 index 000000000000..eef0d0936675 --- /dev/null +++ b/prdoc/pr_6147.prdoc @@ -0,0 +1,17 @@ +title: "[pallet-revive] Ethereum JSON-RPC" + +doc: + - audience: Runtime Dev + description: | + Add a new Ethereum JSON-RPC server that can be used a substrate chain configured with pallet-revive +crates: + - name: pallet-revive + bump: patch + - name: asset-hub-westend-runtime + bump: patch + - name: pallet-revive-eth-rpc + bump: patch + - name: pallet-revive-fixtures + bump: patch + - name: polkadot-sdk + bump: patch diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index d6a17856e470..d5b384632a37 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -76,7 +76,7 @@ use pallet_identity::legacy::IdentityInfo; use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use pallet_nfts::PalletFeatures; use pallet_nis::WithMaximumOf; -use pallet_revive::evm::runtime::EthExtra; +use pallet_revive::{evm::runtime::EthExtra, AddressMapper}; use pallet_session::historical as pallet_session_historical; // Can't use `FungibleAdapter` here until Treasury pallet migrates to fungibles // @@ -3157,8 +3157,18 @@ impl_runtime_apis! { } } - impl pallet_revive::ReviveApi for Runtime + impl pallet_revive::ReviveApi for Runtime { + fn balance(address: H160) -> Balance { + let account = ::AddressMapper::to_account_id(&address); + Balances::usable_balance(account) + } + + fn nonce(address: H160) -> Nonce { + let account = ::AddressMapper::to_account_id(&address); + System::account_nonce(account) + } + fn eth_transact( from: H160, dest: Option, diff --git a/substrate/frame/revive/fixtures/contracts/rpc_demo.rs b/substrate/frame/revive/fixtures/contracts/rpc_demo.rs new file mode 100644 index 000000000000..0d75c6eb8df6 --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/rpc_demo.rs @@ -0,0 +1,36 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] +#![no_main] + +use common::input; +use uapi::{HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() { + input!(128, data: [u8],); + api::deposit_event(&[], data); +} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!(128, data: [u8],); + api::deposit_event(&[], data); +} diff --git a/substrate/frame/revive/rpc/Cargo.toml b/substrate/frame/revive/rpc/Cargo.toml new file mode 100644 index 000000000000..3c05bdeef478 --- /dev/null +++ b/substrate/frame/revive/rpc/Cargo.toml @@ -0,0 +1,83 @@ +[package] +name = "pallet-revive-eth-rpc" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +description = "An Ethereum JSON-RPC server for pallet-revive." + +[[bin]] +name = "eth-rpc" +path = "src/main.rs" + +[[example]] +name = "deploy" +path = "examples/rust/deploy.rs" +required-features = ["example", "riscv"] + +[[example]] +name = "transfer" +path = "examples/rust/transfer.rs" +required-features = ["example", "riscv"] + +[[example]] +name = "rpc-playground" +path = "examples/rust/rpc-playground.rs" +required-features = ["example", "riscv"] + +[[example]] +name = "extrinsic" +path = "examples/rust/extrinsic.rs" +required-features = ["example", "riscv"] + +[[example]] +name = "remark-extrinsic" +path = "examples/rust/remark-extrinsic.rs" +required-features = ["example", "riscv"] + +[dependencies] +clap = { workspace = true, features = ["derive"] } +anyhow = { workspace = true } +futures = { workspace = true, features = ["thread-pool"] } +jsonrpsee = { workspace = true, features = ["full"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +sp-crypto-hashing = { workspace = true } +subxt = { workspace = true, default-features = true, features = [ + "unstable-reconnecting-rpc-client", +] } +tokio = { workspace = true, features = ["full"] } +codec = { workspace = true, features = ["derive"] } +log.workspace = true +tracing-subscriber.workspace = true +pallet-revive = { workspace = true, default-features = true } +sp-core = { workspace = true, default-features = true } +sp-weights = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } +sc-rpc = { workspace = true, default-features = true } + +tower.workspace = true +tower-http = { workspace = true, features = ["full"] } +hyper.workspace = true + +rlp = { workspace = true, optional = true } +subxt-signer = { workspace = true, optional = true, features = [ + "unstable-eth", +] } +hex = { workspace = true, optional = true } +hex-literal = { workspace = true, optional = true } +scale-info = { workspace = true } +secp256k1 = { workspace = true, optional = true, features = ["recovery"] } +env_logger = { workspace = true } + +[features] +dev = [] +example = ["hex", "hex-literal", "rlp", "secp256k1", "subxt-signer"] +riscv = ["pallet-revive/riscv"] + +[dev-dependencies] +hex-literal = { workspace = true } +pallet-revive-fixtures = { workspace = true } +substrate-cli-test-utils = { workspace = true } diff --git a/substrate/frame/revive/rpc/examples/README.md b/substrate/frame/revive/rpc/examples/README.md new file mode 100644 index 000000000000..ebb17fabb724 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/README.md @@ -0,0 +1,84 @@ +## Pre-requisites + + Build `pallet-revive-fixture`, as we need some compiled contracts to exercise the RPC server. + +```bash +cargo build -p pallet-revive-fixtures --features riscv +``` + +## Start the node + +Start the kitchensink node: + +```bash +RUST_LOG="error,evm=debug,sc_rpc_server=info,runtime::revive=debug" cargo run --bin substrate-node -- --dev +``` + +## Start the RPC server + +This command starts the Ethereum JSON-RPC server, which runs on `localhost:9090` by default: + +```bash +RUST_LOG="info,eth-rpc=debug" cargo run -p pallet-revive-eth-rpc --features dev +``` + +## Rust examples + +Run one of the examples from the `examples` directory to send a transaction to the node: + +```bash +RUST_LOG="info,eth-rpc=debug" cargo run -p pallet-revive-eth-rpc --features example --example deploy +``` + +## JS examples + +Interact with the node using MetaMask & Ether.js, by starting the example web app: + +```bash + +cd substrate/frame/revive/rpc/examples/js +bun install +bun run dev +``` + +Alternatively, you can run the example script directly: + +```bash +cd substrate/frame/revive/rpc/examples/js +bun src/script.ts +``` + +### Configure MetaMask + +You can use the following instructions to setup [MetaMask] with the local chain. + +> **Note**: When you interact with MetaMask and restart the chain, you need to clear the activity tab (Settings > +Advanced > Clear activity tab data), and in some cases lock/unlock MetaMask to reset the nonce. +See [this guide][reset-account] for more info on how to reset the account activity. + +#### Add a new network + +To interact with the local chain, add a new network in [MetaMask]. +See [this guide][add-network] for more info on how to add a custom network. + +Make sure the node and the RPC server are started, and use the following settings to configure the network +(MetaMask > Networks > Add a network manually): + +- Network name: KitchenSink +- RPC URL: +- Chain ID: 420420420 +- Currency Symbol: `DEV` + +#### Import Dev account + +You will need to import the following account, endowed with some balance at genesis, to interact with the chain. +See [this guide][import-account] for more info on how to import an account. + +- Account: `0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac` +- Private Key: `5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133` + +[MetaMask]: https://metamask.io +[add-network]: https://support.metamask.io/networks-and-sidechains/managing-networks/how-to-add-a-custom-network-rpc/#adding-a-network-manually +[import-account]: https://support.metamask.io/managing-my-wallet/accounts-and-addresses/how-to-import-an-account/ +[reset-account]: https://support.metamask.io/managing-my-wallet/resetting-deleting-and-restoring/how-to-clear-your-account-activity-reset-account + diff --git a/substrate/frame/revive/rpc/examples/js/.gitignore b/substrate/frame/revive/rpc/examples/js/.gitignore new file mode 100644 index 000000000000..a547bf36d8d1 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/substrate/frame/revive/rpc/examples/js/bun.lockb b/substrate/frame/revive/rpc/examples/js/bun.lockb new file mode 100755 index 000000000000..8bf47d7eb8b8 Binary files /dev/null and b/substrate/frame/revive/rpc/examples/js/bun.lockb differ diff --git a/substrate/frame/revive/rpc/examples/js/index.html b/substrate/frame/revive/rpc/examples/js/index.html new file mode 100644 index 000000000000..72f7026ce5f3 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/index.html @@ -0,0 +1,29 @@ + + + + + + + MetaMask Playground + + + + + + + + + + + + + + + diff --git a/substrate/frame/revive/rpc/examples/js/package.json b/substrate/frame/revive/rpc/examples/js/package.json new file mode 100644 index 000000000000..4d7136606b65 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/package.json @@ -0,0 +1,18 @@ +{ + "name": "demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "ethers": "^6.13.1" + }, + "devDependencies": { + "typescript": "^5.5.3", + "vite": "^5.4.8" + } +} diff --git a/substrate/frame/revive/rpc/examples/js/src/main.ts b/substrate/frame/revive/rpc/examples/js/src/main.ts new file mode 100644 index 000000000000..88b72755aae9 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/src/main.ts @@ -0,0 +1,141 @@ +import { + AddressLike, + BrowserProvider, + Contract, + ContractFactory, + Eip1193Provider, + JsonRpcSigner, + parseEther, +} from "ethers"; + +declare global { + interface Window { + ethereum?: Eip1193Provider; + } +} + +function str_to_bytes(str: string): Uint8Array { + return new TextEncoder().encode(str); +} + +document.addEventListener("DOMContentLoaded", async () => { + if (typeof window.ethereum == "undefined") { + return console.log("MetaMask is not installed"); + } + + console.log("MetaMask is installed!"); + const provider = new BrowserProvider(window.ethereum); + + console.log("Getting signer..."); + let signer: JsonRpcSigner; + try { + signer = await provider.getSigner(); + console.log(`Signer: ${signer.address}`); + } catch (e) { + console.error("Failed to get signer", e); + return; + } + + console.log("Getting block number..."); + try { + const blockNumber = await provider.getBlockNumber(); + console.log(`Block number: ${blockNumber}`); + } catch (e) { + console.error("Failed to get block number", e); + return; + } + + const nonce = await signer.getNonce(); + console.log(`Nonce: ${nonce}`); + + document.getElementById("transferButton")?.addEventListener( + "click", + async () => { + const address = + (document.getElementById("transferInput") as HTMLInputElement).value; + await transfer(address); + }, + ); + + document.getElementById("deployButton")?.addEventListener( + "click", + async () => { + await deploy(); + }, + ); + document.getElementById("deployAndCallButton")?.addEventListener( + "click", + async () => { + const nonce = await signer.getNonce(); + console.log(`deploy with nonce: ${nonce}`); + + const address = await deploy(); + if (address) { + const nonce = await signer.getNonce(); + console.log(`call with nonce: ${nonce}`); + await call(address); + } + }, + ); + document.getElementById("callButton")?.addEventListener("click", async () => { + const address = + (document.getElementById("callInput") as HTMLInputElement).value; + await call(address); + }); + + async function deploy() { + console.log("Deploying contract..."); + + const bytecode = await fetch("rpc_demo.polkavm").then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.arrayBuffer(); + }) + .then((arrayBuffer) => new Uint8Array(arrayBuffer)); + + const contractFactory = new ContractFactory( + [ + "constructor(bytes memory _data)", + ], + bytecode, + signer, + ); + + try { + const args = str_to_bytes("hello"); + const contract = await contractFactory.deploy(args); + await contract.waitForDeployment(); + const address = await contract.getAddress(); + console.log(`Contract deployed: ${address}`); + return address; + } catch (e) { + console.error("Failed to deploy contract", e); + return; + } + } + + async function call(address: string) { + const abi = ["function call(bytes data)"]; + const contract = new Contract(address, abi, signer); + const tx = await contract.call(str_to_bytes("world")); + + console.log("Transaction hash:", tx.hash); + } + + async function transfer(to: AddressLike) { + console.log(`transferring 1 DOT to ${to}...`); + try { + const tx = await signer.sendTransaction({ + to, + value: parseEther("1.0"), + }); + + const receipt = await tx.wait(); + console.log(`Transaction hash: ${receipt?.hash}`); + } catch (e) { + console.error("Failed to send transaction", e); + return; + } + } +}); diff --git a/substrate/frame/revive/rpc/examples/js/src/script.ts b/substrate/frame/revive/rpc/examples/js/src/script.ts new file mode 100644 index 000000000000..70131a2f17d3 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/src/script.ts @@ -0,0 +1,49 @@ +//! Run with bun run script.ts + +import { readFileSync } from "fs"; +import { Contract, ContractFactory, JsonRpcProvider } from "ethers"; + +const provider = new JsonRpcProvider("http://localhost:9090"); +const signer = await provider.getSigner(); +console.log( + `Signer address: ${await signer.getAddress()}, Nonce: ${await signer + .getNonce()}`, +); + +function str_to_bytes(str: string): Uint8Array { + return new TextEncoder().encode(str); +} + +// deploy +async function deploy() { + console.log(`Deploying Contract...`); + + const bytecode = readFileSync("rpc_demo.polkavm"); + const contractFactory = new ContractFactory( + [ + "constructor(bytes memory _data)", + ], + bytecode, + signer, + ); + + const args = str_to_bytes("hello"); + console.log("Deploying contract with args:", args); + const contract = await contractFactory.deploy(args); + await contract.waitForDeployment(); + const address = await contract.getAddress(); + console.log(`Contract deployed: ${address}`); + return address; +} + +async function call(address: string) { + console.log(`Calling Contract at ${address}...`); + + const abi = ["function call(bytes data)"]; + const contract = new Contract(address, abi, signer); + const tx = await contract.call(str_to_bytes("world")); + console.log("Call transaction hash:", tx.hash); +} + +const address = await deploy(); +await call(address); diff --git a/substrate/frame/revive/rpc/examples/js/tsconfig.json b/substrate/frame/revive/rpc/examples/js/tsconfig.json new file mode 100644 index 000000000000..0511b9f0e041 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/substrate/frame/revive/rpc/examples/rpc_demo.polkavm b/substrate/frame/revive/rpc/examples/rpc_demo.polkavm new file mode 120000 index 000000000000..63925dfcc544 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/rpc_demo.polkavm @@ -0,0 +1 @@ +../../../../../target/pallet-revive-fixtures/rpc_demo.polkavm \ No newline at end of file diff --git a/substrate/frame/revive/rpc/examples/rust/deploy.rs b/substrate/frame/revive/rpc/examples/rust/deploy.rs new file mode 100644 index 000000000000..225d44b14c8c --- /dev/null +++ b/substrate/frame/revive/rpc/examples/rust/deploy.rs @@ -0,0 +1,73 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use jsonrpsee::http_client::HttpClientBuilder; +use pallet_revive::{ + create1, + evm::{Account, BlockTag, Bytes, ReceiptInfo, U256}, +}; +use pallet_revive_eth_rpc::{ + example::{send_transaction, wait_for_receipt}, + EthRpcClient, +}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + let account = Account::default(); + let data = vec![]; + let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?; + let input = bytes.into_iter().chain(data.clone()).collect::>(); + + println!("Account:"); + println!("- address: {:?}", account.address()); + println!("- substrate: {}", account.substrate_account()); + let client = HttpClientBuilder::default().build("http://localhost:9090")?; + + println!("\n\n=== Deploying contract ===\n\n"); + + let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?; + let hash = + send_transaction(&account, &client, 5_000_000_000_000u128.into(), input.into(), None) + .await?; + + println!("Deploy Tx hash: {hash:?}"); + let ReceiptInfo { block_number, gas_used, contract_address, .. } = + wait_for_receipt(&client, hash).await?; + + let contract_address = contract_address.unwrap(); + assert_eq!(contract_address, create1(&account.address(), nonce.try_into().unwrap())); + + println!("Receipt:"); + println!("- Block number: {block_number}"); + println!("- Gas used: {gas_used}"); + println!("- Contract address: {contract_address:?}"); + let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?; + println!("- Contract balance: {balance:?}"); + + println!("\n\n=== Calling contract ===\n\n"); + let hash = + send_transaction(&account, &client, U256::zero(), Bytes::default(), Some(contract_address)) + .await?; + + println!("Contract call tx hash: {hash:?}"); + let ReceiptInfo { block_number, gas_used, to, .. } = wait_for_receipt(&client, hash).await?; + println!("Receipt:"); + println!("- Block number: {block_number}"); + println!("- Gas used: {gas_used}"); + println!("- To: {to:?}"); + Ok(()) +} diff --git a/substrate/frame/revive/rpc/examples/rust/extrinsic.rs b/substrate/frame/revive/rpc/examples/rust/extrinsic.rs new file mode 100644 index 000000000000..e15743e2385e --- /dev/null +++ b/substrate/frame/revive/rpc/examples/rust/extrinsic.rs @@ -0,0 +1,54 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use pallet_revive_eth_rpc::subxt_client::{ + self, revive::calls::types::InstantiateWithCode, SrcChainConfig, +}; +use sp_weights::Weight; +use subxt::OnlineClient; +use subxt_signer::sr25519::dev; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = OnlineClient::::new().await?; + + let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?; + + let tx_payload = subxt_client::tx().revive().instantiate_with_code( + 0u32.into(), + Weight::from_parts(100_000, 0).into(), + 3_000_000_000_000_000_000, + bytes, + vec![], + None, + ); + + let res = client + .tx() + .sign_and_submit_then_watch_default(&tx_payload, &dev::alice()) + .await? + .wait_for_finalized() + .await?; + println!("Transaction finalized: {:?}", res.extrinsic_hash()); + + let block_hash = res.block_hash(); + + let block = client.blocks().at(block_hash).await.unwrap(); + let extrinsics = block.extrinsics().await.unwrap(); + let _ = extrinsics.find_first::()?; + + Ok(()) +} diff --git a/substrate/frame/revive/rpc/examples/rust/remark-extrinsic.rs b/substrate/frame/revive/rpc/examples/rust/remark-extrinsic.rs new file mode 100644 index 000000000000..b106d27c218a --- /dev/null +++ b/substrate/frame/revive/rpc/examples/rust/remark-extrinsic.rs @@ -0,0 +1,43 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use pallet_revive_eth_rpc::subxt_client::{self, system::calls::types::Remark, SrcChainConfig}; +use subxt::OnlineClient; +use subxt_signer::sr25519::dev; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = OnlineClient::::new().await?; + let tx_payload = subxt_client::tx().system().remark(b"bonjour".to_vec()); + let res = client + .tx() + .sign_and_submit_then_watch_default(&tx_payload, &dev::alice()) + .await? + .wait_for_finalized() + .await?; + + println!("Transaction finalized: {:?}", res.extrinsic_hash()); + let block_hash = res.block_hash(); + let block = client.blocks().at(block_hash).await.unwrap(); + let extrinsics = block.extrinsics().await.unwrap(); + let remarks = extrinsics + .find::() + .map(|remark| remark.unwrap().value) + .collect::>(); + + dbg!(remarks); + Ok(()) +} diff --git a/substrate/frame/revive/rpc/examples/rust/rpc-playground.rs b/substrate/frame/revive/rpc/examples/rust/rpc-playground.rs new file mode 100644 index 000000000000..635fbb74d5c2 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/rust/rpc-playground.rs @@ -0,0 +1,41 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use jsonrpsee::http_client::HttpClientBuilder; +use pallet_revive::evm::{Account, BlockTag}; +use pallet_revive_eth_rpc::EthRpcClient; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let account = Account::default(); + println!("Account address: {:?}", account.address()); + + let client = HttpClientBuilder::default().build("http://localhost:9090")?; + + let block = client.get_block_by_number(BlockTag::Latest.into(), false).await?; + println!("Latest block: {block:#?}"); + + let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?; + println!("Account nonce: {nonce:?}"); + + let balance = client.get_balance(account.address(), BlockTag::Latest.into()).await?; + println!("Account balance: {balance:?}"); + + let sync_state = client.syncing().await?; + println!("Sync state: {sync_state:?}"); + + Ok(()) +} diff --git a/substrate/frame/revive/rpc/examples/rust/transfer.rs b/substrate/frame/revive/rpc/examples/rust/transfer.rs new file mode 100644 index 000000000000..7c6f69f3b6a7 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/rust/transfer.rs @@ -0,0 +1,55 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use jsonrpsee::http_client::HttpClientBuilder; +use pallet_revive::evm::{Account, BlockTag, Bytes, ReceiptInfo}; +use pallet_revive_eth_rpc::{ + example::{send_transaction, wait_for_receipt}, + EthRpcClient, +}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let alith = Account::default(); + let client = HttpClientBuilder::default().build("http://localhost:9090")?; + + let baltathar = Account::from(subxt_signer::eth::dev::baltathar()); + let value = 1_000_000_000_000_000_000u128.into(); // 1 ETH + + let print_balance = || async { + let balance = client.get_balance(alith.address(), BlockTag::Latest.into()).await?; + println!("Alith {:?} balance: {balance:?}", alith.address()); + let balance = client.get_balance(baltathar.address(), BlockTag::Latest.into()).await?; + println!("Baltathar {:?} balance: {balance:?}", baltathar.address()); + anyhow::Result::<()>::Ok(()) + }; + + print_balance().await?; + println!("\n\n=== Transferring ===\n\n"); + + let hash = + send_transaction(&alith, &client, value, Bytes::default(), Some(baltathar.address())) + .await?; + println!("Transaction hash: {hash:?}"); + + let ReceiptInfo { block_number, gas_used, .. } = wait_for_receipt(&client, hash).await?; + println!("Receipt: "); + println!("- Block number: {block_number}"); + println!("- Gas used: {gas_used}"); + + print_balance().await?; + Ok(()) +} diff --git a/substrate/frame/revive/rpc/src/cli.rs b/substrate/frame/revive/rpc/src/cli.rs new file mode 100644 index 000000000000..426554fd0318 --- /dev/null +++ b/substrate/frame/revive/rpc/src/cli.rs @@ -0,0 +1,131 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! The Ethereum JSON-RPC server. +use crate::{client::Client, EthRpcClient, EthRpcServer, EthRpcServerImpl, LOG_TARGET}; +use clap::Parser; +use hyper::Method; +use jsonrpsee::{ + http_client::HttpClientBuilder, + server::{RpcModule, Server}, +}; +use std::net::SocketAddr; +use tower_http::cors::{Any, CorsLayer}; + +// Parsed command instructions from the command line +#[derive(Parser)] +#[clap(author, about, version)] +pub struct CliCommand { + /// The server address to bind to + #[clap(long, default_value = "9090")] + pub rpc_port: String, + + /// The node url to connect to + #[clap(long, default_value = "ws://127.0.0.1:9944")] + pub node_rpc_url: String, +} + +/// Run the JSON-RPC server. +pub async fn run(cmd: CliCommand) -> anyhow::Result<()> { + let CliCommand { rpc_port, node_rpc_url } = cmd; + let client = Client::from_url(&node_rpc_url).await?; + let mut updates = client.updates.clone(); + + let server_addr = run_server(client, &format!("127.0.0.1:{rpc_port}")).await?; + log::info!("Running JSON-RPC server: addr={server_addr}"); + + let url = format!("http://{}", server_addr); + let client = HttpClientBuilder::default().build(url)?; + + let block_number = client.block_number().await?; + log::info!(target: LOG_TARGET, "Client initialized - Current 📦 block: #{block_number:?}"); + + // keep running server until ctrl-c or client subscription fails + let _ = updates.wait_for(|_| false).await; + Ok(()) +} + +#[cfg(feature = "dev")] +mod dev { + use crate::LOG_TARGET; + use futures::{future::BoxFuture, FutureExt}; + use jsonrpsee::{server::middleware::rpc::RpcServiceT, types::Request, MethodResponse}; + + /// Dev Logger middleware, that logs the method and params of the request, along with the + /// success of the response. + #[derive(Clone)] + pub struct DevLogger(pub S); + + impl<'a, S> RpcServiceT<'a> for DevLogger + where + S: RpcServiceT<'a> + Send + Sync + Clone + 'static, + { + type Future = BoxFuture<'a, MethodResponse>; + + fn call(&self, req: Request<'a>) -> Self::Future { + let service = self.0.clone(); + let method = req.method.clone(); + let params = req.params.clone().unwrap_or_default(); + + async move { + log::info!(target: LOG_TARGET, "Method: {method} params: {params}"); + let resp = service.call(req).await; + if resp.is_success() { + log::info!(target: LOG_TARGET, "✅ rpc: {method}"); + } else { + log::info!(target: LOG_TARGET, "❌ rpc: {method} {}", resp.as_result()); + } + resp + } + .boxed() + } + } +} + +/// Starts the rpc server and returns the server address. +async fn run_server(client: Client, url: &str) -> anyhow::Result { + let cors = CorsLayer::new() + .allow_methods([Method::POST]) + .allow_origin(Any) + .allow_headers([hyper::header::CONTENT_TYPE]); + let cors_middleware = tower::ServiceBuilder::new().layer(cors); + + let builder = Server::builder().set_http_middleware(cors_middleware); + + #[cfg(feature = "dev")] + let builder = builder + .set_rpc_middleware(jsonrpsee::server::RpcServiceBuilder::new().layer_fn(dev::DevLogger)); + + let server = builder.build(url.parse::()?).await?; + let addr = server.local_addr()?; + + let eth_api = EthRpcServerImpl::new(client) + .with_accounts(if cfg!(feature = "dev") { + use pallet_revive::evm::Account; + vec![Account::default()] + } else { + vec![] + }) + .into_rpc(); + + let mut module = RpcModule::new(()); + module.merge(eth_api)?; + + let handle = server.start(module); + tokio::spawn(handle.stopped()); + + Ok(addr) +} diff --git a/substrate/frame/revive/rpc/src/client.rs b/substrate/frame/revive/rpc/src/client.rs new file mode 100644 index 000000000000..ef7945f1591a --- /dev/null +++ b/substrate/frame/revive/rpc/src/client.rs @@ -0,0 +1,784 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! The client connects to the source substrate chain +//! and is used by the rpc server to query and send transactions to the substrate chain. +use crate::{ + rlp, + runtime::GAS_PRICE, + subxt_client::{ + revive::calls::types::EthTransact, runtime_types::pallet_revive::storage::ContractInfo, + }, + TransactionLegacySigned, LOG_TARGET, +}; +use codec::Encode; +use futures::{stream, StreamExt}; +use jsonrpsee::types::{ErrorCode, ErrorObjectOwned}; +use pallet_revive::{ + create1, + evm::{ + Block, BlockNumberOrTag, BlockNumberOrTagOrHash, Bytes256, GenericTransaction, ReceiptInfo, + SyncingProgress, SyncingStatus, TransactionSigned, H160, H256, U256, + }, + EthContractResult, +}; +use sp_runtime::traits::{BlakeTwo256, Hash}; +use sp_weights::Weight; +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, + time::Duration, +}; +use subxt::{ + backend::{ + legacy::LegacyRpcMethods, + rpc::{ + reconnecting_rpc_client::{Client as ReconnectingRpcClient, ExponentialBackoff}, + RpcClient, + }, + }, + config::Header, + error::RpcError, + storage::Storage, + Config, OnlineClient, +}; +use subxt_client::transaction_payment::events::TransactionFeePaid; +use thiserror::Error; +use tokio::{ + sync::{watch::Sender, RwLock}, + task::JoinSet, +}; + +use crate::subxt_client::{self, system::events::ExtrinsicSuccess, SrcChainConfig}; + +/// The substrate block type. +pub type SubstrateBlock = subxt::blocks::Block>; + +/// The substrate block number type. +pub type SubstrateBlockNumber = <::Header as Header>::Number; + +/// The substrate block hash type. +pub type SubstrateBlockHash = ::Hash; + +/// Type alias for shared data. +pub type Shared = Arc>; + +/// The runtime balance type. +pub type Balance = u128; + +/// The cache maintains a buffer of the last N blocks, +#[derive(Default)] +struct BlockCache { + /// A double-ended queue of the last N blocks. + /// The most recent block is at the back of the queue, and the oldest block is at the front. + buffer: VecDeque>, + + /// A map of blocks by block number. + blocks_by_number: HashMap>, + + /// A map of blocks by block hash. + blocks_by_hash: HashMap>, + + /// A map of receipts by hash. + receipts_by_hash: HashMap, + + /// A map of Signed transaction by hash. + signed_tx_by_hash: HashMap, + + /// A map of receipt hashes by block hash. + tx_hashes_by_block_and_index: HashMap>, +} + +/// The error type for the client. +#[derive(Error, Debug)] +pub enum ClientError { + /// A [`subxt::Error`] wrapper error. + #[error("Subxt error: {0}")] + SubxtError(#[from] subxt::Error), + /// A [`RpcError`] wrapper error. + #[error("RPC error: {0}")] + RpcError(#[from] RpcError), + /// A [`codec::Error`] wrapper error. + #[error("Codec error: {0}")] + CodecError(#[from] codec::Error), + /// The dry run failed. + #[error("Dry run failed")] + DryRunFailed, + /// A decimal conversion failed. + #[error("Conversion failed")] + ConversionFailed, + /// The block hash was not found. + #[error("Hash not found")] + BlockNotFound, + /// The transaction fee could not be found + #[error("TransactionFeePaid event not found")] + TxFeeNotFound, + /// The token decimals property was not found + #[error("tokenDecimals not found in properties")] + TokenDecimalsNotFound, + /// The cache is empty. + #[error("Cache is empty")] + CacheEmpty, +} + +// Convert a `ClientError` to an RPC `ErrorObjectOwned`. +impl From for ErrorObjectOwned { + fn from(value: ClientError) -> Self { + log::debug!(target: LOG_TARGET, "ClientError: {value:?}"); + ErrorObjectOwned::owned::<()>( + ErrorCode::InternalError.code(), + ErrorCode::InternalError.message(), + None, + ) + } +} + +/// The number of recent blocks maintained by the cache. +/// For each block in the cache, we also store the EVM transaction receipts. +pub const CACHE_SIZE: usize = 10; + +impl BlockCache { + fn latest_block(&self) -> Option<&Arc> { + self.buffer.back() + } + + /// Insert an entry into the cache, and prune the oldest entry if the cache is full. + fn insert(&mut self, block: SubstrateBlock) { + if self.buffer.len() >= N { + if let Some(block) = self.buffer.pop_front() { + log::trace!(target: LOG_TARGET, "Pruning block: {}", block.number()); + let hash = block.hash(); + self.blocks_by_hash.remove(&hash); + self.blocks_by_number.remove(&block.number()); + if let Some(entries) = self.tx_hashes_by_block_and_index.remove(&hash) { + for hash in entries.values() { + self.receipts_by_hash.remove(hash); + } + } + } + } + + let block = Arc::new(block); + self.buffer.push_back(block.clone()); + self.blocks_by_number.insert(block.number(), block.clone()); + self.blocks_by_hash.insert(block.hash(), block); + } +} + +/// A client connect to a node and maintains a cache of the last `CACHE_SIZE` blocks. +pub struct Client { + /// The inner state of the client. + inner: Arc, + // JoinSet to manage spawned tasks. + join_set: JoinSet>, + /// A watch channel to signal cache updates. + pub updates: tokio::sync::watch::Receiver<()>, +} + +/// The inner state of the client. +struct ClientInner { + api: OnlineClient, + rpc_client: ReconnectingRpcClient, + rpc: LegacyRpcMethods, + cache: Shared>, + chain_id: u64, + max_block_weight: Weight, + native_to_evm_ratio: U256, +} + +impl ClientInner { + /// Create a new client instance connecting to the substrate node at the given URL. + async fn from_url(url: &str) -> Result { + let rpc_client = ReconnectingRpcClient::builder() + .retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10))) + .build(url.to_string()) + .await?; + + let api = OnlineClient::::from_rpc_client(rpc_client.clone()).await?; + let cache = Arc::new(RwLock::new(BlockCache::::default())); + + let rpc = LegacyRpcMethods::::new(RpcClient::new(rpc_client.clone())); + + let (native_to_evm_ratio, chain_id, max_block_weight) = + tokio::try_join!(native_to_evm_ratio(&rpc), chain_id(&api), max_block_weight(&api))?; + + Ok(Self { api, rpc_client, rpc, cache, chain_id, max_block_weight, native_to_evm_ratio }) + } + + /// Convert a native balance to an EVM balance. + fn native_to_evm_decimals(&self, value: U256) -> U256 { + value.saturating_mul(self.native_to_evm_ratio) + } + + /// Get the receipt infos from the extrinsics in a block. + async fn receipt_infos( + &self, + block: &SubstrateBlock, + ) -> Result, ClientError> { + // Get extrinsics from the block + let extrinsics = block.extrinsics().await?; + + // Filter extrinsics from pallet_revive + let extrinsics = extrinsics.iter().flat_map(|ext| { + let ext = ext.ok()?; + + let call = ext.as_extrinsic::().ok()??; + let tx = rlp::decode::(&call.payload).ok()?; + let from = tx.recover_eth_address().ok()?; + let contract_address = if tx.transaction_legacy_unsigned.to.is_none() { + Some(create1(&from, tx.transaction_legacy_unsigned.nonce.try_into().ok()?)) + } else { + None + }; + + Some((from, tx, contract_address, ext)) + }); + + // Map each extrinsic to a receipt + stream::iter(extrinsics) + .map(|(from, tx, contract_address, ext)| async move { + let events = ext.events().await?; + let tx_fees = + events.find_first::()?.ok_or(ClientError::TxFeeNotFound)?; + + let gas_price = tx.transaction_legacy_unsigned.gas_price; + let gas_used = (tx_fees.tip.saturating_add(tx_fees.actual_fee)) + .checked_div(gas_price.as_u128()) + .unwrap_or_default(); + + let success = events.find_first::().is_ok(); + let transaction_index = ext.index(); + let transaction_hash = BlakeTwo256::hash(&Vec::from(ext.bytes()).encode()); + let block_hash = block.hash(); + let block_number = block.number().into(); + + let receipt = ReceiptInfo { + block_hash, + block_number, + contract_address, + from, + to: tx.transaction_legacy_unsigned.to, + effective_gas_price: gas_price, + gas_used: gas_used.into(), + status: Some(if success { U256::one() } else { U256::zero() }), + transaction_hash, + transaction_index: transaction_index.into(), + ..Default::default() + }; + + Ok::<_, ClientError>((receipt.transaction_hash, (tx.into(), receipt))) + }) + .buffer_unordered(10) + .collect::>>() + .await + .into_iter() + .collect::, _>>() + } +} + +/// Drop all the tasks spawned by the client on drop. +impl Drop for Client { + fn drop(&mut self) { + self.join_set.abort_all() + } +} + +/// Fetch the chain ID from the substrate chain. +async fn chain_id(api: &OnlineClient) -> Result { + let query = subxt_client::constants().revive().chain_id(); + api.constants().at(&query).map_err(|err| err.into()) +} + +/// Fetch the max block weight from the substrate chain. +async fn max_block_weight(api: &OnlineClient) -> Result { + let query = subxt_client::constants().system().block_weights(); + let weights = api.constants().at(&query)?; + let max_block = weights.per_class.normal.max_extrinsic.unwrap_or(weights.max_block); + Ok(max_block.0) +} + +/// Fetch the native to EVM ratio from the substrate chain. +async fn native_to_evm_ratio(rpc: &LegacyRpcMethods) -> Result { + let props = rpc.system_properties().await?; + let eth_decimals = U256::from(18u32); + let native_decimals: U256 = props + .get("tokenDecimals") + .and_then(|v| v.as_number()?.as_u64()) + .ok_or(ClientError::TokenDecimalsNotFound)? + .into(); + + Ok(U256::from(10u32).pow(eth_decimals - native_decimals)) +} + +/// Extract the block timestamp. +async fn extract_block_timestamp(block: &SubstrateBlock) -> Option { + let extrinsics = block.extrinsics().await.ok()?; + let ext = extrinsics + .find_first::() + .ok()??; + + Some(ext.value.now / 1000) +} + +impl Client { + /// Create a new client instance. + /// The client will subscribe to new blocks and maintain a cache of [`CACHE_SIZE`] blocks. + pub async fn from_url(url: &str) -> Result { + log::info!(target: LOG_TARGET, "Connecting to node at: {url} ..."); + let inner: Arc = Arc::new(ClientInner::from_url(url).await?); + log::info!(target: LOG_TARGET, "Connected to node at: {url}"); + + let (tx, mut updates) = tokio::sync::watch::channel(()); + let mut join_set = JoinSet::new(); + join_set.spawn(Self::subscribe_blocks(inner.clone(), tx)); + join_set.spawn(Self::subscribe_reconnect(inner.clone())); + + updates.changed().await.expect("tx is not dropped"); + Ok(Self { inner, join_set, updates }) + } + + /// Expose the storage API. + async fn storage_api( + &self, + at: &BlockNumberOrTagOrHash, + ) -> Result>, ClientError> { + match at { + BlockNumberOrTagOrHash::U256(block_number) => { + let n: SubstrateBlockNumber = + (*block_number).try_into().map_err(|_| ClientError::ConversionFailed)?; + + let hash = self.get_block_hash(n).await?.ok_or(ClientError::BlockNotFound)?; + Ok(self.inner.api.storage().at(hash)) + }, + BlockNumberOrTagOrHash::H256(hash) => Ok(self.inner.api.storage().at(*hash)), + BlockNumberOrTagOrHash::BlockTag(_) => { + if let Some(block) = self.latest_block().await { + return Ok(self.inner.api.storage().at(block.hash())); + } + let storage = self.inner.api.storage().at_latest().await?; + Ok(storage) + }, + } + } + + /// Expose the runtime API. + async fn runtime_api( + &self, + at: &BlockNumberOrTagOrHash, + ) -> Result< + subxt::runtime_api::RuntimeApi>, + ClientError, + > { + match at { + BlockNumberOrTagOrHash::U256(block_number) => { + let n: SubstrateBlockNumber = + (*block_number).try_into().map_err(|_| ClientError::ConversionFailed)?; + + let hash = self.get_block_hash(n).await?.ok_or(ClientError::BlockNotFound)?; + Ok(self.inner.api.runtime_api().at(hash)) + }, + BlockNumberOrTagOrHash::H256(hash) => Ok(self.inner.api.runtime_api().at(*hash)), + BlockNumberOrTagOrHash::BlockTag(_) => { + if let Some(block) = self.latest_block().await { + return Ok(self.inner.api.runtime_api().at(block.hash())); + } + + let api = self.inner.api.runtime_api().at_latest().await?; + Ok(api) + }, + } + } + + /// Subscribe and log reconnection events. + async fn subscribe_reconnect(inner: Arc) -> Result<(), ClientError> { + let rpc = inner.as_ref().rpc_client.clone(); + loop { + let reconnected = rpc.reconnect_initiated().await; + log::info!(target: LOG_TARGET, "RPC client connection lost"); + let now = std::time::Instant::now(); + reconnected.await; + log::info!(target: LOG_TARGET, "RPC client reconnection took `{}s`", now.elapsed().as_secs()); + } + } + + /// Subscribe to new blocks and update the cache. + async fn subscribe_blocks(inner: Arc, tx: Sender<()>) -> Result<(), ClientError> { + log::info!(target: LOG_TARGET, "Subscribing to new blocks"); + let mut block_stream = + inner.as_ref().api.blocks().subscribe_best().await.inspect_err(|err| { + log::error!("Failed to subscribe to blocks: {err:?}"); + })?; + + while let Some(block) = block_stream.next().await { + let block = match block { + Ok(block) => block, + Err(err) => { + if err.is_disconnected_will_reconnect() { + log::warn!( + "The RPC connection was lost and we may have missed a few blocks" + ); + continue; + } + + log::error!("Failed to fetch block: {err:?}"); + return Err(err.into()); + }, + }; + + log::trace!(target: LOG_TARGET, "Pushing block: {}", block.number()); + let mut cache = inner.cache.write().await; + + let receipts = inner + .receipt_infos(&block) + .await + .inspect_err(|err| { + log::error!("Failed to get receipts: {err:?}"); + }) + .unwrap_or_default(); + + if !receipts.is_empty() { + log::debug!(target: LOG_TARGET, "Adding {} receipts", receipts.len()); + let values = receipts + .iter() + .map(|(hash, (_, receipt))| (receipt.transaction_index, *hash)) + .collect::>(); + + cache.tx_hashes_by_block_and_index.insert(block.hash(), values); + + cache + .receipts_by_hash + .extend(receipts.iter().map(|(hash, (_, receipt))| (*hash, receipt.clone()))); + + cache.signed_tx_by_hash.extend( + receipts.iter().map(|(hash, (signed_tx, _))| (*hash, signed_tx.clone())), + ) + } + + cache.insert(block); + tx.send_replace(()); + } + + log::info!(target: LOG_TARGET, "Block subscription ended"); + Ok(()) + } +} + +impl Client { + /// Get the most recent block stored in the cache. + pub async fn latest_block(&self) -> Option> { + let cache = self.inner.cache.read().await; + let block = cache.latest_block()?; + Some(block.clone()) + } + + /// Expose the transaction API. + pub async fn submit( + &self, + call: subxt::tx::DefaultPayload, + ) -> Result { + let ext = self.inner.api.tx().create_unsigned(&call).map_err(ClientError::from)?; + let hash = ext.submit().await?; + Ok(hash) + } + + /// Get an EVM transaction receipt by hash. + pub async fn receipt(&self, tx_hash: &H256) -> Option { + let cache = self.inner.cache.read().await; + cache.receipts_by_hash.get(tx_hash).cloned() + } + + /// Get the syncing status of the chain. + pub async fn syncing(&self) -> Result { + let health = self.inner.rpc.system_health().await?; + + let status = if health.is_syncing { + let client = RpcClient::new(self.inner.rpc_client.clone()); + let sync_state: sc_rpc::system::SyncState = + client.request("system_syncState", Default::default()).await?; + + SyncingProgress { + current_block: Some(sync_state.current_block.into()), + highest_block: Some(sync_state.highest_block.into()), + starting_block: Some(sync_state.starting_block.into()), + } + .into() + } else { + SyncingStatus::Bool(false) + }; + + Ok(status) + } + + /// Get an EVM transaction receipt by hash. + pub async fn receipt_by_hash_and_index( + &self, + block_hash: &H256, + transaction_index: &U256, + ) -> Option { + let cache = self.inner.cache.read().await; + let receipt_hash = + cache.tx_hashes_by_block_and_index.get(block_hash)?.get(transaction_index)?; + let receipt = cache.receipts_by_hash.get(receipt_hash)?; + Some(receipt.clone()) + } + + pub async fn signed_tx_by_hash(&self, tx_hash: &H256) -> Option { + let cache = self.inner.cache.read().await; + cache.signed_tx_by_hash.get(tx_hash).cloned() + } + + /// Get receipts count per block. + pub async fn receipts_count_per_block(&self, block_hash: &SubstrateBlockHash) -> Option { + let cache = self.inner.cache.read().await; + cache.tx_hashes_by_block_and_index.get(block_hash).map(|v| v.len()) + } + + /// Get the balance of the given address. + pub async fn balance( + &self, + address: H160, + at: &BlockNumberOrTagOrHash, + ) -> Result { + // TODO: remove once subxt is updated + let address = address.0.into(); + + let runtime_api = self.runtime_api(at).await?; + let payload = subxt_client::apis().revive_api().balance(address); + let balance = runtime_api.call(payload).await?.into(); + Ok(self.inner.native_to_evm_decimals(balance)) + } + + /// Get the contract storage for the given contract address and key. + pub async fn get_contract_storage( + &self, + contract_address: H160, + key: U256, + block: BlockNumberOrTagOrHash, + ) -> Result, ClientError> { + let runtime_api = self.runtime_api(&block).await?; + + // TODO: remove once subxt is updated + let contract_address = contract_address.0.into(); + + let payload = subxt_client::apis() + .revive_api() + .get_storage(contract_address, key.to_big_endian()); + let result = runtime_api.call(payload).await?.unwrap_or_default().unwrap_or_default(); + Ok(result) + } + + /// Get the contract code for the given contract address. + pub async fn get_contract_code( + &self, + contract_address: &H160, + block: BlockNumberOrTagOrHash, + ) -> Result, ClientError> { + let storage_api = self.storage_api(&block).await?; + + // TODO: remove once subxt is updated + let contract_address: subxt::utils::H160 = contract_address.0.into(); + + let query = subxt_client::storage().revive().contract_info_of(contract_address); + let Some(ContractInfo { code_hash, .. }) = storage_api.fetch(&query).await? else { + return Ok(Vec::new()); + }; + + let query = subxt_client::storage().revive().pristine_code(code_hash); + let result = storage_api.fetch(&query).await?.map(|v| v.0).unwrap_or_default(); + Ok(result) + } + + /// Dry run a transaction and returns the [`EthContractResult`] for the transaction. + pub async fn dry_run( + &self, + tx: &GenericTransaction, + block: BlockNumberOrTagOrHash, + ) -> Result, ClientError> { + let runtime_api = self.runtime_api(&block).await?; + let from = tx.from.ok_or_else(|| { + log::debug!(target: LOG_TARGET, "Dry run failed: from address is missing"); + ClientError::DryRunFailed + })?; + let value = tx + .value + .unwrap_or_default() + .try_into() + .map_err(|_| ClientError::ConversionFailed)?; + + // TODO: remove once subxt is updated + let from = from.0.into(); + let to = tx.to.map(|v| v.0.into()); + + let payload = subxt_client::apis().revive_api().eth_transact( + from, + to, + value, + tx.input.clone().unwrap_or_default().0, + None, + None, + ); + let res = runtime_api.call(payload).await?.0; + Ok(res) + } + + /// Dry run a transaction and returns the gas estimate for the transaction. + pub async fn estimate_gas( + &self, + tx: &GenericTransaction, + block: BlockNumberOrTagOrHash, + ) -> Result { + let dry_run = self.dry_run(tx, block).await?; + Ok(U256::from(dry_run.fee / GAS_PRICE as u128) + GAS_PRICE) + } + + /// Get the nonce of the given address. + pub async fn nonce( + &self, + address: H160, + at: BlockNumberOrTagOrHash, + ) -> Result { + let address = address.0.into(); + + let runtime_api = self.runtime_api(&at).await?; + let payload = subxt_client::apis().revive_api().nonce(address); + let nonce = runtime_api.call(payload).await?; + Ok(nonce.into()) + } + + /// Get the block number of the latest block. + pub async fn block_number(&self) -> Result { + let cache = self.inner.cache.read().await; + let latest_block = cache.buffer.back().ok_or(ClientError::CacheEmpty)?; + Ok(latest_block.number()) + } + + /// Get a block hash for the given block number. + pub async fn get_block_hash( + &self, + block_number: SubstrateBlockNumber, + ) -> Result, ClientError> { + let cache = self.inner.cache.read().await; + if let Some(block) = cache.blocks_by_number.get(&block_number) { + return Ok(Some(block.hash())); + } + + let hash = self.inner.rpc.chain_get_block_hash(Some(block_number.into())).await?; + Ok(hash) + } + + /// Get a block for the specified hash or number. + pub async fn block_by_number_or_tag( + &self, + block: &BlockNumberOrTag, + ) -> Result>, ClientError> { + match block { + BlockNumberOrTag::U256(n) => { + let n = (*n).try_into().map_err(|_| ClientError::ConversionFailed)?; + self.block_by_number(n).await + }, + BlockNumberOrTag::BlockTag(_) => { + let cache = self.inner.cache.read().await; + Ok(cache.buffer.back().cloned()) + }, + } + } + + /// Get a block by hash + pub async fn block_by_hash( + &self, + hash: &SubstrateBlockHash, + ) -> Result>, ClientError> { + let cache = self.inner.cache.read().await; + if let Some(block) = cache.blocks_by_hash.get(hash) { + return Ok(Some(block.clone())); + } + + match self.inner.api.blocks().at(*hash).await { + Ok(block) => Ok(Some(Arc::new(block))), + Err(subxt::Error::Block(subxt::error::BlockError::NotFound(_))) => Ok(None), + Err(err) => Err(err.into()), + } + } + + /// Get a block by number + pub async fn block_by_number( + &self, + block_number: SubstrateBlockNumber, + ) -> Result>, ClientError> { + let cache = self.inner.cache.read().await; + if let Some(block) = cache.blocks_by_number.get(&block_number) { + return Ok(Some(block.clone())); + } + + let Some(hash) = self.get_block_hash(block_number).await? else { + return Ok(None); + }; + + self.block_by_hash(&hash).await + } + + /// Get the EVM block for the given hash. + pub async fn evm_block(&self, block: Arc) -> Result { + let runtime_api = self.inner.api.runtime_api().at(block.hash()); + let max_fee = Self::weight_to_fee(&runtime_api, self.max_block_weight()).await?; + let gas_limit = U256::from(max_fee / GAS_PRICE as u128); + + let header = block.header(); + let timestamp = extract_block_timestamp(&block).await.unwrap_or_default(); + + // TODO: remove once subxt is updated + let parent_hash = header.parent_hash.0.into(); + let state_root = header.state_root.0.into(); + let extrinsics_root = header.extrinsics_root.0.into(); + + Ok(Block { + hash: block.hash(), + parent_hash, + state_root, + transactions_root: extrinsics_root, + number: header.number.into(), + timestamp: timestamp.into(), + difficulty: Some(0u32.into()), + gas_limit, + logs_bloom: Bytes256([0u8; 256]), + receipts_root: extrinsics_root, + ..Default::default() + }) + } + + /// Convert a weight to a fee. + async fn weight_to_fee( + runtime_api: &subxt::runtime_api::RuntimeApi>, + weight: Weight, + ) -> Result { + let payload = subxt_client::apis() + .transaction_payment_api() + .query_weight_to_fee(weight.into()); + + let fee = runtime_api.call(payload).await?; + Ok(fee) + } + + /// Get the chain ID. + pub fn chain_id(&self) -> u64 { + self.inner.chain_id + } + + /// Get the Max Block Weight. + pub fn max_block_weight(&self) -> Weight { + self.inner.max_block_weight + } +} diff --git a/substrate/frame/revive/rpc/src/example.rs b/substrate/frame/revive/rpc/src/example.rs new file mode 100644 index 000000000000..cdf5ce9d1b98 --- /dev/null +++ b/substrate/frame/revive/rpc/src/example.rs @@ -0,0 +1,96 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Example utilities +#![cfg(any(feature = "example", test))] + +use crate::{EthRpcClient, ReceiptInfo}; +use anyhow::Context; +use pallet_revive::evm::{ + rlp::*, Account, BlockTag, Bytes, GenericTransaction, TransactionLegacyUnsigned, H160, H256, + U256, +}; + +/// Wait for a transaction receipt. +pub async fn wait_for_receipt( + client: &(impl EthRpcClient + Send + Sync), + hash: H256, +) -> anyhow::Result { + for _ in 0..30 { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let receipt = client.get_transaction_receipt(hash).await?; + if let Some(receipt) = receipt { + return Ok(receipt) + } + } + + anyhow::bail!("Failed to get receipt") +} + +/// Send a transaction. +pub async fn send_transaction( + signer: &Account, + client: &(impl EthRpcClient + Send + Sync), + value: U256, + input: Bytes, + to: Option, +) -> anyhow::Result { + let from = signer.address(); + + let chain_id = Some(client.chain_id().await?); + + let gas_price = client.gas_price().await?; + let nonce = client + .get_transaction_count(from, BlockTag::Latest.into()) + .await + .with_context(|| "Failed to fetch account nonce")?; + + let gas = client + .estimate_gas( + GenericTransaction { + from: Some(from), + input: Some(input.clone()), + value: Some(value), + gas_price: Some(gas_price), + to, + ..Default::default() + }, + None, + ) + .await + .with_context(|| "Failed to fetch gas estimate")?; + + let unsigned_tx = TransactionLegacyUnsigned { + gas, + nonce, + to, + value, + input, + gas_price, + chain_id, + ..Default::default() + }; + + let tx = signer.sign_transaction(unsigned_tx.clone()); + let bytes = tx.rlp_bytes().to_vec(); + + let hash = client + .send_raw_transaction(bytes.clone().into()) + .await + .with_context(|| "transaction failed")?; + + Ok(hash) +} diff --git a/substrate/frame/revive/rpc/src/lib.rs b/substrate/frame/revive/rpc/src/lib.rs new file mode 100644 index 000000000000..a6d47063ef90 --- /dev/null +++ b/substrate/frame/revive/rpc/src/lib.rs @@ -0,0 +1,359 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! The [`EthRpcServer`] RPC server implementation +#![cfg_attr(docsrs, feature(doc_cfg))] + +use crate::runtime::GAS_PRICE; +use client::ClientError; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + types::{ErrorCode, ErrorObjectOwned}, +}; +use pallet_revive::{evm::*, EthContractResult}; +use sp_core::{H160, H256, U256}; +use thiserror::Error; + +pub mod cli; +pub mod client; +pub mod example; +pub mod subxt_client; + +#[cfg(test)] +mod tests; + +mod rpc_methods_gen; +pub use rpc_methods_gen::*; + +pub const LOG_TARGET: &str = "eth-rpc"; + +/// An EVM RPC server implementation. +pub struct EthRpcServerImpl { + /// The client used to interact with the substrate node. + client: client::Client, + + /// The accounts managed by the server. + accounts: Vec, +} + +impl EthRpcServerImpl { + /// Creates a new [`EthRpcServerImpl`]. + pub fn new(client: client::Client) -> Self { + Self { client, accounts: vec![] } + } + + /// Sets the accounts managed by the server. + pub fn with_accounts(mut self, accounts: Vec) -> Self { + self.accounts = accounts; + self + } +} + +/// The error type for the EVM RPC server. +#[derive(Error, Debug)] +pub enum EthRpcError { + /// A [`ClientError`] wrapper error. + #[error("Client error: {0}")] + ClientError(#[from] ClientError), + /// A [`rlp::DecoderError`] wrapper error. + #[error("Decoding error: {0}")] + RlpError(#[from] rlp::DecoderError), + /// A Decimals conversion error. + #[error("Conversion error")] + ConversionError, + /// An invalid signature error. + #[error("Invalid signature")] + InvalidSignature, + /// The account was not found at the given address + #[error("Account not found for address {0:?}")] + AccountNotFound(H160), + /// Received an invalid transaction + #[error("Invalid transaction")] + InvalidTransaction, + /// Received an invalid transaction + #[error("Invalid transaction {0:?}")] + TransactionTypeNotSupported(Byte), +} + +impl From for ErrorObjectOwned { + fn from(value: EthRpcError) -> Self { + let code = match value { + EthRpcError::ClientError(_) => ErrorCode::InternalError, + _ => ErrorCode::InvalidRequest, + }; + Self::owned::(code.code(), value.to_string(), None) + } +} + +#[async_trait] +impl EthRpcServer for EthRpcServerImpl { + async fn net_version(&self) -> RpcResult { + Ok(self.client.chain_id().to_string()) + } + + async fn syncing(&self) -> RpcResult { + Ok(self.client.syncing().await?) + } + + async fn block_number(&self) -> RpcResult { + let number = self.client.block_number().await?; + Ok(number.into()) + } + + async fn get_transaction_receipt( + &self, + transaction_hash: H256, + ) -> RpcResult> { + let receipt = self.client.receipt(&transaction_hash).await; + Ok(receipt) + } + + async fn estimate_gas( + &self, + transaction: GenericTransaction, + _block: Option, + ) -> RpcResult { + let result = self.client.estimate_gas(&transaction, BlockTag::Latest.into()).await?; + Ok(result) + } + + async fn send_raw_transaction(&self, transaction: Bytes) -> RpcResult { + let tx = rlp::decode::(&transaction.0).map_err(|err| { + log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}"); + EthRpcError::from(err) + })?; + + let eth_addr = tx.recover_eth_address().map_err(|err| { + log::debug!(target: LOG_TARGET, "Failed to recover eth address: {err:?}"); + EthRpcError::InvalidSignature + })?; + + // Dry run the transaction to get the weight limit and storage deposit limit + let TransactionLegacyUnsigned { to, input, value, .. } = tx.transaction_legacy_unsigned; + let dry_run = self + .client + .dry_run( + &GenericTransaction { + from: Some(eth_addr), + input: Some(input.clone()), + to, + value: Some(value), + ..Default::default() + }, + BlockTag::Latest.into(), + ) + .await?; + + let EthContractResult { gas_required, storage_deposit, .. } = dry_run; + let call = subxt_client::tx().revive().eth_transact( + transaction.0, + gas_required.into(), + storage_deposit, + ); + let hash = self.client.submit(call).await?; + Ok(hash) + } + + async fn send_transaction(&self, transaction: GenericTransaction) -> RpcResult { + log::debug!(target: LOG_TARGET, "{transaction:#?}"); + let GenericTransaction { from, gas, gas_price, input, to, value, r#type, .. } = transaction; + + let Some(from) = from else { + log::debug!(target: LOG_TARGET, "Transaction must have a sender"); + return Err(EthRpcError::InvalidTransaction.into()); + }; + + let account = self + .accounts + .iter() + .find(|account| account.address() == from) + .ok_or(EthRpcError::AccountNotFound(from))?; + + let gas_price = gas_price.unwrap_or_else(|| U256::from(GAS_PRICE)); + let chain_id = Some(self.client.chain_id().into()); + let input = input.unwrap_or_default(); + let value = value.unwrap_or_default(); + let r#type = r#type.unwrap_or_default(); + + let Some(gas) = gas else { + log::debug!(target: LOG_TARGET, "Transaction must have a gas limit"); + return Err(EthRpcError::InvalidTransaction.into()); + }; + + let r#type = Type0::try_from_byte(r#type.clone()) + .map_err(|_| EthRpcError::TransactionTypeNotSupported(r#type))?; + + let nonce = self.get_transaction_count(from, BlockTag::Latest.into()).await?; + + let tx = + TransactionLegacyUnsigned { chain_id, gas, gas_price, input, nonce, to, value, r#type }; + let tx = account.sign_transaction(tx); + let rlp_bytes = rlp::encode(&tx).to_vec(); + self.send_raw_transaction(Bytes(rlp_bytes)).await + } + + async fn get_block_by_hash( + &self, + block_hash: H256, + _hydrated_transactions: bool, + ) -> RpcResult> { + let Some(block) = self.client.block_by_hash(&block_hash).await? else { + return Ok(None); + }; + let block = self.client.evm_block(block).await?; + Ok(Some(block)) + } + + async fn get_balance(&self, address: H160, block: BlockNumberOrTagOrHash) -> RpcResult { + let balance = self.client.balance(address, &block).await?; + log::debug!(target: LOG_TARGET, "balance({address}): {balance:?}"); + Ok(balance) + } + + async fn chain_id(&self) -> RpcResult { + Ok(self.client.chain_id().into()) + } + + async fn gas_price(&self) -> RpcResult { + Ok(U256::from(GAS_PRICE)) + } + + async fn get_code(&self, address: H160, block: BlockNumberOrTagOrHash) -> RpcResult { + let code = self.client.get_contract_code(&address, block).await?; + Ok(code.into()) + } + + async fn accounts(&self) -> RpcResult> { + Ok(self.accounts.iter().map(|account| account.address()).collect()) + } + + async fn call( + &self, + transaction: GenericTransaction, + block: Option, + ) -> RpcResult { + let dry_run = self + .client + .dry_run(&transaction, block.unwrap_or_else(|| BlockTag::Latest.into())) + .await?; + let output = dry_run.result.map_err(|err| { + log::debug!(target: LOG_TARGET, "Dry run failed: {err:?}"); + ClientError::DryRunFailed + })?; + + Ok(output.into()) + } + + async fn get_block_by_number( + &self, + block: BlockNumberOrTag, + _hydrated_transactions: bool, + ) -> RpcResult> { + let Some(block) = self.client.block_by_number_or_tag(&block).await? else { + return Ok(None); + }; + let block = self.client.evm_block(block).await?; + Ok(Some(block)) + } + + async fn get_block_transaction_count_by_hash( + &self, + block_hash: Option, + ) -> RpcResult> { + let block_hash = if let Some(block_hash) = block_hash { + block_hash + } else { + self.client.latest_block().await.ok_or(ClientError::BlockNotFound)?.hash() + }; + Ok(self.client.receipts_count_per_block(&block_hash).await.map(U256::from)) + } + + async fn get_block_transaction_count_by_number( + &self, + block: Option, + ) -> RpcResult> { + let Some(block) = self + .get_block_by_number(block.unwrap_or_else(|| BlockTag::Latest.into()), false) + .await? + else { + return Ok(None); + }; + + Ok(self.client.receipts_count_per_block(&block.hash).await.map(U256::from)) + } + + async fn get_storage_at( + &self, + address: H160, + storage_slot: U256, + block: BlockNumberOrTagOrHash, + ) -> RpcResult { + let bytes = self.client.get_contract_storage(address, storage_slot, block).await?; + Ok(bytes.into()) + } + + async fn get_transaction_by_block_hash_and_index( + &self, + block_hash: H256, + transaction_index: U256, + ) -> RpcResult> { + let Some(receipt) = + self.client.receipt_by_hash_and_index(&block_hash, &transaction_index).await + else { + return Ok(None); + }; + + let Some(signed_tx) = self.client.signed_tx_by_hash(&receipt.transaction_hash).await else { + return Ok(None); + }; + + Ok(Some(TransactionInfo::new(receipt, signed_tx))) + } + + async fn get_transaction_by_block_number_and_index( + &self, + block: BlockNumberOrTag, + transaction_index: U256, + ) -> RpcResult> { + let Some(block) = self.client.block_by_number_or_tag(&block).await? else { + return Ok(None); + }; + self.get_transaction_by_block_hash_and_index(block.hash(), transaction_index) + .await + } + + async fn get_transaction_by_hash( + &self, + transaction_hash: H256, + ) -> RpcResult> { + let receipt = self.client.receipt(&transaction_hash).await; + let signed_tx = self.client.signed_tx_by_hash(&transaction_hash).await; + if let (Some(receipt), Some(signed_tx)) = (receipt, signed_tx) { + return Ok(Some(TransactionInfo::new(receipt, signed_tx))); + } + + Ok(None) + } + + async fn get_transaction_count( + &self, + address: H160, + block: BlockNumberOrTagOrHash, + ) -> RpcResult { + let nonce = self.client.nonce(address, block).await?; + Ok(nonce) + } +} diff --git a/substrate/frame/revive/rpc/src/main.rs b/substrate/frame/revive/rpc/src/main.rs new file mode 100644 index 000000000000..b1306ad096b0 --- /dev/null +++ b/substrate/frame/revive/rpc/src/main.rs @@ -0,0 +1,39 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! The Ethereum JSON-RPC server. +use clap::Parser; +use pallet_revive_eth_rpc::cli; +use tracing_subscriber::{util::SubscriberInitExt, EnvFilter, FmtSubscriber}; + +/// Initialize tracing +fn init_tracing() { + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("eth_rpc=trace")); + + FmtSubscriber::builder() + .with_env_filter(env_filter) + .finish() + .try_init() + .expect("failed to initialize tracing"); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + init_tracing(); + let cmd = cli::CliCommand::parse(); + cli::run(cmd).await +} diff --git a/substrate/frame/revive/rpc/src/rpc_methods_gen.rs b/substrate/frame/revive/rpc/src/rpc_methods_gen.rs new file mode 100644 index 000000000000..339080368969 --- /dev/null +++ b/substrate/frame/revive/rpc/src/rpc_methods_gen.rs @@ -0,0 +1,160 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Generated JSON-RPC methods. +#![allow(missing_docs)] + +use super::*; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +#[rpc(server, client)] +pub trait EthRpc { + /// Returns a list of addresses owned by client. + #[method(name = "eth_accounts")] + async fn accounts(&self) -> RpcResult>; + + /// Returns the number of most recent block. + #[method(name = "eth_blockNumber")] + async fn block_number(&self) -> RpcResult; + + /// Executes a new message call immediately without creating a transaction on the block chain. + #[method(name = "eth_call")] + async fn call( + &self, + transaction: GenericTransaction, + block: Option, + ) -> RpcResult; + + /// Returns the chain ID of the current network. + #[method(name = "eth_chainId")] + async fn chain_id(&self) -> RpcResult; + + /// Generates and returns an estimate of how much gas is necessary to allow the transaction to + /// complete. + #[method(name = "eth_estimateGas")] + async fn estimate_gas( + &self, + transaction: GenericTransaction, + block: Option, + ) -> RpcResult; + + /// Returns the current price per gas in wei. + #[method(name = "eth_gasPrice")] + async fn gas_price(&self) -> RpcResult; + + /// Returns the balance of the account of given address. + #[method(name = "eth_getBalance")] + async fn get_balance(&self, address: Address, block: BlockNumberOrTagOrHash) + -> RpcResult; + + /// Returns information about a block by hash. + #[method(name = "eth_getBlockByHash")] + async fn get_block_by_hash( + &self, + block_hash: H256, + hydrated_transactions: bool, + ) -> RpcResult>; + + /// Returns information about a block by number. + #[method(name = "eth_getBlockByNumber")] + async fn get_block_by_number( + &self, + block: BlockNumberOrTag, + hydrated_transactions: bool, + ) -> RpcResult>; + + /// Returns the number of transactions in a block from a block matching the given block hash. + #[method(name = "eth_getBlockTransactionCountByHash")] + async fn get_block_transaction_count_by_hash( + &self, + block_hash: Option, + ) -> RpcResult>; + + /// Returns the number of transactions in a block matching the given block number. + #[method(name = "eth_getBlockTransactionCountByNumber")] + async fn get_block_transaction_count_by_number( + &self, + block: Option, + ) -> RpcResult>; + + /// Returns code at a given address. + #[method(name = "eth_getCode")] + async fn get_code(&self, address: Address, block: BlockNumberOrTagOrHash) -> RpcResult; + + /// Returns the value from a storage position at a given address. + #[method(name = "eth_getStorageAt")] + async fn get_storage_at( + &self, + address: Address, + storage_slot: U256, + block: BlockNumberOrTagOrHash, + ) -> RpcResult; + + /// Returns information about a transaction by block hash and transaction index position. + #[method(name = "eth_getTransactionByBlockHashAndIndex")] + async fn get_transaction_by_block_hash_and_index( + &self, + block_hash: H256, + transaction_index: U256, + ) -> RpcResult>; + + /// Returns information about a transaction by block number and transaction index position. + #[method(name = "eth_getTransactionByBlockNumberAndIndex")] + async fn get_transaction_by_block_number_and_index( + &self, + block: BlockNumberOrTag, + transaction_index: U256, + ) -> RpcResult>; + + /// Returns the information about a transaction requested by transaction hash. + #[method(name = "eth_getTransactionByHash")] + async fn get_transaction_by_hash( + &self, + transaction_hash: H256, + ) -> RpcResult>; + + /// Returns the number of transactions sent from an address. + #[method(name = "eth_getTransactionCount")] + async fn get_transaction_count( + &self, + address: Address, + block: BlockNumberOrTagOrHash, + ) -> RpcResult; + + /// Returns the receipt of a transaction by transaction hash. + #[method(name = "eth_getTransactionReceipt")] + async fn get_transaction_receipt( + &self, + transaction_hash: H256, + ) -> RpcResult>; + + /// Submits a raw transaction. For EIP-4844 transactions, the raw form must be the network form. + /// This means it includes the blobs, KZG commitments, and KZG proofs. + #[method(name = "eth_sendRawTransaction")] + async fn send_raw_transaction(&self, transaction: Bytes) -> RpcResult; + + /// Signs and submits a transaction. + #[method(name = "eth_sendTransaction")] + async fn send_transaction(&self, transaction: GenericTransaction) -> RpcResult; + + /// Returns an object with data about the sync status or false. + #[method(name = "eth_syncing")] + async fn syncing(&self) -> RpcResult; + + /// The string value of current network id + #[method(name = "net_version")] + async fn net_version(&self) -> RpcResult; +} diff --git a/substrate/frame/revive/rpc/src/subxt_client.rs b/substrate/frame/revive/rpc/src/subxt_client.rs new file mode 100644 index 000000000000..1362143fa3a7 --- /dev/null +++ b/substrate/frame/revive/rpc/src/subxt_client.rs @@ -0,0 +1,71 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! The generated subxt client. +//! Generated against a substrate chain configured with [`pallet_revive`] using: +//! subxt metadata --url ws://localhost:9944 -o rpc/revive_chain.scale +use subxt::config::{signed_extensions, Config, PolkadotConfig}; + +#[subxt::subxt( + runtime_metadata_path = "revive_chain.scale", + substitute_type( + path = "pallet_revive::primitives::EthContractResult", + with = "::subxt::utils::Static<::pallet_revive::EthContractResult>" + ), + substitute_type( + path = "sp_weights::weight_v2::Weight", + with = "::subxt::utils::Static<::sp_weights::Weight>" + ) +)] +mod src_chain {} +pub use src_chain::*; + +/// The configuration for the source chain. +pub enum SrcChainConfig {} +impl Config for SrcChainConfig { + type Hash = sp_core::H256; + type AccountId = ::AccountId; + type Address = ::Address; + type Signature = ::Signature; + type Hasher = BlakeTwo256; + type Header = subxt::config::substrate::SubstrateHeader; + type AssetId = ::AssetId; + type ExtrinsicParams = signed_extensions::AnyOf< + Self, + ( + signed_extensions::CheckSpecVersion, + signed_extensions::CheckTxVersion, + signed_extensions::CheckNonce, + signed_extensions::CheckGenesis, + signed_extensions::CheckMortality, + signed_extensions::ChargeAssetTxPayment, + signed_extensions::ChargeTransactionPayment, + signed_extensions::CheckMetadataHash, + ), + >; +} + +/// A type that can hash values using the blaks2_256 algorithm. +/// TODO remove once subxt is updated +#[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode)] +pub struct BlakeTwo256; + +impl subxt::config::Hasher for BlakeTwo256 { + type Output = sp_core::H256; + fn hash(s: &[u8]) -> Self::Output { + sp_crypto_hashing::blake2_256(s).into() + } +} diff --git a/substrate/frame/revive/rpc/src/tests.rs b/substrate/frame/revive/rpc/src/tests.rs new file mode 100644 index 000000000000..8b618469f25d --- /dev/null +++ b/substrate/frame/revive/rpc/src/tests.rs @@ -0,0 +1,103 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Test the eth-rpc cli with the kitchensink node. + +// We require the `riscv` feature to get access to the compiled fixtures. +#![cfg(feature = "riscv")] +use crate::{ + cli, + example::{send_transaction, wait_for_receipt}, + EthRpcClient, +}; +use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; +use pallet_revive::{ + create1, + evm::{Account, BlockTag, Bytes, U256}, +}; +use std::thread; +use substrate_cli_test_utils::*; + +/// Create a websocket client with a 30s timeout. +async fn ws_client_with_retry(url: &str) -> WsClient { + let timeout = tokio::time::Duration::from_secs(30); + tokio::time::timeout(timeout, async { + loop { + if let Ok(client) = WsClientBuilder::default().build(url).await { + return client + } else { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + } + }) + .await + .expect("Hit timeout") +} + +#[tokio::test] +async fn test_jsonrpsee_server() -> anyhow::Result<()> { + // Start the node. + let _ = thread::spawn(move || match start_node_inline(vec!["--dev", "--rpc-port=45789"]) { + Ok(_) => {}, + Err(e) => { + panic!("Node exited with error: {}", e); + }, + }); + + // Start the rpc server. + tokio::spawn(cli::run(cli::CliCommand { + rpc_port: "45788".to_string(), + node_rpc_url: "ws://localhost:45789".to_string(), + })); + + let client = ws_client_with_retry("ws://localhost:45788").await; + let account = Account::default(); + + // Deploy contract + let data = b"hello world".to_vec(); + let value = U256::from(5_000_000_000_000u128); + let (bytes, _) = pallet_revive_fixtures::compile_module("dummy")?; + let input = bytes.into_iter().chain(data.clone()).collect::>(); + let nonce = client.get_transaction_count(account.address(), BlockTag::Latest.into()).await?; + let hash = send_transaction(&account, &client, value, input.into(), None).await?; + let receipt = wait_for_receipt(&client, hash).await?; + let contract_address = create1(&account.address(), nonce.try_into().unwrap()); + assert_eq!( + Some(contract_address), + receipt.contract_address, + "Contract should be deployed with the correct address." + ); + + let balance = client.get_balance(contract_address, BlockTag::Latest.into()).await?; + assert_eq!( + value * 1_000_000, + balance, + "Contract balance should be the same as the value sent." + ); + + // Call contract + let hash = + send_transaction(&account, &client, U256::zero(), Bytes::default(), Some(contract_address)) + .await?; + let receipt = wait_for_receipt(&client, hash).await?; + assert_eq!( + Some(contract_address), + receipt.to, + "Receipt should have the correct contract address." + ); + + Ok(()) +} diff --git a/substrate/frame/revive/src/evm/api/account.rs b/substrate/frame/revive/src/evm/api/account.rs index c2217defc31f..06fb6e7e9c21 100644 --- a/substrate/frame/revive/src/evm/api/account.rs +++ b/substrate/frame/revive/src/evm/api/account.rs @@ -20,6 +20,7 @@ use crate::{ H160, }; use rlp::Encodable; +use sp_runtime::AccountId32; /// A simple account that can sign transactions pub struct Account(subxt_signer::eth::Keypair); @@ -42,6 +43,14 @@ impl Account { H160::from_slice(&self.0.account_id().as_ref()) } + /// Get the substrate [`AccountId32`] of the account. + pub fn substrate_account(&self) -> AccountId32 { + let mut account_id = AccountId32::new([0xEE; 32]); + >::as_mut(&mut account_id)[..20] + .copy_from_slice(self.address().as_ref()); + account_id + } + /// Sign a transaction. pub fn sign_transaction(&self, tx: TransactionLegacyUnsigned) -> TransactionLegacySigned { let rlp_encoded = tx.rlp_bytes(); diff --git a/substrate/frame/revive/src/evm/runtime.rs b/substrate/frame/revive/src/evm/runtime.rs index e4340b27a187..dcb7be4979f2 100644 --- a/substrate/frame/revive/src/evm/runtime.rs +++ b/substrate/frame/revive/src/evm/runtime.rs @@ -48,7 +48,7 @@ type CallOf = ::RuntimeCall; /// We use a fixed value for the gas price. /// This let us calculate the gas estimate for a transaction with the formula: /// `estimate_gas = substrate_fee / gas_price`. -pub const GAS_PRICE: u32 = 1_000u32; +pub const GAS_PRICE: u32 = 1u32; /// Wraps [`generic::UncheckedExtrinsic`] to support checking unsigned /// [`crate::Call::eth_transact`] extrinsic. @@ -356,12 +356,7 @@ pub trait EthExtra { Default::default(), ) .into(); - - log::debug!(target: LOG_TARGET, "Checking Ethereum transaction fees: - dispatch_info: {info:?} - encoded_len: {encoded_len:?} - fees: {actual_fee:?} - "); + log::debug!(target: LOG_TARGET, "try_into_checked_extrinsic: encoded_len: {encoded_len:?} actual_fee: {actual_fee:?} eth_fee: {eth_fee:?}"); if eth_fee < actual_fee { log::debug!(target: LOG_TARGET, "fees {eth_fee:?} too low for the extrinsic {actual_fee:?}"); @@ -490,11 +485,30 @@ mod test { } } + fn estimate_gas(&mut self) { + let dry_run = crate::Pallet::::bare_eth_transact( + Account::default().account_id(), + self.tx.to, + self.tx.value.try_into().unwrap(), + self.tx.input.clone().0, + Weight::MAX, + u64::MAX, + |call| { + let call = RuntimeCall::Contracts(call); + let uxt: Ex = sp_runtime::generic::UncheckedExtrinsic::new_bare(call).into(); + uxt.encoded_size() as u32 + }, + crate::DebugInfo::Skip, + crate::CollectEvents::Skip, + ); + self.tx.gas = ((dry_run.fee + GAS_PRICE as u64) / (GAS_PRICE as u64)).into(); + } + /// Create a new builder with a call to the given address. fn call_with(dest: H160) -> Self { let mut builder = Self::new(); builder.tx.to = Some(dest); - builder.tx.gas = U256::from(516_708u128); + builder.estimate_gas(); builder } @@ -502,7 +516,7 @@ mod test { fn instantiate_with(code: Vec, data: Vec) -> Self { let mut builder = Self::new(); builder.tx.input = Bytes(code.into_iter().chain(data.into_iter()).collect()); - builder.tx.gas = U256::from(1_035_070u128); + builder.estimate_gas(); builder } diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index 9b0bbb2d6bcb..c9d254838c91 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -1212,6 +1212,7 @@ where to: Some(dest), ..Default::default() }; + let eth_dispatch_call = crate::Call::::eth_transact { payload: tx.dummy_signed_payload(), gas_limit: result.gas_required, @@ -1238,7 +1239,7 @@ where ) .into(); - log::debug!(target: LOG_TARGET, "Call dry run Result: dispatch_info: {dispatch_info:?} len: {encoded_len:?} fee: {fee:?}"); + log::debug!(target: LOG_TARGET, "bare_eth_call: len: {encoded_len:?} fee: {fee:?}"); EthContractResult { gas_required: result.gas_required, storage_deposit: result.storage_deposit.charge_or_zero(), @@ -1393,12 +1394,19 @@ environmental!(executing_contract: bool); sp_api::decl_runtime_apis! { /// The API used to dry-run contract interactions. #[api_version(1)] - pub trait ReviveApi where + pub trait ReviveApi where AccountId: Codec, Balance: Codec, + Nonce: Codec, BlockNumber: Codec, EventRecord: Codec, { + /// Returns the free balance of the given `[H160]` address. + fn balance(address: H160) -> Balance; + + /// Returns the nonce of the given `[H160]` address. + fn nonce(address: H160) -> Nonce; + /// Perform a call from a specified account to a given contract. /// /// See [`crate::Pallet::bare_call`]. diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 370673622b95..28d6a2c3fb01 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -605,12 +605,13 @@ runtime = [ "sp-wasm-interface", "sp-weights", ] -node = ["asset-test-utils", "bridge-hub-test-utils", "cumulus-client-cli", "cumulus-client-collator", "cumulus-client-consensus-aura", "cumulus-client-consensus-common", "cumulus-client-consensus-proposer", "cumulus-client-consensus-relay-chain", "cumulus-client-network", "cumulus-client-parachain-inherent", "cumulus-client-pov-recovery", "cumulus-client-service", "cumulus-relay-chain-inprocess-interface", "cumulus-relay-chain-interface", "cumulus-relay-chain-minimal-node", "cumulus-relay-chain-rpc-interface", "cumulus-test-relay-sproof-builder", "emulated-integration-tests-common", "fork-tree", "frame-benchmarking-cli", "frame-remote-externalities", "frame-support-procedural-tools", "generate-bags", "mmr-gadget", "mmr-rpc", "pallet-contracts-mock-network", "pallet-revive-mock-network", "pallet-transaction-payment-rpc", "parachains-runtimes-test-utils", "polkadot-approval-distribution", "polkadot-availability-bitfield-distribution", "polkadot-availability-distribution", "polkadot-availability-recovery", "polkadot-cli", "polkadot-collator-protocol", "polkadot-dispute-distribution", "polkadot-erasure-coding", "polkadot-gossip-support", "polkadot-network-bridge", "polkadot-node-collation-generation", "polkadot-node-core-approval-voting", "polkadot-node-core-approval-voting-parallel", "polkadot-node-core-av-store", "polkadot-node-core-backing", "polkadot-node-core-bitfield-signing", "polkadot-node-core-candidate-validation", "polkadot-node-core-chain-api", "polkadot-node-core-chain-selection", "polkadot-node-core-dispute-coordinator", "polkadot-node-core-parachains-inherent", "polkadot-node-core-prospective-parachains", "polkadot-node-core-provisioner", "polkadot-node-core-pvf", "polkadot-node-core-pvf-checker", "polkadot-node-core-pvf-common", "polkadot-node-core-pvf-execute-worker", "polkadot-node-core-pvf-prepare-worker", "polkadot-node-core-runtime-api", "polkadot-node-metrics", "polkadot-node-network-protocol", "polkadot-node-primitives", "polkadot-node-subsystem", "polkadot-node-subsystem-types", "polkadot-node-subsystem-util", "polkadot-omni-node-lib", "polkadot-overseer", "polkadot-rpc", "polkadot-service", "polkadot-statement-distribution", "polkadot-statement-table", "sc-allocator", "sc-authority-discovery", "sc-basic-authorship", "sc-block-builder", "sc-chain-spec", "sc-cli", "sc-client-api", "sc-client-db", "sc-consensus", "sc-consensus-aura", "sc-consensus-babe", "sc-consensus-babe-rpc", "sc-consensus-beefy", "sc-consensus-beefy-rpc", "sc-consensus-epochs", "sc-consensus-grandpa", "sc-consensus-grandpa-rpc", "sc-consensus-manual-seal", "sc-consensus-pow", "sc-consensus-slots", "sc-executor", "sc-executor-common", "sc-executor-polkavm", "sc-executor-wasmtime", "sc-informant", "sc-keystore", "sc-mixnet", "sc-network", "sc-network-common", "sc-network-gossip", "sc-network-light", "sc-network-statement", "sc-network-sync", "sc-network-transactions", "sc-network-types", "sc-offchain", "sc-proposer-metrics", "sc-rpc", "sc-rpc-api", "sc-rpc-server", "sc-rpc-spec-v2", "sc-service", "sc-state-db", "sc-statement-store", "sc-storage-monitor", "sc-sync-state-rpc", "sc-sysinfo", "sc-telemetry", "sc-tracing", "sc-transaction-pool", "sc-transaction-pool-api", "sc-utils", "snowbridge-runtime-test-common", "sp-blockchain", "sp-consensus", "sp-core-hashing", "sp-core-hashing-proc-macro", "sp-database", "sp-maybe-compressed-blob", "sp-panic-handler", "sp-rpc", "staging-chain-spec-builder", "staging-node-inspect", "staging-tracking-allocator", "std", "subkey", "substrate-build-script-utils", "substrate-frame-rpc-support", "substrate-frame-rpc-system", "substrate-prometheus-endpoint", "substrate-rpc-client", "substrate-state-trie-migration-rpc", "substrate-wasm-builder", "tracing-gum", "xcm-emulator", "xcm-simulator"] +node = ["asset-test-utils", "bridge-hub-test-utils", "cumulus-client-cli", "cumulus-client-collator", "cumulus-client-consensus-aura", "cumulus-client-consensus-common", "cumulus-client-consensus-proposer", "cumulus-client-consensus-relay-chain", "cumulus-client-network", "cumulus-client-parachain-inherent", "cumulus-client-pov-recovery", "cumulus-client-service", "cumulus-relay-chain-inprocess-interface", "cumulus-relay-chain-interface", "cumulus-relay-chain-minimal-node", "cumulus-relay-chain-rpc-interface", "cumulus-test-relay-sproof-builder", "emulated-integration-tests-common", "fork-tree", "frame-benchmarking-cli", "frame-remote-externalities", "frame-support-procedural-tools", "generate-bags", "mmr-gadget", "mmr-rpc", "pallet-contracts-mock-network", "pallet-revive-eth-rpc", "pallet-revive-mock-network", "pallet-transaction-payment-rpc", "parachains-runtimes-test-utils", "polkadot-approval-distribution", "polkadot-availability-bitfield-distribution", "polkadot-availability-distribution", "polkadot-availability-recovery", "polkadot-cli", "polkadot-collator-protocol", "polkadot-dispute-distribution", "polkadot-erasure-coding", "polkadot-gossip-support", "polkadot-network-bridge", "polkadot-node-collation-generation", "polkadot-node-core-approval-voting", "polkadot-node-core-approval-voting-parallel", "polkadot-node-core-av-store", "polkadot-node-core-backing", "polkadot-node-core-bitfield-signing", "polkadot-node-core-candidate-validation", "polkadot-node-core-chain-api", "polkadot-node-core-chain-selection", "polkadot-node-core-dispute-coordinator", "polkadot-node-core-parachains-inherent", "polkadot-node-core-prospective-parachains", "polkadot-node-core-provisioner", "polkadot-node-core-pvf", "polkadot-node-core-pvf-checker", "polkadot-node-core-pvf-common", "polkadot-node-core-pvf-execute-worker", "polkadot-node-core-pvf-prepare-worker", "polkadot-node-core-runtime-api", "polkadot-node-metrics", "polkadot-node-network-protocol", "polkadot-node-primitives", "polkadot-node-subsystem", "polkadot-node-subsystem-types", "polkadot-node-subsystem-util", "polkadot-omni-node-lib", "polkadot-overseer", "polkadot-rpc", "polkadot-service", "polkadot-statement-distribution", "polkadot-statement-table", "sc-allocator", "sc-authority-discovery", "sc-basic-authorship", "sc-block-builder", "sc-chain-spec", "sc-cli", "sc-client-api", "sc-client-db", "sc-consensus", "sc-consensus-aura", "sc-consensus-babe", "sc-consensus-babe-rpc", "sc-consensus-beefy", "sc-consensus-beefy-rpc", "sc-consensus-epochs", "sc-consensus-grandpa", "sc-consensus-grandpa-rpc", "sc-consensus-manual-seal", "sc-consensus-pow", "sc-consensus-slots", "sc-executor", "sc-executor-common", "sc-executor-polkavm", "sc-executor-wasmtime", "sc-informant", "sc-keystore", "sc-mixnet", "sc-network", "sc-network-common", "sc-network-gossip", "sc-network-light", "sc-network-statement", "sc-network-sync", "sc-network-transactions", "sc-network-types", "sc-offchain", "sc-proposer-metrics", "sc-rpc", "sc-rpc-api", "sc-rpc-server", "sc-rpc-spec-v2", "sc-service", "sc-state-db", "sc-statement-store", "sc-storage-monitor", "sc-sync-state-rpc", "sc-sysinfo", "sc-telemetry", "sc-tracing", "sc-transaction-pool", "sc-transaction-pool-api", "sc-utils", "snowbridge-runtime-test-common", "sp-blockchain", "sp-consensus", "sp-core-hashing", "sp-core-hashing-proc-macro", "sp-database", "sp-maybe-compressed-blob", "sp-panic-handler", "sp-rpc", "staging-chain-spec-builder", "staging-node-inspect", "staging-tracking-allocator", "std", "subkey", "substrate-build-script-utils", "substrate-frame-rpc-support", "substrate-frame-rpc-system", "substrate-prometheus-endpoint", "substrate-rpc-client", "substrate-state-trie-migration-rpc", "substrate-wasm-builder", "tracing-gum", "xcm-emulator", "xcm-simulator"] tuples-96 = [ "frame-support-procedural?/tuples-96", "frame-support?/tuples-96", ] riscv = [ + "pallet-revive-eth-rpc?/riscv", "pallet-revive-fixtures?/riscv", "pallet-revive-mock-network?/riscv", "pallet-revive?/riscv", @@ -1902,6 +1903,11 @@ path = "../substrate/frame/contracts/mock-network" default-features = false optional = true +[dependencies.pallet-revive-eth-rpc] +path = "../substrate/frame/revive/rpc" +default-features = false +optional = true + [dependencies.pallet-revive-mock-network] path = "../substrate/frame/revive/mock-network" default-features = false diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index f3fc949c66ec..2216864fad0f 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -580,6 +580,10 @@ pub use pallet_remark; #[cfg(feature = "pallet-revive")] pub use pallet_revive; +/// An Ethereum JSON-RPC server for pallet-revive. +#[cfg(feature = "pallet-revive-eth-rpc")] +pub use pallet_revive_eth_rpc; + /// Fixtures for testing and benchmarking. #[cfg(feature = "pallet-revive-fixtures")] pub use pallet_revive_fixtures;