From 4e24cc035a433e31fe1bb7de493257e08a2f4f6f Mon Sep 17 00:00:00 2001 From: sterliakov <50529348+sterliakov@users.noreply.github.com> Date: Sat, 23 Dec 2023 22:46:10 +0400 Subject: [PATCH] Add several examples. Prepare 0.1.0-beta.1 (#17) * Add first examples, use U256 for references. * Add more examples * Add MSRV * Release 0.1.0-beta.1 --- .github/workflows/build.yml | 70 ++++++++++++- .github/workflows/test.yml | 42 -------- CONTRIBUTING.md | 4 +- Cargo.toml | 7 +- data/eip20.abi | 163 ++++++++++++++++++++++++++++++ examples/abi.rs | 49 +++++++++ examples/bloom_filter.rs | 36 +++++++ examples/create_wallet.rs | 46 +++++++++ examples/my_example.rs | 24 ----- examples/transaction_broadcast.rs | 82 +++++++++++++++ src/hdnode.rs | 2 +- src/lib.rs | 2 +- src/network.rs | 110 +++++++++++--------- src/transactions.rs | 14 +-- tests/test_network.rs | 158 ++++++++++++++--------------- 15 files changed, 595 insertions(+), 214 deletions(-) delete mode 100644 .github/workflows/test.yml create mode 100644 data/eip20.abi create mode 100644 examples/abi.rs create mode 100644 examples/bloom_filter.rs create mode 100644 examples/create_wallet.rs delete mode 100644 examples/my_example.rs create mode 100644 examples/transaction_broadcast.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4eee57c..317406a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,10 +6,12 @@ on: pull_request: branches: - master + schedule: + - cron: "0 0 * * *" jobs: - build: - name: Build + clippy: + name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -17,9 +19,69 @@ jobs: uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - - name: Release build - run: cargo build --release --all-features - uses: giraffate/clippy-action@v1 with: reporter: 'github-pr-review' github_token: ${{ secrets.GITHUB_TOKEN }} + + build: + name: Build with rust ${{matrix.rust}} on ${{matrix.os == 'ubuntu' && 'Linux' || matrix.os == 'macos' && 'macOS' || matrix.os == 'windows' && 'Windows' || '???'}} + runs-on: ${{matrix.os}}-latest + strategy: + fail-fast: false + matrix: + rust: + - "1.67.1" # MSRV + - "stable" + - "beta" + - "nightly" + os: [ubuntu, windows, macos] + steps: + - uses: actions/checkout@v4 + - name: Stable with rustfmt and clippy + uses: dtolnay/rust-toolchain@master + with: + components: rustfmt, clippy + toolchain: ${{matrix.rust}} + - name: Release build + run: cargo build --release --all-features + + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-tarpaulin,cargo-rdme + + - name: Check README is up-to-date + run: cargo rdme --check + + - name: Run tests with coverage + run: RUST_BACKTRACE=1 cargo tarpaulin --out Xml --all-features + + - name: Submit a transaction to chain + run: RUST_BACKTRACE=1 cargo run --example transaction_broadcast --all-features + env: + TEST_TO_ADDRESS: ${{ secrets.TO_ADDRESS }} + TEST_MNEMONIC: ${{ secrets.MNEMONIC }} + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Archive code coverage results + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: cobertura.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 0206483..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: test -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - name: Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v3 - - uses: pre-commit/action@v3.0.0 - - - name: Install stable toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - - uses: taiki-e/install-action@v2 - with: - tool: cargo-tarpaulin,cargo-rdme - - - name: Check README is up-to-date - run: cargo rdme --check - - - name: Run tests with coverage - run: RUST_BACKTRACE=1 cargo tarpaulin --out Xml --all-features - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - name: Archive code coverage results - uses: actions/upload-artifact@v4 - with: - name: code-coverage-report - path: cobertura.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 471b9b6..e57847f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,12 +8,12 @@ guidelines for this repo. Here's what you need to know! **thor-devkit.rs** is proudly licenced under the GNU General Public License v3, and so are all contributions. Please see the [`LICENSE`] file in this directory for more details. -[`LICENSE`]: https://github.com/sterliakov/thor-devkit.rs/blob/main/LICENSE +[`LICENSE`]: https://github.com/sterliakov/thor-devkit.rs/blob/master/LICENSE ## Pull Requests To make changes to **thor-devkit.rs**, please send in pull requests on GitHub to -the `main` branch. I'll review them and either merge or request changes. GitHub Actions +the `master` branch. I'll review them and either merge or request changes. GitHub Actions tests everything as well, so you may get feedback from it too. If you make additions or other changes to a pull request, feel free to either amend diff --git a/Cargo.toml b/Cargo.toml index f6453bf..7701de2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thor-devkit" -version = "0.1.0-alpha.1" +version = "0.1.0-beta.1" authors = ["Stanislav Terliakov "] description = "Rust library to aid coding with VeChain, eg. Wallets/Tx/Sign/Verify." documentation = "https://docs.rs/thor-devkit.rs" @@ -10,6 +10,7 @@ keywords = ["vechain", "crypto", "blockchain", "cryptography"] categories = ["cryptography"] license = "GPL-3.0" edition = "2021" +rust-version = "1.67.1" # See here for more info: https://blog.rust-lang.org/2020/03/15/docs-rs-opt-into-fewer-targets.html @@ -40,8 +41,10 @@ serde_with = { version = "^3.4", features = ["hex"], optional = true } version-sync = "0.9.4" rand = { version = "0.8.5", features = ["getrandom"] } tokio = { version = "1", features = ["full"] } +bloomfilter = "^1.0" +ethabi = "^18.0" [features] -default = ['http'] +# default = ['http'] serde = ["dep:serde", "dep:serde_json", "dep:serde_with"] http = ["dep:reqwest", "serde"] diff --git a/data/eip20.abi b/data/eip20.abi new file mode 100644 index 0000000..57d7623 --- /dev/null +++ b/data/eip20.abi @@ -0,0 +1,163 @@ +[ + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "total", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "remaining", + "type": "uint256" + } + ], + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } +] diff --git a/examples/abi.rs b/examples/abi.rs new file mode 100644 index 0000000..7622833 --- /dev/null +++ b/examples/abi.rs @@ -0,0 +1,49 @@ +//! thor-devkit does not vendor ABI parsing solution, because a good one +//! already exists in wild ([`ethabi`](https://docs.rs/ethabi/latest/ethabi/index.html)). +//! +//! To decode or encode data given a contract ABI and some input data, +//! you can create a contract from JSON ABI specification and process it +//! as necessary. +//! +//! Future version of thor-devkit will likely depend on [`ethabi`] to improve +//! interoperability and enable more smooth experience. + +use ethabi::{Contract, Token}; +use std::fs::File; +use thor_devkit::rlp::Bytes; + +fn demo_abi() { + let eip20 = Contract::load(File::open("data/eip20.abi").expect("Must exist")) + .expect("Should be loadable"); + let owner = [0u8; 20]; + let spender = [1u8; 20]; + let inputs = vec![Token::Address(owner.into()), Token::Address(spender.into())]; + let allowance = eip20.function("allowance").expect("Exists"); + println!("Function signature: {}", allowance.signature()); + println!( + "Function short signature: {:02x?}", + allowance.short_signature() + ); + let encoded = allowance + .encode_input(&inputs) + .expect("Should be encodable"); + println!( + "Encoded data for allowance call: {:x}", + Bytes::copy_from_slice(&encoded[..]) + ); + // To decode, we strip a function signature first: + let decoded = allowance.decode_input(&encoded[4..]).expect("Should parse"); + assert_eq!(decoded, inputs); + + let decoded_out = allowance.decode_output(&[0x01; 32]).expect("Should parse"); + assert_eq!(decoded_out, vec![Token::Uint([0x01; 32].into())]) +} + +#[test] +fn test_run() { + demo_abi(); +} + +fn main() { + demo_abi(); +} diff --git a/examples/bloom_filter.rs b/examples/bloom_filter.rs new file mode 100644 index 0000000..57cac74 --- /dev/null +++ b/examples/bloom_filter.rs @@ -0,0 +1,36 @@ +//! Bloom filter usage. +//! +//! Bloom filter is a probabilistic data structure that is used to check +//! whether the element definitely is not in set or may be in the set. +//! +//! Instead of a traditional hash-based set, that takes up too much memory, +//! this structure permits less memory with a tolerable false positive rate. +//! +//! This library does not provide explicit bloom filter reimplementation. +//! You can use specialized [`bloomfilter`](https://docs.rs/bloomfilter/latest/bloomfilter/) +//! crate instead. + +use bloomfilter::Bloom; + +fn bloom_filter_example() { + // Create a filter with <0.01% probability of false positive for 10_000 elements + let mut filter = Bloom::new_for_fp_rate(10_000, 1e-4); + println!("Adding items..."); + for i in 1..=100u64 { + filter.set(&i); + } + println!("Checking items presence..."); + for i in 1..=100u64 { + assert!(filter.check(&i)); + } + println!("All added items found!"); +} + +#[test] +fn test_run() { + bloom_filter_example(); +} + +fn main() { + bloom_filter_example(); +} diff --git a/examples/create_wallet.rs b/examples/create_wallet.rs new file mode 100644 index 0000000..7e7be6f --- /dev/null +++ b/examples/create_wallet.rs @@ -0,0 +1,46 @@ +use itertools::Itertools; +use rand::rngs::OsRng; +use rand::RngCore; +use secp256k1::Secp256k1; +use thor_devkit::hdnode::{HDNode, Language, Mnemonic}; +use thor_devkit::AddressConvertible; + +fn create_wallet() { + let mut entropy = [0u8; 32]; + OsRng.fill_bytes(&mut entropy); + let mnemonic = + Mnemonic::from_entropy(&entropy, Language::English).expect("Should be constructible"); + println!("Mnemonic text: {}", mnemonic.clone().into_phrase()); + let node = HDNode::build() + .mnemonic(mnemonic) + .build() + .expect("Should build"); + let priv_key = node.private_key().unwrap().private_key().clone(); + println!( + "Private key: {}", + priv_key + .secret_bytes() + .iter() + .map(|&c| format!("{:02x}", c)) + .join("") + ); + let pub_key = priv_key.public_key(&Secp256k1::signing_only()); + println!( + "Public key: {}", + pub_key + .serialize() + .iter() + .map(|&c| format!("{:02x}", c)) + .join("") + ); + println!("Address: {}", pub_key.address().to_checksum_address()) +} + +#[test] +fn test_run() { + create_wallet(); +} + +fn main() { + create_wallet(); +} diff --git a/examples/my_example.rs b/examples/my_example.rs deleted file mode 100644 index e6590d5..0000000 --- a/examples/my_example.rs +++ /dev/null @@ -1,24 +0,0 @@ -// #![deny(warnings)] -// #![warn(rust_2018_idioms)] - -// #[macro_use] -// extern crate log; - -// // use std::time::Instant; - -// use thor_devkit::*; - -// // A simple type alias so as to DRY. -// type Result = std::result::Result>; - -// #[tokio::main] -// async fn main() -> Result<()> { -// sensible_env_logger::init!(); - -// // TODO -// trace!("Hello world!"); - -// Ok(()) -// } - -fn main() {} diff --git a/examples/transaction_broadcast.rs b/examples/transaction_broadcast.rs new file mode 100644 index 0000000..bb89e51 --- /dev/null +++ b/examples/transaction_broadcast.rs @@ -0,0 +1,82 @@ +//! Network communication requires `http` create feature. + +use std::{thread, time::Duration}; +use thor_devkit::hdnode::{HDNode, Language, Mnemonic}; +use thor_devkit::network::{AResult, BlockReference, ThorNode}; +use thor_devkit::transactions::{Clause, Transaction}; +use thor_devkit::Address; + +async fn create_and_broadcast_transaction() -> AResult<()> { + let node = ThorNode::testnet(); + let block_ref = node + .fetch_block(BlockReference::Best) + .await? + .expect("Must exist") + .0 + .id + .0[3]; + let recipient: Address = std::env::var("TEST_TO_ADDRESS") + .expect("Address must be provided") + .parse() + .unwrap(); + let transaction = Transaction { + chain_tag: node.chain_tag, + block_ref: block_ref, + expiration: 128, + clauses: vec![Clause { + to: Some(recipient), + value: 1000.into(), + data: b"".to_vec().into(), + }], + gas_price_coef: 128, + gas: 21000, + depends_on: None, + nonce: 0xbc614e, + reserved: None, + signature: None, + }; + let mnemonic = Mnemonic::from_phrase( + &std::env::var("TEST_MNEMONIC").expect("Mnemonic must be provided"), + Language::English, + )?; + let wallet = HDNode::build().mnemonic(mnemonic).build()?.derive(0)?; + let sender = wallet.address(); + println!( + "Sending from {:?} to {:?}", + sender.to_checksum_address(), + recipient.to_checksum_address() + ); + println!( + "Balances before: {:?}, {:?}", + node.fetch_account(sender).await?.balance, + node.fetch_account(recipient).await?.balance + ); + let signed = transaction.sign(&wallet.private_key()?.private_key()); + let id = node.broadcast_transaction(&signed).await?; + loop { + if let Some((_, tx_meta)) = node.fetch_extended_transaction(id).await? { + if let Some(meta) = tx_meta { + println!("Transaction included into block {:064x}", meta.block_id); + break; + } else { + println!("Transaction not finalized yet"); + } + } else { + println!("Transaction not processed yet"); + thread::sleep(Duration::from_secs(2)); + } + } + println!( + "Balances after: {:?}, {:?}", + node.fetch_account(sender).await?.balance, + node.fetch_account(recipient).await?.balance + ); + Ok(()) +} + +#[tokio::main] +async fn main() { + create_and_broadcast_transaction() + .await + .expect("Must not fail"); +} diff --git a/src/hdnode.rs b/src/hdnode.rs index 7d30869..1108af6 100644 --- a/src/hdnode.rs +++ b/src/hdnode.rs @@ -89,7 +89,7 @@ impl HDNode { Restricted(pubkey) => pubkey.attrs().depth, } } - pub fn address(self) -> crate::address::Address { + pub fn address(&self) -> crate::address::Address { //! Get the address of current node. use crate::address::AddressConvertible; diff --git a/src/lib.rs b/src/lib.rs index 7a7462a..d55f7f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/thor-devkit/0.1.0-alpha.1")] +#![doc(html_root_url = "https://docs.rs/thor-devkit/0.1.0-beta.1")] #![warn(rust_2018_idioms, missing_docs)] #![deny(dead_code, unused_imports, unused_mut)] diff --git a/src/network.rs b/src/network.rs index 9ae517d..e841a29 100644 --- a/src/network.rs +++ b/src/network.rs @@ -6,7 +6,6 @@ use crate::transactions::{Clause, Transaction}; use crate::utils::unhex; use crate::{Address, U256}; use reqwest::{Client, Url}; -use rustc_hex::ToHex; use serde::{Deserialize, Serialize}; /// Generic result of all asynchronous calls in this module. @@ -17,6 +16,8 @@ pub type AResult = std::result::Result) -> std::fmt::Result { match self { Self::ZeroStorageKey => f.write_str("Account storage key cannot be zero"), + Self::BroadcastFailed(text) => { + f.write_str("Failed to broadcast: ")?; + f.write_str(text.strip_suffix('\n').unwrap_or(text)) + } } } } -/// 256-byte binary sequence (usually a hash of something) -pub type Hash256 = [u8; 32]; - /// A simple HTTP REST client for a VeChain node. pub struct ThorNode { base_url: Url, - #[allow(dead_code)] - chain_tag: u8, + /// Chain tag used for this network. + pub chain_tag: u8, } #[serde_with::serde_as] @@ -51,8 +53,8 @@ struct RawTxResponse { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ExtendedTransaction { /// Identifier of the transaction - #[serde_as(as = "unhex::Hex")] - pub id: Hash256, + #[serde_as(as = "unhex::HexNum<32, U256>")] + pub id: U256, /// The one who signed the transaction pub origin: Address, /// The delegator who paid the gas fee @@ -132,8 +134,8 @@ struct ExtendedTransactionResponse { pub struct TransactionMeta { /// Block identifier #[serde(rename = "blockID")] - #[serde_as(as = "unhex::Hex")] - pub block_id: Hash256, + #[serde_as(as = "unhex::HexNum<32, U256>")] + pub block_id: U256, /// Block number (height) #[serde(rename = "blockNumber")] pub block_number: u32, @@ -186,8 +188,8 @@ pub struct ReceiptOutput { pub struct ReceiptMeta { /// Block identifier #[serde(rename = "blockID")] - #[serde_as(as = "unhex::Hex")] - pub block_id: Hash256, + #[serde_as(as = "unhex::HexNum<32, U256>")] + pub block_id: U256, /// Block number (height) #[serde(rename = "blockNumber")] pub block_number: u32, @@ -196,8 +198,8 @@ pub struct ReceiptMeta { pub block_timestamp: u32, /// Transaction identifier #[serde(rename = "txID")] - #[serde_as(as = "unhex::Hex")] - pub tx_id: Hash256, + #[serde_as(as = "unhex::HexNum<32, U256>")] + pub tx_id: U256, /// Transaction origin (signer) #[serde(rename = "txOrigin")] pub tx_origin: Address, @@ -210,8 +212,8 @@ pub struct Event { /// The address of contract which produces the event pub address: Address, /// Event topics - #[serde_as(as = "Vec")] - pub topics: Vec, + #[serde_as(as = "Vec>")] + pub topics: Vec, /// Event data #[serde_as(as = "unhex::Hex")] pub data: Bytes, @@ -235,14 +237,14 @@ pub struct BlockInfo { /// Block number (height) pub number: u32, /// Block identifier - #[serde_as(as = "unhex::Hex")] - pub id: Hash256, + #[serde_as(as = "unhex::HexNum<32, U256>")] + pub id: U256, /// RLP encoded block size in bytes pub size: u32, /// Parent block ID - #[serde_as(as = "unhex::Hex")] + #[serde_as(as = "unhex::HexNum<32, U256>")] #[serde(rename = "parentID")] - pub parent_id: Hash256, + pub parent_id: U256, /// Block unix timestamp pub timestamp: u32, /// Block gas limit (max allowed accumulative gas usage of transactions) @@ -257,20 +259,20 @@ pub struct BlockInfo { #[serde(rename = "totalScore")] pub total_score: u32, /// Root hash of transactions in the block - #[serde_as(as = "unhex::Hex")] + #[serde_as(as = "unhex::HexNum<32, U256>")] #[serde(rename = "txsRoot")] - pub txs_root: Hash256, + pub txs_root: U256, /// Supported txs features bitset #[serde(rename = "txsFeatures")] pub txs_features: u32, /// Root hash of accounts state - #[serde_as(as = "unhex::Hex")] + #[serde_as(as = "unhex::HexNum<32, U256>")] #[serde(rename = "stateRoot")] - pub state_root: Hash256, + pub state_root: U256, /// Root hash of transaction receipts - #[serde_as(as = "unhex::Hex")] + #[serde_as(as = "unhex::HexNum<32, U256>")] #[serde(rename = "receiptsRoot")] - pub receipts_root: Hash256, + pub receipts_root: U256, /// Is in trunk? #[serde(rename = "isTrunk")] pub is_trunk: bool, @@ -301,8 +303,8 @@ pub struct BlockTransaction { struct BlockResponse { #[serde(flatten)] base: BlockInfo, - #[serde_as(as = "Vec")] - transactions: Vec, + #[serde_as(as = "Vec>")] + transactions: Vec, } #[serde_with::serde_as] @@ -323,7 +325,7 @@ pub enum BlockReference { /// Block ordinal number (1..) Number(u64), /// Block ID - ID(Hash256), + ID(U256), } impl BlockReference { @@ -332,10 +334,7 @@ impl BlockReference { BlockReference::Best => "best".to_string(), BlockReference::Finalized => "finalized".to_string(), BlockReference::Number(num) => format!("0x{:02x}", num), - BlockReference::ID(id) => { - let hex: String = id.to_hex(); - format!("0x{}", hex) - } + BlockReference::ID(id) => format!("0x{:064x}", id), } } } @@ -367,6 +366,18 @@ struct AccountStorageResponse { #[serde_as(as = "unhex::HexNum<32, U256>")] value: U256, } +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Serialize)] +struct TransactionBroadcastRequest { + #[serde_as(as = "unhex::Hex")] + raw: Bytes, +} +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Deserialize)] +struct TransactionIdResponse { + #[serde_as(as = "unhex::HexNum<32, U256>")] + id: U256, +} /// Transaction execution simulation request #[serde_with::serde_as] @@ -446,7 +457,7 @@ impl ThorNode { pub async fn fetch_transaction( &self, - transaction_id: Hash256, + transaction_id: U256, ) -> AResult)>> { //! Retrieve a [`Transaction`] from node by its ID. //! @@ -459,8 +470,7 @@ impl ThorNode { //! from other parts of library. You can get more info from node //! with [`ThorNode::fetch_extended_transaction`]. let client = Client::new(); - let hex_id: String = transaction_id.to_hex(); - let path = format!("/transactions/0x{}", hex_id); + let path = format!("/transactions/0x{:064x}", transaction_id); let response = client .get(self.base_url.join(&path)?) .query(&[("raw", "true")]) @@ -479,7 +489,7 @@ impl ThorNode { pub async fn fetch_extended_transaction( &self, - transaction_id: Hash256, + transaction_id: U256, ) -> AResult)>> { //! Retrieve a [`Transaction`] from node by its ID. //! @@ -491,8 +501,7 @@ impl ThorNode { //! This method returns more data than [`ThorNode::fetch_transaction`], //! but is not interoperable with [`Transaction`]. let client = Client::new(); - let hex_id: String = transaction_id.to_hex(); - let path = format!("/transactions/0x{}", hex_id); + let path = format!("/transactions/0x{:064x}", transaction_id); let response = client .get(self.base_url.join(&path)?) .send() @@ -509,14 +518,13 @@ impl ThorNode { pub async fn fetch_transaction_receipt( &self, - transaction_id: Hash256, + transaction_id: U256, ) -> AResult> { //! Retrieve a transaction receipt from node given a transaction ID. //! //! Returns [`None`] for nonexistent or not mined transactions. let client = Client::new(); - let hex_id: String = transaction_id.to_hex(); - let path = format!("/transactions/0x{}/receipt", hex_id); + let path = format!("/transactions/0x{:064x}/receipt", transaction_id); let response = client .get(self.base_url.join(&path)?) .send() @@ -534,7 +542,7 @@ impl ThorNode { pub async fn fetch_block( &self, block_ref: BlockReference, - ) -> AResult)>> { + ) -> AResult)>> { //! Retrieve a block from node by given identifier. //! //! Returns [`None`] for nonexistent blocks. @@ -579,15 +587,21 @@ impl ThorNode { } } - pub async fn broadcast_transaction(&self, transaction: &Transaction) -> AResult<()> { + pub async fn broadcast_transaction(&self, transaction: &Transaction) -> AResult { //! Broadcast a new [`Transaction`] to the node. let client = Client::new(); - client - .post(self.base_url.join("/transactions/")?) - .body(transaction.to_broadcastable_bytes()?) + let response = client + .post(self.base_url.join("/transactions")?) + .json(&TransactionBroadcastRequest { + raw: transaction.to_broadcastable_bytes()?, + }) .send() + .await? + .text() .await?; - Ok(()) + let decoded: TransactionIdResponse = serde_json::from_str(&response) + .map_err(|_| ValidationError::BroadcastFailed(response.to_string()))?; + Ok(decoded.id) } pub async fn fetch_account(&self, address: Address) -> AResult { diff --git a/src/transactions.rs b/src/transactions.rs index bed5f23..1d7a495 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -19,28 +19,28 @@ rlp_encodable! { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Transaction { /// Chain tag - #[serde(rename="chainTag")] + #[cfg_attr(feature="serde", serde(rename="chainTag"))] pub chain_tag: u8, /// Previous block reference /// /// First 4 bytes (BE) are block height, the rest is part of referred block ID. - #[serde(rename="blockRef")] + #[cfg_attr(feature="serde", serde(rename="blockRef"))] pub block_ref: u64, /// Expiration (in blocks) pub expiration: u32, /// Vector of clauses pub clauses: Vec, /// Coefficient to calculate the gas price. - #[serde(rename="gasPriceCoef")] + #[cfg_attr(feature="serde", serde(rename="gasPriceCoef"))] pub gas_price_coef: u8, /// Maximal amount of gas to spend for transaction. pub gas: u64, /// Hash of transaction on which current transaction depends. /// /// May be left unspecified if this functionality is not necessary. - #[serde(rename="dependsOn")] + #[cfg_attr(feature="serde", serde(rename="dependsOn"))] pub depends_on: Option => AsBytes, - /// Transaction nonceserde_as + /// Transaction nonce pub nonce: u64, /// Reserved fields. pub reserved: Option => AsVec, @@ -257,9 +257,9 @@ impl Transaction { rlp_encodable! { /// Represents a single transaction clause (recipient, value and data). - #[serde_with::serde_as] + #[cfg_attr(feature="serde", serde_with::serde_as)] #[cfg_attr(feature="serde", derive(Deserialize, Serialize))] - #[cfg_attr(feature="serde", derive(Clone, Debug, Eq, PartialEq))] + #[derive(Clone, Debug, Eq, PartialEq)] pub struct Clause { /// Recipient pub to: Option
=> AsBytes
, diff --git a/tests/test_network.rs b/tests/test_network.rs index 3c225f1..33c3ea7 100644 --- a/tests/test_network.rs +++ b/tests/test_network.rs @@ -1,8 +1,12 @@ use rustc_hex::FromHex; +use thor_devkit::U256; fn decode_hex(hex: &str) -> Vec { hex.from_hex().unwrap() } +fn decode_u256(hex: &str) -> U256 { + U256::from_big_endian(&decode_hex(hex)) +} #[cfg(feature = "http")] #[cfg(test)] @@ -10,17 +14,13 @@ mod test_network { use super::*; use thor_devkit::network::*; use thor_devkit::rlp::Bytes; - use thor_devkit::transactions::*; + use thor_devkit::transactions::{Transaction, *}; - fn existing_tx_id() -> [u8; 32] { - decode_hex("ea4c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") - .try_into() - .unwrap() + fn existing_tx_id() -> U256 { + decode_u256("ea4c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") } - fn existing_block_id() -> [u8; 32] { - decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c") - .try_into() - .unwrap() + fn existing_block_id() -> U256 { + decode_u256("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c") } fn transaction_details() -> (Transaction, TransactionMeta) { @@ -51,11 +51,11 @@ mod test_network { signature: Some(Bytes::copy_from_slice(&signature[..])), }; let meta = TransactionMeta { - block_id: "0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c" - .from_hex::>() - .unwrap() - .try_into() - .unwrap(), + block_id: U256::from_big_endian( + &"0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c" + .from_hex::>() + .unwrap(), + ), block_number: 17282695, block_timestamp: 1702858320, }; @@ -66,11 +66,9 @@ mod test_network { number: 17282695, id: existing_block_id(), size: 655, - parent_id: decode_hex( + parent_id: decode_u256( "0107b686375eabe6821225b0218ef5d51d0933756ca95d558c0a6b010f45f503", - ) - .try_into() - .unwrap(), + ), timestamp: 1702858320, gas_limit: 30000000, beneficiary: "0xb4094c25f86d628fdd571afc4077f0d0196afb48" @@ -78,22 +76,16 @@ mod test_network { .unwrap(), gas_used: 37918, total_score: 136509202, - txs_root: decode_hex( + txs_root: decode_u256( "0e3d83681601227e22c2eaa3dd5ef1c3301fe23dd7db21ac15984d7b6e2c6552", - ) - .try_into() - .unwrap(), + ), txs_features: 1, - state_root: decode_hex( + state_root: decode_u256( "dc94484d4f0d01b068dc1d66c5731dd36b32ba3b08bcfad977d599e5c9342dd1", - ) - .try_into() - .unwrap(), - receipts_root: decode_hex( + ), + receipts_root: decode_u256( "df5066746c62904390f2312e81fb7a98811098627a2ae8474457e69cd60b846a", - ) - .try_into() - .unwrap(), + ), com: true, signer: "0x0771bc0fe8b6dcf72372440c79a309e92ffb93e8" .parse() @@ -105,10 +97,10 @@ mod test_network { let exec_data= b"vtho-usd\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x84\x17\x19\x1a\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0e\x7f\x8eJ"; let transaction = BlockTransaction { transaction: ExtendedTransaction { - id: [ + id: U256::from_big_endian(&[ 234, 76, 61, 139, 131, 15, 119, 122, 229, 80, 82, 189, 146, 242, 198, 90, 233, 246, 195, 110, 179, 145, 172, 82, 232, 231, 125, 93, 43, 245, 243, 8, - ], + ]), origin: "0x56cb0e0276ad689cc68954d47460cd70f46244dc" .parse() .unwrap(), @@ -149,11 +141,11 @@ mod test_network { address: "0x12e3582d7ca22234f39d2a7be12c98ea9c077e25" .parse() .unwrap(), - topics: vec![[ + topics: vec![U256::from_big_endian(&[ 239, 200, 244, 4, 28, 11, 164, 208, 151, 229, 75, 206, 126, 91, 196, 126, 197, 180, 92, 2, 112, 196, 35, 121, 196, 182, 145, 200, 89, 67, 237, 240, - ]], + ])], data: Bytes::copy_from_slice(exec_data), }], transfers: vec![], @@ -252,26 +244,26 @@ mod test_network { address: "0x12e3582d7ca22234f39d2a7be12c98ea9c077e25" .parse() .unwrap(), - topics: vec![[ + topics: vec![U256::from_big_endian(&[ 239, 200, 244, 4, 28, 11, 164, 208, 151, 229, 75, 206, 126, 91, 196, 126, 197, 180, 92, 2, 112, 196, 35, 121, 196, 182, 145, 200, 89, 67, 237, 240, - ]], + ])], data: Bytes::copy_from_slice(&clause_data[..]), }], transfers: vec![], }], }; let expected_meta = ReceiptMeta { - block_id: [ + block_id: U256::from_big_endian(&[ 1, 7, 182, 135, 92, 112, 222, 176, 46, 218, 122, 103, 36, 137, 30, 119, 116, 179, 75, 138, 236, 197, 125, 40, 152, 243, 99, 132, 198, 166, 134, 140, - ], + ]), block_number: 17282695, block_timestamp: 1702858320, - tx_id: [ + tx_id: U256::from_big_endian(&[ 234, 76, 61, 139, 131, 15, 119, 122, 229, 80, 82, 189, 146, 242, 198, 90, 233, 246, 195, 110, 179, 145, 172, 82, 232, 231, 125, 93, 43, 245, 243, 8, - ], + ]), tx_origin: "0x56cb0e0276ad689cc68954d47460cd70f46244dc" .parse() .unwrap(), @@ -284,11 +276,9 @@ mod test_network { async fn test_fetch_existing_receipt_with_transfers() { let client = ThorNode::mainnet(); let (receipt, meta) = client - .fetch_transaction_receipt( - decode_hex("1755319a898a52fbceb4c58f51d63e6fa53592678512dd786de953d55895946a") - .try_into() - .unwrap(), - ) + .fetch_transaction_receipt(decode_u256( + "1755319a898a52fbceb4c58f51d63e6fa53592678512dd786de953d55895946a", + )) .await .expect("Must not fail") .expect("Must have been found"); @@ -315,16 +305,16 @@ mod test_network { }], }; let expected_meta = ReceiptMeta { - block_id: [ + block_id: U256::from_big_endian(&[ 1, 7, 123, 88, 83, 194, 165, 215, 242, 232, 190, 239, 4, 90, 10, 32, 219, 20, 208, 49, 108, 178, 89, 39, 71, 129, 5, 138, 112, 103, 10, 247, - ], + ]), block_number: 17267544, block_timestamp: 1703201660, - tx_id: [ + tx_id: U256::from_big_endian(&[ 23, 85, 49, 154, 137, 138, 82, 251, 206, 180, 197, 143, 81, 214, 62, 111, 165, 53, 146, 103, 133, 18, 221, 120, 109, 233, 83, 213, 88, 149, 148, 106, - ], + ]), tx_origin: "0xb0c224a96655ba8d51f35f98068f5fc12f930946" .parse() .unwrap(), @@ -337,11 +327,9 @@ mod test_network { async fn test_fetch_block() { let client = ThorNode::testnet(); let (blockinfo, transactions) = client - .fetch_block(BlockReference::ID( - decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c") - .try_into() - .unwrap(), - )) + .fetch_block(BlockReference::ID(decode_u256( + "0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c", + ))) .await .expect("Must not fail") .expect("Must have been found"); @@ -483,11 +471,9 @@ mod test_network { async fn test_fetch_nonexisting() { let client = ThorNode::testnet(); let result = client - .fetch_transaction( - decode_hex("ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") - .try_into() - .unwrap(), - ) + .fetch_transaction(decode_u256( + "ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308", + )) .await .expect("Must not fail"); assert!(result.is_none()); @@ -497,11 +483,9 @@ mod test_network { async fn test_fetch_nonexisting_ext() { let client = ThorNode::testnet(); let result = client - .fetch_extended_transaction( - decode_hex("ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") - .try_into() - .unwrap(), - ) + .fetch_extended_transaction(decode_u256( + "ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308", + )) .await .expect("Must not fail"); assert!(result.is_none()); @@ -511,11 +495,9 @@ mod test_network { async fn test_fetch_nonexisting_receipt() { let client = ThorNode::testnet(); let result = client - .fetch_transaction_receipt( - decode_hex("ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") - .try_into() - .unwrap(), - ) + .fetch_transaction_receipt(decode_u256( + "ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308", + )) .await .expect("Must not fail"); assert!(result.is_none()); @@ -525,11 +507,9 @@ mod test_network { async fn test_fetch_nonexisting_block() { let client = ThorNode::testnet(); let result = client - .fetch_block(BlockReference::ID( - decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868d") - .try_into() - .unwrap(), - )) + .fetch_block(BlockReference::ID(decode_u256( + "0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868d", + ))) .await .expect("Must not fail"); assert!(result.is_none()); @@ -539,11 +519,9 @@ mod test_network { async fn test_fetch_nonexisting_block_ext() { let client = ThorNode::testnet(); let result = client - .fetch_block_expanded(BlockReference::ID( - decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868d") - .try_into() - .unwrap(), - )) + .fetch_block_expanded(BlockReference::ID(decode_u256( + "0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868d", + ))) .await .expect("Must not fail"); assert!(result.is_none()); @@ -680,11 +658,9 @@ mod test_network { address: "0x12e3582d7ca22234f39d2a7be12c98ea9c077e25" .parse() .unwrap(), - topics: vec![decode_hex( + topics: vec![decode_u256( "efc8f4041c0ba4d097e54bce7e5bc47ec5b45c0270c42379c4b691c85943edf0" - ) - .try_into() - .unwrap()], + )], data: Bytes::copy_from_slice(&decode_hex(event_data)[..]), }], transfers: vec![], @@ -694,4 +670,20 @@ mod test_network { }] ); } + + #[tokio::test] + async fn test_broadcast_transaction_fail() { + use thor_devkit::rlp::Decodable; + + let node = ThorNode::testnet(); + let signed = decode_hex("f8804a880106f4db1482fd5a81b4e1e09477845a52acad7fe6a346f5b09e5e89e7caec8e3b890391c64cd2bc206c008080828ca08088a63565b632b9b7c3c0b841d76de99625a1a8795e467d509818701ec5961a8a4cf7cc2d75cee95f9ad70891013aaa4088919cc46df4f1e3f87b4ea44d002033fa3f7bd69485cb807aa2985100"); + let signed = Transaction::decode(&mut &signed[..]).unwrap(); + assert_eq!( + node.broadcast_transaction(&signed) + .await + .unwrap_err() + .to_string(), + "Failed to broadcast: bad tx: chain tag mismatch" + ); + } }