diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..472818e --- /dev/null +++ b/.clippy.toml @@ -0,0 +1 @@ +msrv = "1.76" diff --git a/.github/actions/install-just/action.yml b/.github/actions/install-just/action.yml new file mode 100644 index 0000000..e5f3dd6 --- /dev/null +++ b/.github/actions/install-just/action.yml @@ -0,0 +1,21 @@ +name: 'Install Just' +description: 'Installs Just command runner with caching' +runs: + using: "composite" + steps: + - name: Cache Just + uses: actions/cache@v4 + id: cache-just + with: + path: ~/.cargo/bin/just + key: ${{ runner.os }}-just-${{ hashFiles('**/justfile') }} + + - name: Install Just + if: steps.cache-just.outputs.cache-hit != 'true' + uses: taiki-e/install-action@v2 + with: + tool: just + + - name: Verify Just Installation + run: just --version + shell: bash diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml new file mode 100644 index 0000000..1d9c93a --- /dev/null +++ b/.github/actions/setup-rust/action.yml @@ -0,0 +1,12 @@ +name: "Setup Rust Environment" +description: "Setup Rust environment with caching" +runs: + using: "composite" + steps: + - name: "${{ matrix.toolchain }}" + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.toolchain }} + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1ad6b29 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,90 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + clippy: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-just + - run: just lint + + cargo-fmt: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-just + - run: just fmt + + cargo-doc: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-just + - run: RUSTDOCFLAGS="-D warnings" cargo doc + + build: + needs: [clippy, cargo-fmt, cargo-doc] + strategy: + matrix: + platform: [ubuntu-latest, macos-latest] + toolchain: [stable, nightly] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-just + - uses: ./.github/actions/setup-rust + - run: just check + + build-wasm: + needs: [clippy, cargo-fmt, cargo-doc] + strategy: + matrix: + platform: [ubuntu-latest, macos-latest] + toolchain: [stable, nightly] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-just + - uses: ./.github/actions/setup-rust + - name: Add wasm32 target + run: rustup target add wasm32-unknown-unknown + - name: Check for WASM target + run: just check-wasm + + unit-tests: + needs: [build, build-wasm] + strategy: + matrix: + platform: [ubuntu-latest, macos-latest] + toolchain: [stable, nightly] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-just + - uses: ./.github/actions/setup-rust + - uses: foundry-rs/foundry-toolchain@v1 + - name: Run unit tests + run: just test-unit + + integration-tests: + needs: [build, build-wasm] + strategy: + matrix: + platform: [ubuntu-latest, macos-latest] + toolchain: [stable, nightly] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-just + - uses: ./.github/actions/setup-rust + - uses: foundry-rs/foundry-toolchain@v1 + - name: Run unit tests + run: just test-integration diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..3a26366 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/Cargo.toml b/Cargo.toml index 73b2bc2..8fc5cad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,23 @@ overflow-checks = true rlp = "0.5.2" hex = "0.4.3" borsh = { version = "1.0.0", features = ["derive"] } -near-primitives = { version = "0.23" } -near-crypto = { version = "0.23" } near-sdk = "5.2.0" + [dev-dependencies] +# ethereum +alloy = { version = "0.2.1", features = ["full", "node-bindings", "rlp"] } +alloy-rlp = { version = "0.3.8", features = ["derive"] } +alloy-primitives = { version = "0.7.7" } + +# near +near-primitives = { version = "0.24.1" } +near-crypto = { version = "0.24.1" } + +# async +tokio = { version = "1.38", features = ["full"] } + +# misc +eyre = "0.6" +serde = "1.0" +serde_json = "1.0" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..749aa1e --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 68ecb25..077ddf0 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,52 @@ Library to be used inside Rust smart contracts to construct Transactions for dif ## Examples -Building NEAR transaction: +Building a NEAR transaction: ```rust -let bytes = TransactionBuilder::new() - .sender("alice.near".to_string()) - .signer_public_key([0u8; 64]) - .receiver("bob.near".to_string()) - .amount(100) - .build(ChainKind::NEAR); +let signer_id = "alice.near"; +let signer_public_key = [0u8; 64]; +let nonce = 0; +let receiver_id = "bob.near"; +let block_hash = [0u8; 32]; +let transfer_action = Action::Transfer(TransferAction { deposit: 1u128 }); +let actions = vec![transfer_action]; + +let near_tx = TransactionBuilder::new::() + .signer_id(signer_id.to_string()) + .signer_public_key(PublicKey::SECP256K1(signer_public_key.into())) + .nonce(nonce) + .receiver_id(receiver_id.to_string()) + .block_hash(block_hash) + .actions(actions) + .build(); + +// Now you have access to build_for_signing that returns the encoded payload +let near_tx_encoded = near_tx.build_for_signing(); ``` Building Ethereum transaction: ```rust -let bytes = TransactionBuilder::new() - .receiver("0123456789abcdefdeadbeef0123456789abcdef".to_string()) - .amount(100) - .build(ChainKind::EVM { chain_id: 1 }); +let to_address_str = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; +let to_address = parse_eth_address(to_address_str); +let max_gas_fee: u128 = 20_000_000_000; +let max_priority_fee_per_gas: u128 = 1_000_000_000; +let gas_limit: u128 = 21_000; +let chain_id: u64 = 1; +let nonce: u64 = 0; +let data: Vec = vec![]; +let value: u128 = 10000000000000000; // 0.01 ETH + +let evm_tx = TransactionBuilder::new::() + .nonce(nonce) + .to(to_address) + .value(value) + .input(data.clone()) + .max_priority_fee_per_gas(max_priority_fee_per_gas) + .max_fee_per_gas(max_gas_fee) + .gas_limit(gas_limit) + .chain_id(chain_id) + .build(); + +// Now you have access to build_for_signing that returns the encoded payload +let rlp_encoded = evm_tx.build_for_signing(); ``` diff --git a/justfile b/justfile new file mode 100644 index 0000000..278b8b8 --- /dev/null +++ b/justfile @@ -0,0 +1,23 @@ +# Run linting +lint: + cargo clippy --all-targets -- -D clippy::all -D clippy::nursery + +# Check formatting +fmt: + cargo fmt --check + +# Verify all compiles +check: + cargo check + +# Verify all compiles with wasm +check-wasm: + cargo check --target wasm32-unknown-unknown + +# Run unit tests +test-unit: + cargo test --lib + +# Run integration tests +test-integration: + cargo test --test '*' \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..db908d4 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy", "rust-analyzer"] +targets = ["wasm32-unknown-unknown"] diff --git a/src/bitcoin/.gitkeep b/src/bitcoin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..cdf2c2e --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,3 @@ +pub const EIP_1559_TYPE: u8 = 0x02; +pub const ED25519_PUBLIC_KEY_LENGTH: usize = 32; +pub const SECP256K1_PUBLIC_KEY_LENGTH: usize = 64; diff --git a/src/ethereum.rs b/src/ethereum.rs deleted file mode 100644 index a7292b2..0000000 --- a/src/ethereum.rs +++ /dev/null @@ -1,59 +0,0 @@ -use rlp::RlpStream; -use hex; - -type Address = [u8; 20]; -type AccessList = Vec<(Address, Vec<[u8; 32]>)>; - -const EIP_1559_TYPE: u8 = 0x02; - -pub fn parse_eth_address(address: &str) -> Address { - let address = hex::decode(address).expect("address should be hex"); - assert_eq!(address.len(), 20, "address should be 20 bytes long"); - let mut result = [0u8; 20]; - result.copy_from_slice(&address); - result -} - -pub fn ethereum_transaction(chain: u64, nonce: u128, max_priority_fee_per_gas: u128, max_fee_per_gas: u128, gas: u128, to: Option
, value: u128, data: Vec, access_list: AccessList) -> Vec { - let mut rlp_stream = RlpStream::new(); - - let to: Vec = match to { - Some(ref to) => to.to_vec(), - None => vec![], - }; - - rlp_stream.begin_unbounded_list(); - rlp_stream.append(&chain); - rlp_stream.append(&nonce); - rlp_stream.append(&max_priority_fee_per_gas); - rlp_stream.append(&max_fee_per_gas); - rlp_stream.append(&gas); - rlp_stream.append(&to); - rlp_stream.append(&value); - rlp_stream.append(&data); - - // Write access list. - { - rlp_stream.begin_unbounded_list(); - for access in access_list { - rlp_stream.begin_unbounded_list(); - rlp_stream.append(&access.0.to_vec()); - // Append list of storage keys. - { - rlp_stream.begin_unbounded_list(); - for storage_key in access.1 { - rlp_stream.append(&storage_key.to_vec()); - } - rlp_stream.finalize_unbounded_list(); - } - rlp_stream.finalize_unbounded_list(); - } - rlp_stream.finalize_unbounded_list(); - } - - rlp_stream.finalize_unbounded_list(); - let mut rlp_bytes = rlp_stream.out().to_vec(); - // Insert the type of transaction as the first byte. - rlp_bytes.insert(0usize, EIP_1559_TYPE); - rlp_bytes -} diff --git a/src/evm/evm_transaction.rs b/src/evm/evm_transaction.rs new file mode 100644 index 0000000..00c0785 --- /dev/null +++ b/src/evm/evm_transaction.rs @@ -0,0 +1,288 @@ +use rlp::RlpStream; + +use crate::constants::EIP_1559_TYPE; + +use super::types::{AccessList, Address, Signature}; + +pub struct EVMTransaction { + pub chain_id: u64, + pub nonce: u64, + pub to: Option
, + pub value: u128, + pub input: Vec, + pub gas_limit: u128, + pub max_fee_per_gas: u128, + pub max_priority_fee_per_gas: u128, + pub access_list: AccessList, +} + +impl EVMTransaction { + pub fn build_for_signing(&self) -> Vec { + let mut rlp_stream = RlpStream::new(); + + rlp_stream.append(&EIP_1559_TYPE); + + rlp_stream.begin_unbounded_list(); + + self.encode_fields(&mut rlp_stream); + + rlp_stream.finalize_unbounded_list(); + + rlp_stream.out().to_vec() + } + + pub fn build_with_signature(&self, signature: &Signature) -> Vec { + let mut rlp_stream = RlpStream::new(); + + rlp_stream.append(&EIP_1559_TYPE); + + rlp_stream.begin_unbounded_list(); + + self.encode_fields(&mut rlp_stream); + + rlp_stream.append(&signature.v); + rlp_stream.append(&signature.r); + rlp_stream.append(&signature.s); + + rlp_stream.finalize_unbounded_list(); + + rlp_stream.out().to_vec() + } + + fn encode_fields(&self, rlp_stream: &mut RlpStream) { + let to: Vec = self.to.map_or(vec![], |to| to.to_vec()); + let access_list = self.access_list.clone(); + + rlp_stream.append(&self.chain_id); + rlp_stream.append(&self.nonce); + rlp_stream.append(&self.max_priority_fee_per_gas); + rlp_stream.append(&self.max_fee_per_gas); + rlp_stream.append(&self.gas_limit); + rlp_stream.append(&to); + rlp_stream.append(&self.value); + rlp_stream.append(&self.input); + + // Write access list. + { + rlp_stream.begin_unbounded_list(); + for access in access_list { + rlp_stream.begin_unbounded_list(); + rlp_stream.append(&access.0.to_vec()); + // Append list of storage keys. + { + rlp_stream.begin_unbounded_list(); + for storage_key in access.1 { + rlp_stream.append(&storage_key.to_vec()); + } + rlp_stream.finalize_unbounded_list(); + } + rlp_stream.finalize_unbounded_list(); + } + rlp_stream.finalize_unbounded_list(); + } + } +} + +#[cfg(test)] +mod tests { + use alloy::{ + consensus::{SignableTransaction, TxEip1559}, + network::TransactionBuilder, + primitives::{address, hex, Address, Bytes, U256}, + rpc::types::{AccessList, TransactionRequest}, + }; + use alloy_primitives::{b256, Signature}; + + use crate::evm::types::Signature as OmniSignature; + use crate::evm::{evm_transaction::EVMTransaction, utils::parse_eth_address}; + const MAX_FEE_PER_GAS: u128 = 20_000_000_000; + const MAX_PRIORITY_FEE_PER_GAS: u128 = 1_000_000_000; + const GAS_LIMIT: u128 = 21_000; + + #[test] + fn test_build_for_signing_for_evm_against_alloy() { + let nonce: u64 = 0; + let to: Address = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); + let value = 10000000000000000u128; // 0.01 ETH + let data: Vec = vec![]; + let chain_id = 1; + let to_address_str = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + let to_address = Some(parse_eth_address(to_address_str)); + + // Generate using EVMTransaction + let tx = EVMTransaction { + chain_id, + nonce, + to: to_address, + value, + input: data.clone(), + gas_limit: GAS_LIMIT, + max_fee_per_gas: MAX_FEE_PER_GAS, + max_priority_fee_per_gas: MAX_PRIORITY_FEE_PER_GAS, + access_list: vec![], + }; + + let rlp_bytes = tx.build_for_signing(); + + println!( + "RLP Encoded Transaction usando OmniTransactionBuilder: {:?}", + rlp_bytes + ); + + // Now let's compare with the Alloy RLP encoding + let alloy_tx = TransactionRequest::default() + .with_chain_id(chain_id) + .with_nonce(nonce) + .with_to(to) + .with_value(U256::from(value)) + .with_max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .with_max_fee_per_gas(MAX_FEE_PER_GAS) + .with_gas_limit(GAS_LIMIT) + .with_input(data); + + let alloy_rlp_bytes: alloy::consensus::TypedTransaction = alloy_tx + .build_unsigned() + .expect("Failed to build unsigned transaction"); + + let rlp_encoded = alloy_rlp_bytes.eip1559().unwrap(); + + // Prepare the buffer and encode + let mut buf = vec![]; + rlp_encoded.encode_for_signing(&mut buf); + + println!("RLP Encoded Transaction usando Alloy: {:?}", buf); + println!("RLP Encoded Transaction usando la mia: {:?}", rlp_bytes); + + assert!(buf == rlp_bytes); + } + + #[test] + fn test_build_for_signing_with_data_for_evm_against_alloy() { + let input: Bytes = hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(); + let nonce: u64 = 0; + let to: Address = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); + let value = 10000000000000000u128; // 0.01 ETH + let chain_id = 1; + let to_address_str = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + let to_address = Some(parse_eth_address(to_address_str)); + + // Generate using EVMTransaction + let tx = EVMTransaction { + chain_id, + nonce, + to: to_address, + value, + input: input.to_vec(), + gas_limit: GAS_LIMIT, + max_fee_per_gas: MAX_FEE_PER_GAS, + max_priority_fee_per_gas: MAX_PRIORITY_FEE_PER_GAS, + access_list: vec![], + }; + + let rlp_bytes = tx.build_for_signing(); + + println!( + "RLP Encoded Transaction usando OmniTransactionBuilder: {:?}", + rlp_bytes + ); + + // Now let's compare with the Alloy RLP encoding + let alloy_tx = TransactionRequest::default() + .with_chain_id(chain_id) + .with_nonce(nonce) + .with_to(to) + .with_value(U256::from(value)) + .with_max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .with_max_fee_per_gas(MAX_FEE_PER_GAS) + .with_gas_limit(GAS_LIMIT) + .access_list(AccessList::default()) + .with_input(input); + + let alloy_rlp_bytes: alloy::consensus::TypedTransaction = alloy_tx + .build_unsigned() + .expect("Failed to build unsigned transaction"); + + let rlp_encoded = alloy_rlp_bytes.eip1559().unwrap(); + + // Prepare the buffer and encode + let mut buf = vec![]; + rlp_encoded.encode_for_signing(&mut buf); + + println!("RLP Encoded Transaction usando Alloy: {:?}", buf); + + assert!(buf == rlp_bytes); + } + + #[test] + fn test_build_with_signature_for_evm_against_alloy() { + let chain_id = 1; + let nonce = 0x42; + let gas_limit = 44386; + + let to_str = "6069a6c32cf691f5982febae4faf8a6f3ab2f0f6"; + let to = address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6").into(); + let to_address = Some(parse_eth_address(to_str)); + let value_as_128 = 0_u128; + let value = U256::from(value_as_128); + + let max_fee_per_gas = 0x4a817c800; + let max_priority_fee_per_gas = 0x3b9aca00; + let input: Bytes = hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(); + + let tx: TxEip1559 = TxEip1559 { + chain_id, + nonce, + gas_limit, + to, + value, + input: input.clone(), + max_fee_per_gas, + max_priority_fee_per_gas, + access_list: AccessList::default(), + }; + + let mut tx_encoded = vec![]; + tx.encode_for_signing(&mut tx_encoded); + + // Generate using EVMTransaction + let tx_omni = EVMTransaction { + chain_id, + nonce, + to: to_address, + value: value_as_128, + input: input.to_vec(), + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + access_list: vec![], + }; + + let rlp_bytes_for_omni_tx = tx_omni.build_for_signing(); + + assert_eq!(tx_encoded.len(), rlp_bytes_for_omni_tx.len()); + + let sig = Signature::from_scalars_and_parity( + b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"), + b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"), + false, + ) + .unwrap(); + + let mut tx_encoded_with_signature: Vec = vec![]; + tx.encode_with_signature(&sig, &mut tx_encoded_with_signature, false); + + let signature: OmniSignature = OmniSignature { + v: sig.v().to_u64(), + r: sig.r().to_be_bytes::<32>().to_vec(), + s: sig.s().to_be_bytes::<32>().to_vec(), + }; + + let omni_encoded_with_signature = tx_omni.build_with_signature(&signature); + + assert_eq!( + tx_encoded_with_signature.len(), + omni_encoded_with_signature.len() + ); + assert_eq!(tx_encoded_with_signature, omni_encoded_with_signature); + } +} diff --git a/src/evm/evm_transaction_builder.rs b/src/evm/evm_transaction_builder.rs new file mode 100644 index 0000000..5b0bec7 --- /dev/null +++ b/src/evm/evm_transaction_builder.rs @@ -0,0 +1,227 @@ +use crate::transaction_builder::TxBuilder; + +use super::{ + evm_transaction::EVMTransaction, + types::{AccessList, Address}, +}; + +pub struct EVMTransactionBuilder { + chain_id: Option, + nonce: Option, + to: Option
, + value: Option, + input: Option>, + gas_limit: Option, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + access_list: Option, +} + +impl Default for EVMTransactionBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TxBuilder for EVMTransactionBuilder { + fn build(&self) -> EVMTransaction { + EVMTransaction { + chain_id: self.chain_id.expect("chain_id is mandatory"), + nonce: self.nonce.expect("nonce is mandatory"), + to: self.to, + value: self.value.unwrap_or_default(), + input: self.input.clone().unwrap_or_default(), + gas_limit: self.gas_limit.expect("gas_limit is mandatory"), + max_fee_per_gas: self.max_fee_per_gas.expect("max_fee_per_gas is mandatory"), + max_priority_fee_per_gas: self.max_priority_fee_per_gas.unwrap_or_default(), + access_list: self.access_list.clone().unwrap_or_default(), + } + } +} + +impl EVMTransactionBuilder { + pub const fn new() -> Self { + Self { + chain_id: None, + nonce: None, + to: None, + value: None, + input: None, + gas_limit: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + access_list: None, + } + } + + /// Chain ID of the transaction. + pub const fn chain_id(mut self, chain_id: u64) -> Self { + self.chain_id = Some(chain_id); + self + } + + /// Nonce of the transaction. + pub const fn nonce(mut self, nonce: u64) -> Self { + self.nonce = Some(nonce); + self + } + + /// Address of the recipient. + pub const fn to(mut self, to: Address) -> Self { + self.to = Some(to); + self + } + + /// Value attached to the transaction. + pub const fn value(mut self, value: u128) -> Self { + self.value = Some(value); + self + } + + /// Input data of the transaction. + pub fn input(mut self, input: Vec) -> Self { + self.input = Some(input); + self + } + + /// Gas limit of the transaction. + pub const fn gas_limit(mut self, gas_limit: u128) -> Self { + self.gas_limit = Some(gas_limit); + self + } + + /// Maximum fee per gas of the transaction. + pub const fn max_fee_per_gas(mut self, max_fee_per_gas: u128) -> Self { + self.max_fee_per_gas = Some(max_fee_per_gas); + self + } + + /// Maximum priority fee per gas of the transaction. + pub const fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: u128) -> Self { + self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); + self + } + + /// Access list of the transaction. + pub fn access_list(mut self, access_list: AccessList) -> Self { + self.access_list = Some(access_list); + self + } +} + +#[cfg(test)] +mod tests { + use alloy::{ + consensus::SignableTransaction, + network::TransactionBuilder, + primitives::{address, hex, Address, Bytes, U256}, + rpc::types::{AccessList, TransactionRequest}, + }; + + use crate::{ + evm::{evm_transaction_builder::EVMTransactionBuilder, utils::parse_eth_address}, + transaction_builder::TxBuilder, + }; + + const MAX_FEE_PER_GAS: u128 = 20_000_000_000; + const MAX_PRIORITY_FEE_PER_GAS: u128 = 1_000_000_000; + const GAS_LIMIT: u128 = 21_000; + + #[test] + fn test_evm_transaction_builder_against_alloy() { + let nonce: u64 = 0; + let to: Address = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); + let value = 10000000000000000u128; // 0.01 ETH + let data: Vec = vec![]; + let chain_id = 1; + let to_address_str = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + let to_address = parse_eth_address(to_address_str); + + // Generate using EVMTransactionBuilder + let tx_1 = EVMTransactionBuilder::new() + .chain_id(chain_id) + .nonce(nonce) + .max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .max_fee_per_gas(MAX_FEE_PER_GAS) + .gas_limit(GAS_LIMIT) + .to(to_address) + .value(value) + .input(data.clone()) + .access_list(vec![]) + .build(); + + let rlp_bytes = tx_1.build_for_signing(); + + // Now let's compare with the Alloy RLP encoding + let alloy_tx = TransactionRequest::default() + .with_chain_id(chain_id) + .with_nonce(nonce) + .with_to(to) + .with_value(U256::from(value)) + .with_max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .with_max_fee_per_gas(MAX_FEE_PER_GAS) + .with_gas_limit(GAS_LIMIT) + .with_input(data); + + let alloy_rlp_bytes: alloy::consensus::TypedTransaction = alloy_tx + .build_unsigned() + .expect("Failed to build unsigned transaction"); + + let rlp_encoded = alloy_rlp_bytes.eip1559().unwrap(); + + let mut rlp_alloy_bytes = vec![]; + rlp_encoded.encode_for_signing(&mut rlp_alloy_bytes); + + assert!(rlp_alloy_bytes == rlp_bytes); + } + + #[test] + fn test_evm_transaction_builder_with_data_against_alloy() { + let input: Bytes = hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(); + let nonce: u64 = 0; + let to: Address = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); + let value = 10000000000000000u128; // 0.01 ETH + let chain_id = 1; + let to_address_str = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + let to_address = parse_eth_address(to_address_str); + + // Generate using EVMTransactionBuilder + let evm_transaction = EVMTransactionBuilder::new() + .chain_id(chain_id) + .nonce(nonce) + .max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .max_fee_per_gas(MAX_FEE_PER_GAS) + .gas_limit(GAS_LIMIT) + .to(to_address) + .value(value) + .input(input.to_vec()) + .access_list(vec![]) + .build(); + + let rlp_bytes = evm_transaction.build_for_signing(); + + // Now let's compare with the Alloy RLP encoding + let alloy_tx = TransactionRequest::default() + .with_chain_id(chain_id) + .with_nonce(nonce) + .with_to(to) + .with_value(U256::from(value)) + .with_max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .with_max_fee_per_gas(MAX_FEE_PER_GAS) + .with_gas_limit(GAS_LIMIT) + .access_list(AccessList::default()) + .with_input(input); + + let alloy_rlp_bytes: alloy::consensus::TypedTransaction = alloy_tx + .build_unsigned() + .expect("Failed to build unsigned transaction"); + + let rlp_encoded = alloy_rlp_bytes.eip1559().unwrap(); + + // Prepare the buffer and encode + let mut rlp_encoded_encoded_for_signing = vec![]; + rlp_encoded.encode_for_signing(&mut rlp_encoded_encoded_for_signing); + + assert!(rlp_encoded_encoded_for_signing == rlp_bytes); + } +} diff --git a/src/evm/mod.rs b/src/evm/mod.rs new file mode 100644 index 0000000..2e6ff91 --- /dev/null +++ b/src/evm/mod.rs @@ -0,0 +1,4 @@ +pub mod evm_transaction; +pub mod evm_transaction_builder; +pub mod types; +pub mod utils; diff --git a/src/evm/types.rs b/src/evm/types.rs new file mode 100644 index 0000000..9f1f75d --- /dev/null +++ b/src/evm/types.rs @@ -0,0 +1,9 @@ +pub type Address = [u8; 20]; + +pub type AccessList = Vec<(Address, Vec<[u8; 32]>)>; + +pub struct Signature { + pub v: u64, + pub r: Vec, + pub s: Vec, +} diff --git a/src/evm/utils.rs b/src/evm/utils.rs new file mode 100644 index 0000000..9c7240f --- /dev/null +++ b/src/evm/utils.rs @@ -0,0 +1,11 @@ +use hex; + +use super::types::Address; + +pub fn parse_eth_address(address: &str) -> Address { + let address = hex::decode(address).expect("address should be hex"); + assert_eq!(address.len(), 20, "address should be 20 bytes long"); + let mut result = [0u8; 20]; + result.copy_from_slice(&address); + result +} diff --git a/src/lib.rs b/src/lib.rs index 40bb4d7..74524a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ - -mod ethereum; -mod near; - -pub mod transaction; +pub mod constants; +pub mod evm; +pub mod near; +pub mod transaction_builder; pub mod types; diff --git a/src/near.rs b/src/near.rs deleted file mode 100644 index 0876fa4..0000000 --- a/src/near.rs +++ /dev/null @@ -1,19 +0,0 @@ - -use near_primitives::account::id::AccountId; -use near_primitives::transaction::{Transaction, Action}; -use near_crypto::PublicKey; -use near_primitives::hash::CryptoHash; -use near_sdk::borsh; - - -pub fn near_transaction(signer_id: String, public_key: [u8; 64], nonce: u64, receiver_id: String, actions: Vec) -> Vec { - let tx = Transaction { - signer_id: AccountId::new_unvalidated(signer_id), - public_key: PublicKey::SECP256K1(public_key.into()), - nonce, - receiver_id: AccountId::new_unvalidated(receiver_id), - block_hash: CryptoHash([0; 32]), - actions - }; - borsh::to_vec(&tx).expect("failed to serialize NEAR transaction") -} diff --git a/src/near/mod.rs b/src/near/mod.rs new file mode 100644 index 0000000..c533def --- /dev/null +++ b/src/near/mod.rs @@ -0,0 +1,3 @@ +pub mod near_transaction; +pub mod near_transaction_builder; +pub mod types; diff --git a/src/near/near_transaction.rs b/src/near/near_transaction.rs new file mode 100644 index 0000000..8dc87af --- /dev/null +++ b/src/near/near_transaction.rs @@ -0,0 +1,82 @@ +use borsh::BorshSerialize; +use near_sdk::{borsh, AccountId}; + +use super::types::{Action, PublicKey}; + +#[derive(Debug, Clone, BorshSerialize)] +pub struct NearTransaction { + /// An account on which behalf transaction is signed + pub signer_id: AccountId, + /// A public key of the access key which was used to sign an account. + /// Access key holds permissions for calling certain kinds of actions. + pub signer_public_key: PublicKey, + /// Nonce is used to determine order of transaction in the pool. + /// It increments for a combination of `signer_id` and `public_key` + pub nonce: u64, + /// Receiver account for this transaction + pub receiver_id: AccountId, + /// The hash of the block in the blockchain on top of which the given transaction is valid + pub block_hash: [u8; 32], + /// A list of actions to be applied + pub actions: Vec, +} + +impl NearTransaction { + pub fn build_for_signing(&self) -> Vec { + let tx = Self { + signer_id: self.signer_id.clone(), + signer_public_key: self.signer_public_key.clone(), + nonce: self.nonce, + receiver_id: self.receiver_id.clone(), + block_hash: self.block_hash, + actions: self.actions.clone(), + }; + borsh::to_vec(&tx).expect("failed to serialize NEAR transaction") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::near::types::{ + Action as OmniAction, PublicKey as OmniPublicKey, TransferAction as OmniTransferAction, + }; + use near_crypto::PublicKey; + use near_primitives::{ + action::Action, action::TransferAction, hash::CryptoHash, transaction::TransactionV0, + }; + + #[test] + fn test_build_for_signing_for_near_against_near_primitives() { + let signer_id = "alice.near"; + let signer_public_key = [0u8; 64]; + let nonce = 0; + let receiver_id: &str = "bob.near"; + let actions = Action::Transfer(TransferAction { deposit: 1u128 }); + let omni_actions = OmniAction::Transfer(OmniTransferAction { deposit: 1u128 }); + + let v0_tx: TransactionV0 = TransactionV0 { + signer_id: signer_id.parse().unwrap(), + public_key: PublicKey::SECP256K1(signer_public_key.into()), + nonce, + receiver_id: receiver_id.parse().unwrap(), + block_hash: CryptoHash([0; 32]), + actions: vec![actions], + }; + + let serialized_v0_tx = borsh::to_vec(&v0_tx).expect("failed to serialize NEAR transaction"); + + let omni_tx = NearTransaction { + signer_id: signer_id.parse().unwrap(), + signer_public_key: OmniPublicKey::SECP256K1(signer_public_key.into()), + nonce, + receiver_id: receiver_id.parse().unwrap(), + block_hash: [0u8; 32], + actions: vec![omni_actions], + }; + + let serialized_omni_tx = omni_tx.build_for_signing(); + + assert!(serialized_v0_tx == serialized_omni_tx); + } +} diff --git a/src/near/near_transaction_builder.rs b/src/near/near_transaction_builder.rs new file mode 100644 index 0000000..8db5b9d --- /dev/null +++ b/src/near/near_transaction_builder.rs @@ -0,0 +1,137 @@ +use super::{ + near_transaction::NearTransaction, + types::{Action, PublicKey}, +}; +use crate::transaction_builder::TxBuilder; + +pub struct NearTransactionBuilder { + pub signer_id: Option, + pub signer_public_key: Option, + pub nonce: Option, + pub receiver_id: Option, + pub block_hash: Option<[u8; 32]>, + pub actions: Option>, +} + +impl Default for NearTransactionBuilder { + fn default() -> Self { + Self::new() + } +} + +impl TxBuilder for NearTransactionBuilder { + fn build(&self) -> NearTransaction { + NearTransaction { + signer_id: self + .signer_id + .clone() + .expect("Missing sender ID") + .parse() + .unwrap(), + signer_public_key: self + .signer_public_key + .clone() + .expect("Missing signer public key"), + nonce: self.nonce.expect("Missing nonce"), + receiver_id: self + .receiver_id + .clone() + .expect("Missing receiver ID") + .parse() + .unwrap(), + block_hash: self.block_hash.expect("Missing block hash"), + actions: self.actions.clone().expect("Missing actions"), + } + } +} + +impl NearTransactionBuilder { + pub const fn new() -> Self { + Self { + signer_id: None, + signer_public_key: None, + nonce: None, + receiver_id: None, + block_hash: None, + actions: None, + } + } + + pub fn signer_id(mut self, signer_id: String) -> Self { + self.signer_id = Some(signer_id); + self + } + + pub const fn signer_public_key(mut self, signer_public_key: PublicKey) -> Self { + self.signer_public_key = Some(signer_public_key); + self + } + + pub const fn nonce(mut self, nonce: u64) -> Self { + self.nonce = Some(nonce); + self + } + + pub fn receiver_id(mut self, receiver_id: String) -> Self { + self.receiver_id = Some(receiver_id); + self + } + + pub const fn block_hash(mut self, block_hash: [u8; 32]) -> Self { + self.block_hash = Some(block_hash); + self + } + + pub fn actions(mut self, actions: Vec) -> Self { + self.actions = Some(actions); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::near::types::{ + Action as OmniAction, PublicKey as OmniPublicKey, TransferAction as OmniTransferAction, + }; + use near_crypto::PublicKey; + use near_primitives::{ + action::Action, action::TransferAction, hash::CryptoHash, transaction::TransactionV0, + }; + + #[test] + fn test_near_transaction_builder_against_near_primitives() { + let signer_id = "alice.near"; + let signer_public_key = [0u8; 64]; + let nonce = 0; + let receiver_id: &str = "bob.near"; + let block_hash = [0u8; 32]; + let transfer_action = OmniAction::Transfer(OmniTransferAction { deposit: 1u128 }); + let omni_actions = vec![transfer_action]; + let actions = Action::Transfer(TransferAction { deposit: 1u128 }); + + let omni_near_transaction = NearTransactionBuilder::new() + .signer_id(signer_id.to_string()) + .signer_public_key(OmniPublicKey::SECP256K1(signer_public_key.into())) + .nonce(nonce) + .receiver_id(receiver_id.to_string()) + .block_hash(block_hash) + .actions(omni_actions) + .build(); + + let omni_tx_encoded = omni_near_transaction.build_for_signing(); + + let v0_tx: TransactionV0 = TransactionV0 { + signer_id: signer_id.parse().unwrap(), + public_key: PublicKey::SECP256K1(signer_public_key.into()), + nonce, + receiver_id: receiver_id.parse().unwrap(), + block_hash: CryptoHash([0; 32]), + actions: vec![actions], + }; + + let serialized_v0_tx = borsh::to_vec(&v0_tx).expect("failed to serialize NEAR transaction"); + + assert!(serialized_v0_tx == omni_tx_encoded); + } +} diff --git a/src/near/types.rs b/src/near/types.rs new file mode 100644 index 0000000..043861b --- /dev/null +++ b/src/near/types.rs @@ -0,0 +1,86 @@ +use crate::constants::{ED25519_PUBLIC_KEY_LENGTH, SECP256K1_PUBLIC_KEY_LENGTH}; + +use std::io::{Error, Write}; + +use borsh::BorshSerialize; + +// Actions +#[derive(Debug, Clone, BorshSerialize)] +pub enum Action { + /// Create an (sub)account using a transaction `receiver_id` as an ID for + /// a new account ID must pass validation rules described here + /// . + CreateAccount(CreateAccountAction), + /// Sets a Wasm code to a receiver_id + DeployContract(DeployContractAction), + FunctionCall(Box), + Transfer(TransferAction), + Stake(Box), +} + +#[derive(Debug, Clone, BorshSerialize)] +pub struct CreateAccountAction {} + +#[derive(Debug, Clone, BorshSerialize)] +pub struct DeployContractAction { + pub code: Vec, +} + +#[derive(Debug, Clone, BorshSerialize)] +pub struct FunctionCallAction { + pub method_name: String, + pub args: Vec, + pub gas: u64, + pub deposit: u128, +} + +#[derive(Debug, Clone, BorshSerialize)] +pub struct TransferAction { + pub deposit: u128, +} + +#[derive(Debug, Clone, BorshSerialize)] +pub struct StakeAction { + /// Amount of tokens to stake. + pub stake: u128, + /// Validator key which will be used to sign transactions on behalf of signer_id + pub public_key: PublicKey, +} + +// Public Key + +#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)] +pub struct Secp256K1PublicKey([u8; SECP256K1_PUBLIC_KEY_LENGTH]); + +#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)] +pub struct ED25519PublicKey(pub [u8; ED25519_PUBLIC_KEY_LENGTH]); + +#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)] +pub enum PublicKey { + /// 256 bit elliptic curve based public-key. + ED25519(ED25519PublicKey), + /// 512 bit elliptic curve based public-key used in Bitcoin's public-key cryptography. + SECP256K1(Secp256K1PublicKey), +} + +impl BorshSerialize for PublicKey { + fn serialize(&self, writer: &mut W) -> Result<(), Error> { + match self { + Self::ED25519(public_key) => { + BorshSerialize::serialize(&0u8, writer)?; + writer.write_all(&public_key.0)?; + } + Self::SECP256K1(public_key) => { + BorshSerialize::serialize(&1u8, writer)?; + writer.write_all(&public_key.0)?; + } + } + Ok(()) + } +} + +impl From<[u8; 64]> for Secp256K1PublicKey { + fn from(data: [u8; 64]) -> Self { + Self(data) + } +} diff --git a/src/transaction.rs b/src/transaction.rs deleted file mode 100644 index ac601fb..0000000 --- a/src/transaction.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::types::ChainKind; -use crate::near::{near_transaction}; -use crate::ethereum::{parse_eth_address, ethereum_transaction}; - -// Multichain transaction builder. -pub struct TransactionBuilder { - nonce: Option, - sender_id: Option, - signer_public_key: Option<[u8; 64]>, - receiver_id: Option, - amount: Option, - bytecode: Option>, - gas_price: Option, - gas_limit: Option, -} - -impl TransactionBuilder { - pub fn new() -> Self { - Self { - nonce: None, - sender_id: None, - signer_public_key: None, - receiver_id: None, - amount: None, - bytecode: None, - gas_price: None, - gas_limit: None, - } - } - - /// Nonce of the transaction. - pub fn nonce(mut self, nonce: u64) -> Self { - self.nonce = Some(nonce); - self - } - - /// Sender of the transaction. - pub fn sender(mut self, sender_id: String) -> Self { - self.sender_id = Some(sender_id); - self - } - - /// Sender's public key of the transaction. - pub fn signer_public_key(mut self, signer_public_key: [u8; 64]) -> Self { - self.signer_public_key = Some(signer_public_key); - self - } - - /// Recevier of the transaction. - pub fn receiver(mut self, receiver_id: String) -> Self { - self.receiver_id = Some(receiver_id); - self - } - - /// Amount attached to the transaction. - pub fn amount(mut self, amount: u128) -> Self { - self.amount = Some(amount); - self - } - - /// Deploy contract with the given bytecode. - pub fn deploy_contract(mut self, bytecode: &[u8]) -> Self { - self.bytecode = Some(bytecode.to_vec()); - self - } - - pub fn gas_price(mut self, gas_price: u128) -> Self { - self.gas_price = Some(gas_price); - self - } - - pub fn gas_limit(mut self, gas_limit: u128) -> Self { - self.gas_limit = Some(gas_limit); - self - } - - /// Build a transaction for the given chain into serialized payload. - pub fn build(self, chain_kind: ChainKind) -> Vec { - match chain_kind { - ChainKind::NEAR => { - // Build a NEAR transaction - near_transaction( - self.sender_id.expect("Missing sender ID"), - self.signer_public_key.expect("Missing signer public key"), - self.nonce.unwrap_or(0), - self.receiver_id.unwrap_or("".to_string()), - vec![] - ) - } - ChainKind::EVM { chain_id } => { - // Build an EVM transaction - let to = parse_eth_address(self.receiver_id.unwrap().as_str()); - ethereum_transaction( - chain_id, - self.nonce.unwrap_or(0).into(), - 1, - self.gas_price.unwrap_or(1), - self.gas_limit.unwrap_or(1), - Some(to), - // self.receiver_id.unwrap_or("".to_string()).parse().unwrap(), - self.amount.unwrap_or(0), - vec![], - vec![], - ) - } - ChainKind::Solana => { - // Build a Solana transaction - unimplemented!() - } - ChainKind::Cosmos { chain_id } => { - // Build a Cosmos transaction - unimplemented!() - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use hex; - - #[test] - fn test_build_near_transaction() { - let tx = TransactionBuilder::new() - .sender("alice.near".to_string()) - .signer_public_key([0u8; 64]) - .receiver("alice.near".to_string()) - .amount(100) - .build(ChainKind::NEAR); - assert_eq!(hex::encode(tx), "0a000000616c6963652e6e656172010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000616c6963652e6e656172000000000000000000000000000000000000000000000000000000000000000000000000"); - } - - #[test] - fn test_build_ethereum_transaction() { - let tx = TransactionBuilder::new() - .receiver("0123456789abcdefdeadbeef0123456789abcdef".to_string()) - .amount(100) - .build(ChainKind::EVM { chain_id: 1 }); - assert_eq!(hex::encode(tx), "02dd0180010101940123456789abcdefdeadbeef0123456789abcdef6480c0"); - } -} \ No newline at end of file diff --git a/src/transaction_builder.rs b/src/transaction_builder.rs new file mode 100644 index 0000000..4aca4b5 --- /dev/null +++ b/src/transaction_builder.rs @@ -0,0 +1,125 @@ +pub trait TxBuilder { + fn build(&self) -> T; +} + +pub struct TransactionBuilder; + +impl TransactionBuilder { + #[allow(clippy::new_ret_no_self)] + pub fn new() -> T + where + T: Default, + { + T::default() + } +} + +#[cfg(test)] +mod tests { + + use super::{TransactionBuilder as OmniTransactionBuilder, TxBuilder}; + use crate::near::types::{ + Action as OmniAction, PublicKey as OmniPublicKey, TransferAction as OmniTransferAction, + }; + use crate::{ + evm::utils::parse_eth_address, + types::{EVM, NEAR}, + }; + use alloy::{ + consensus::SignableTransaction, + network::TransactionBuilder, + primitives::{address, Address, U256}, + rpc::types::TransactionRequest, + }; + use near_crypto::PublicKey; + use near_primitives::{ + action::Action, action::TransferAction, hash::CryptoHash, transaction::TransactionV0, + }; + + #[test] + fn test_near_transaction_builder_typed() { + let signer_id = "alice.near"; + let signer_public_key = [0u8; 64]; + let nonce = 0; + let receiver_id: &str = "bob.near"; + let block_hash = [0u8; 32]; + let transfer_action = OmniAction::Transfer(OmniTransferAction { deposit: 1u128 }); + let omni_actions = vec![transfer_action]; + let actions = Action::Transfer(TransferAction { deposit: 1u128 }); + + let omni_near_transaction = OmniTransactionBuilder::new::() + .signer_id(signer_id.to_string()) + .signer_public_key(OmniPublicKey::SECP256K1(signer_public_key.into())) + .nonce(nonce) + .receiver_id(receiver_id.to_string()) + .block_hash(block_hash) + .actions(omni_actions) + .build(); + + let omni_tx_encoded = omni_near_transaction.build_for_signing(); + + let v0_tx: TransactionV0 = TransactionV0 { + signer_id: signer_id.parse().unwrap(), + public_key: PublicKey::SECP256K1(signer_public_key.into()), + nonce, + receiver_id: receiver_id.parse().unwrap(), + block_hash: CryptoHash([0; 32]), + actions: vec![actions], + }; + + let serialized_v0_tx = borsh::to_vec(&v0_tx).expect("failed to serialize NEAR transaction"); + + assert!(serialized_v0_tx == omni_tx_encoded); + } + + #[test] + fn test_evm_transaction_builder_typed() { + const MAX_FEE_PER_GAS: u128 = 20_000_000_000; + const MAX_PRIORITY_FEE_PER_GAS: u128 = 1_000_000_000; + const GAS_LIMIT: u128 = 21_000; + + let nonce: u64 = 0; + let to: Address = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); + let value = 10000000000000000u128; // 0.01 ETH + let data: Vec = vec![]; + let chain_id = 1; + let to_address_str = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + let to_address = parse_eth_address(to_address_str); + + let tx = OmniTransactionBuilder::new::() + .chain_id(chain_id) + .nonce(nonce) + .max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .max_fee_per_gas(MAX_FEE_PER_GAS) + .gas_limit(GAS_LIMIT) + .to(to_address) + .value(value) + .input(data.clone()) + .access_list(vec![]) + .build(); + + let rlp_bytes = tx.build_for_signing(); + + // Now let's compare with the Alloy RLP encoding + let alloy_tx = TransactionRequest::default() + .with_chain_id(chain_id) + .with_nonce(nonce) + .with_to(to) + .with_value(U256::from(value)) + .with_max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .with_max_fee_per_gas(MAX_FEE_PER_GAS) + .with_gas_limit(GAS_LIMIT) + .with_input(data); + + let alloy_rlp_bytes: alloy::consensus::TypedTransaction = alloy_tx + .build_unsigned() + .expect("Failed to build unsigned transaction"); + + let rlp_encoded = alloy_rlp_bytes.eip1559().unwrap(); + + let mut rlp_alloy_bytes = vec![]; + rlp_encoded.encode_for_signing(&mut rlp_alloy_bytes); + + assert!(rlp_alloy_bytes == rlp_bytes); + } +} diff --git a/src/types.rs b/src/types.rs index 7079bec..3062b4a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,18 +1,6 @@ -use near_sdk::serde::{Serialize, Deserialize}; -use near_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use crate::evm::evm_transaction_builder::EVMTransactionBuilder; +use crate::near::near_transaction_builder::NearTransactionBuilder; -#[derive(Serialize, Deserialize, BorshDeserialize, BorshSerialize, Clone, PartialEq, Eq, PartialOrd, Hash)] -#[serde(crate = "near_sdk::serde")] -pub enum ChainKind { - NEAR, - EVM { chain_id: u64 }, - Solana, - Cosmos { chain_id: String }, -} +pub type NEAR = NearTransactionBuilder; -#[derive(Serialize, Deserialize, BorshDeserialize, BorshSerialize, PartialEq, Eq)] -#[serde(crate = "near_sdk::serde")] -pub struct OmniAddress { - pub chain_kind: ChainKind, - pub address: String, -} +pub type EVM = EVMTransactionBuilder; diff --git a/tests/transaction_integration_test.rs b/tests/transaction_integration_test.rs new file mode 100644 index 0000000..8f09939 --- /dev/null +++ b/tests/transaction_integration_test.rs @@ -0,0 +1,90 @@ +use alloy::providers::Provider; +use alloy::signers::Signer; +use alloy::{ + network::EthereumWallet, node_bindings::Anvil, providers::ProviderBuilder, + signers::local::PrivateKeySigner, +}; +use alloy_primitives::{keccak256, U256}; +use eyre::Result; +use std::result::Result::Ok; + +use omni_transaction::evm::types::Signature as OmniSignature; +use omni_transaction::evm::utils::parse_eth_address; +use omni_transaction::transaction_builder::{ + TransactionBuilder as OmniTransactionBuilder, TxBuilder, +}; +use omni_transaction::types::EVM; + +const MAX_FEE_PER_GAS: u128 = 20_000_000_000; +const MAX_PRIORITY_FEE_PER_GAS: u128 = 1_000_000_000; +const GAS_LIMIT: u128 = 21_000; + +#[tokio::test] +async fn test_send_raw_transaction_created_with_omnitransactionbuilder() -> Result<()> { + let nonce: u64 = 0; + let to_address_str = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; + let to_address = parse_eth_address(to_address_str); + let value_as_128 = 10000000000000000u128; // 0.01 ETH + let value = U256::from(value_as_128); + let data: Vec = vec![]; + + // Spin up a local Anvil node. + let anvil = Anvil::new().block_time(1).try_spawn()?; + + // Configure the signer from the first default Anvil account (Alice). + let signer: PrivateKeySigner = anvil.keys()[0].clone().into(); + let wallet = EthereumWallet::from(signer.clone()); + + // Create a provider with the wallet. + let rpc_url = anvil.endpoint().parse()?; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet.clone()) + .on_http(rpc_url); + + let signer_balance = provider.get_balance(signer.address()).await?; + + assert!(signer_balance >= value); + + // ========= OmniTransactionBuilder tx hash and signature ========= + + // Build the transaction using OmniTransactionBuilder + let omni_evm_tx = OmniTransactionBuilder::new::() + .nonce(nonce) + .to(to_address) + .value(value_as_128) + .input(data.clone()) + .max_priority_fee_per_gas(MAX_PRIORITY_FEE_PER_GAS) + .max_fee_per_gas(MAX_FEE_PER_GAS) + .gas_limit(GAS_LIMIT) + .chain_id(anvil.chain_id()) + .build(); + + // Encode the transaction with EIP-1559 prefix + let omni_evm_tx_encoded = omni_evm_tx.build_for_signing(); + + // Hash the encoded transaction + let omni_evm_tx_hash = keccak256(&omni_evm_tx_encoded); + + // Sign the transaction hash + let signature = signer.sign_hash(&omni_evm_tx_hash).await?; + + let signature_omni: OmniSignature = OmniSignature { + v: signature.v().to_u64(), + r: signature.r().to_be_bytes::<32>().to_vec(), + s: signature.s().to_be_bytes::<32>().to_vec(), + }; + + let omni_evm_tx_encoded_with_signature = omni_evm_tx.build_with_signature(&signature_omni); + + // Send the transaction + match provider + .send_raw_transaction(&omni_evm_tx_encoded_with_signature) + .await + { + Ok(tx_hash) => println!("Transaction sent successfully. Hash: {:?}", tx_hash), + Err(e) => println!("Failed to send transaction: {:?}", e), + } + + eyre::Ok(()) +}