├── .cargo-husky └── hooks │ └── pre-commit ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── bundles.ts ├── examples ├── balance.rs ├── fund.rs ├── price.rs ├── upload.rs ├── verify_receipt.rs └── withdraw.rs ├── package-lock.json ├── package.json ├── release.toml ├── res ├── gen_bundles │ └── .gitkeep ├── test_bundles │ ├── algorand_sig │ ├── aptos_multisig │ ├── aptos_sig │ ├── arbitrum_sig │ ├── arweave_sig │ ├── avalanche_sig │ ├── bnb_sig │ ├── boba-eth_sig │ ├── chainlink_sig │ ├── cosmos_sig │ ├── ethereum_sig │ ├── kyve_sig │ ├── matic_sig │ ├── near_sig │ ├── solana_sig │ ├── test_bundle │ └── typedethereum_sig ├── test_image.jpg ├── test_receipt.json └── test_wallet.json ├── src ├── .DS_Store ├── bundler.rs ├── client │ ├── balance.rs │ ├── bin │ │ └── cli.rs │ ├── fund.rs │ ├── method.rs │ ├── mod.rs │ ├── price.rs │ ├── upload.rs │ └── withdraw.rs ├── consts.rs ├── deep_hash.rs ├── deep_hash_sync.rs ├── error.rs ├── index.rs ├── lib.rs ├── signers │ ├── aptos.rs │ ├── arweave.rs │ ├── cosmos.rs │ ├── ed25519.rs │ ├── mod.rs │ ├── secp256k1.rs │ └── typed_ethereum.rs ├── tags.rs ├── token │ ├── arweave.rs │ ├── ethereum.rs │ ├── mod.rs │ └── solana.rs ├── transaction │ ├── irys.rs │ ├── mod.rs │ └── poll.rs ├── upload.rs ├── utils │ ├── eip712 │ │ ├── encode.rs │ │ ├── error.rs │ │ ├── lexer.rs │ │ ├── mod.rs │ │ └── parser.rs │ └── mod.rs └── verify │ ├── file.rs │ ├── mod.rs │ ├── stream.rs │ └── types.rs └── tsconfig.json /.cargo-husky/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This hook was set by cargo-husky v1.5.0: https://github.com/rhysd/cargo-husky#readme 4 | 5 | set -e 6 | 7 | echo '+cargo test' 8 | cargo test --all-features 9 | echo '+cargo clippy' 10 | cargo clippy 11 | echo '+cargo format' 12 | cargo fmt 13 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | 7 | name: Continuous integration 8 | 9 | jobs: 10 | check: 11 | name: Build and Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: sudo apt-get update 15 | - run: sudo apt-get install libudev-dev 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v3 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | - uses: actions-rs/cargo@v1 24 | with: 25 | command: build 26 | - run: npm install 27 | - run: npm run generate-bundles 28 | - uses: actions-rs/cargo@v1 29 | with: 30 | command: test 31 | 32 | fmt: 33 | name: Rustfmt 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: actions-rs/toolchain@v1 38 | with: 39 | profile: minimal 40 | toolchain: stable 41 | override: true 42 | - run: rustup component add rustfmt 43 | - uses: actions-rs/cargo@v1 44 | with: 45 | command: fmt 46 | args: --all -- --check 47 | 48 | clippy: 49 | name: Clippy 50 | runs-on: ubuntu-latest 51 | steps: 52 | - run: sudo apt-get update 53 | - run: sudo apt-get install libudev-dev 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.history 2 | /target 3 | test_item 4 | wallet.json 5 | .wallet.json 6 | test_data_item* 7 | node_modules 8 | dist 9 | /res/gen_bundles/bundle* 10 | bundles.js -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "irys-sdk" 3 | description = "Irys Network Rust SDK" 4 | homepage = "https://irys.xyz" 5 | version = "0.1.0" 6 | edition = "2021" 7 | repository = "https://github.com/Irys-xyz/rust-sdk" 8 | readme = "README.md" 9 | license = "Apache-2.0" 10 | 11 | exclude = [ 12 | ".cargo-husky", 13 | "res", 14 | "package.json", 15 | "package-lock.json", 16 | "tsconfig.json", 17 | "bundles.ts", 18 | ".github", 19 | "release.toml", 20 | ] 21 | 22 | [dependencies] 23 | anyhow = "1.0.52" 24 | async-recursion = "0.3.2" 25 | async-stream = "0.3.2" 26 | async-trait = "0.1.57" 27 | avro-rs = "0.13.0" 28 | arweave-rs = { version = "0.2.0", optional = true } 29 | bs58 = "0.4.0" 30 | bytes = "1.1.0" 31 | clap = { version = "4.4.4", features = ["derive", "env"], optional = true } 32 | data-encoding = "2.3.2" 33 | derive_builder = "0.10.2" 34 | derive_more = "0.99.17" 35 | ed25519-dalek = { version = "1.0.1", optional = true } 36 | futures = "0.3.19" 37 | indexmap = "1.9.3" 38 | lazy_static = "1.4.0" 39 | logos = "0.13.0" 40 | mime_guess = "2.0.4" 41 | num = "0.4" 42 | num-derive = "0.3.3" 43 | num-traits = "0.2.14" 44 | pipe = "0.4.0" 45 | primitive-types = "0.11.1" 46 | rand = "0.8.5" 47 | regex = "1.8.1" 48 | reqwest = { version = "0.11.20", default-features = false, features = [ 49 | "rustls-tls", 50 | "json", 51 | ] } 52 | ring = "0.16.20" 53 | rustc-hex = "2.1.0" 54 | secp256k1 = { version = "0.22.1", optional = true, features = ["recovery"] } 55 | serde = "1.0.132" 56 | serde_json = "1.0.73" 57 | sha2 = "0.10.2" 58 | strum = { version = "0.24", features = ["derive"] } 59 | strum_macros = "0.24" 60 | thiserror = "1.0.30" 61 | tokio = { version = "1.14.0", features = ["fs"] } 62 | tokio-util = "0.6.9" 63 | validator = { version = "0.16", features = ["derive"] } 64 | web3 = { version = "0.19.0", optional = true, default-features = false, features = [ 65 | "http-rustls-tls", 66 | "signing", 67 | ] } 68 | 69 | [dev-dependencies] 70 | tokio-test = "0.4.2" 71 | httpmock = "0.6" 72 | 73 | [dev-dependencies.cargo-husky] 74 | version = "1" 75 | default-features = false 76 | features = ["user-hooks"] 77 | 78 | [features] 79 | default = [ 80 | "solana", 81 | "ethereum", 82 | "erc20", 83 | "cosmos", 84 | "arweave", 85 | "algorand", 86 | "aptos", 87 | ] 88 | arweave = ["arweave-rs"] 89 | cosmos = ["secp256k1"] 90 | erc20 = ["secp256k1", "web3"] 91 | ethereum = ["secp256k1", "web3"] 92 | solana = ["ed25519-dalek"] 93 | algorand = ["ed25519-dalek"] 94 | aptos = ["ed25519-dalek"] 95 | build-binary = ["clap"] 96 | 97 | [[bin]] 98 | name = "cli" 99 | path = "src/client/bin/cli.rs" 100 | required-features = ["build-binary"] 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Irys Rust SDK 2 | SDK for interacting with Irys bundler nodes, using Rust. 3 | 4 | ## Examples 5 | Code examples can be found in `examples` directory 6 | 7 | ## Client 8 | For using the client binary, you have to build it using: 9 | ``` 10 | cargo build --release --features="build-binary" 11 | ``` 12 | 13 | The client bin will be generated at `target/release/cli`. Then you can execute the binary with `./cli --help` 14 | 15 | ``` 16 | USAGE: 17 | cli 18 | 19 | OPTIONS: 20 | -h, --help Print help information 21 | 22 | SUBCOMMANDS: 23 | balance Gets the specified user's balance for the current Irys bundler node 24 | fund Funds your account with the specified amount of atomic units 25 | help Print this message or the help of the given subcommand(s) 26 | price Check how much of a specific token is required for an upload of 27 | bytes 28 | upload Uploads a specified file 29 | upload-dir Uploads a folder (with a manifest) 30 | withdraw Sends a fund withdrawal request 31 | ``` 32 | ### Examples 33 | ``` 34 | ./cli balance
--host --token 35 | ./cli price --host --token 36 | ./cli fund --host --token --wallet 37 | ./cli withdraw --host --token --wallet 38 | ./cli upload --host --token --wallet 39 | ``` 40 | 41 | # Roadmap 42 | Some functionalities are still work in progress. If you need to use one of them, you may want to have a look in the [js-sdk](https://github.com/Irys-xyz/js-sdk), or open an issue in this repository. 43 | | Item | Solana | Ethereum | ERC20 | Cosmos | Aptos | 44 | |-----------------|------------|-----------|-----------|------------|------------| 45 | | Balance | [x] | [x] | [ ] | [ ] | [ ] | 46 | | Price | [x] | [x] | [ ] | [ ] | [ ] | 47 | | Fund | [ ] | [ ] | [ ] | [ ] | [ ] | 48 | | Withdraw | [ ] | [ ] | [ ] | [ ] | [ ] | 49 | | Upload | [x] | [x] | [ ] | [ ] | [ ] | 50 | | Upload Directory| [ ] | [ ] | [ ] | [ ] | [ ] | 51 | | Verify bundle | [x] | [x] | [x] | [x] | [x] | 52 | 53 | # Testing 54 | In order to run tests properly, you need to generate random bundles. Run: 55 | ``` 56 | npm install 57 | npm run generate-bundles 58 | ``` 59 | To generate random bundles in `res/gen_bundles`, and then run: 60 | ``` 61 | cargo test 62 | ``` -------------------------------------------------------------------------------- /bundles.ts: -------------------------------------------------------------------------------- 1 | import { AptosAccount } from "aptos"; 2 | import { AlgorandSigner, AptosSigner, ArweaveSigner, bundleAndSignData, createData, DataItem, EthereumSigner, MultiSignatureAptosSigner, Signer, SolanaSigner, TypedEthereumSigner } from "arbundles"; 3 | import { Keypair } from "@solana/web3.js"; 4 | import bs58 from "bs58"; 5 | import fs from "fs/promises"; 6 | import crypto from "crypto"; 7 | import Bundlr from "@bundlr-network/client"; 8 | import { Wallet } from "ethers/wallet"; 9 | import Arweave from "arweave"; 10 | 11 | const MAX_BUNDLES_AMOUNT = 100; 12 | const MAX_DATA_ITEMS = 100; 13 | const MAX_DATA_BYTES = 1000; 14 | const MAX_APTOS_SIGNERS = 20; 15 | 16 | //Arweave 17 | const jwk = await Arweave.init({}).wallets.generate(); 18 | 19 | //Ethereum 20 | var { privateKey } = Wallet.createRandom(); 21 | 22 | //Solana 23 | const solKeypair = Keypair.generate(); 24 | 25 | //Algorand 26 | const algoKeypair = Keypair.generate(); 27 | 28 | //Multiaptos 29 | const aptosAccounts = Array.from({ length: Math.ceil(Math.random() * MAX_APTOS_SIGNERS + 1) }, () => new AptosAccount()); 30 | const wallet = { 31 | participants: aptosAccounts.map(a => a.signingKey.publicKey.toString()), 32 | threshold: 2 33 | }; 34 | 35 | // create signature collection function 36 | // this function is called whenever the client needs to collect signatures for signing 37 | const collectSignatures = async (message: Uint8Array) => { 38 | //Select random amount of random acccounts within our aptos accounts 39 | const accountAmount = Math.ceil(Math.random() * aptosAccounts.length); 40 | const randomAccounts = aptosAccounts 41 | .map((account, i) => { return { account, i } }) // Store original array position 42 | .sort(() => Math.random() - Math.random()) // Shuffle array so we get randoms 43 | .slice(0, accountAmount); // Get sample size 44 | const signatures = randomAccounts.map(el => Buffer.from(el.account.signBuffer(message).toUint8Array())); 45 | const bitmap = randomAccounts.map(el => el.i); 46 | return { signatures, bitmap }; 47 | } 48 | 49 | const bundlesAmount = MAX_BUNDLES_AMOUNT; 50 | 51 | //Create all signers 52 | //TODO: figure out how to instantiate signer directly (see below) 53 | const bundlerClient = new Bundlr.default( 54 | "https://devnet.irys.xyz", 55 | "multiAptos", 56 | wallet, 57 | { providerUrl: "https://fullnode.devnet.aptoslabs.com", currencyOpts: { collectSignatures } } 58 | ); 59 | await bundlerClient.ready(); 60 | let multiAptosSigner = bundlerClient.getSigner(); 61 | 62 | const signers: Signer[] = [ 63 | new ArweaveSigner(jwk), 64 | new AlgorandSigner(algoKeypair.secretKey, algoKeypair.publicKey.toBuffer()), 65 | new EthereumSigner(privateKey), 66 | new TypedEthereumSigner(privateKey), 67 | new SolanaSigner(bs58.encode(solKeypair.secretKey)), 68 | new AptosSigner(aptosAccounts[0].toPrivateKeyObject().privateKeyHex, aptosAccounts[0].toPrivateKeyObject().publicKeyHex), 69 | //new MultiSignatureAptosSigner(Buffer.from(wallet.participants.join("")), collectSignatures) 70 | //multiAptosSigner //TODO: fix signer 71 | ]; 72 | 73 | for (let i = 1; i <= bundlesAmount; i++) { 74 | const dataItemsAmount = Math.floor(Math.random() * MAX_DATA_ITEMS + 1); 75 | const dataItems: DataItem[] = []; 76 | for (let j = 1; j <= dataItemsAmount; j++) { 77 | const signer = signers[Math.floor(Math.random() * signers.length)]; 78 | const randomData = crypto.randomBytes(MAX_DATA_BYTES).toString('hex'); 79 | try { 80 | const data = createData(randomData, signer); 81 | await data.sign(signer).then(() => { 82 | if (data.isSigned()) { 83 | dataItems.push(data); 84 | } else { 85 | console.log(`Invalid or unsigned data item: ${data.id}`); 86 | } 87 | }).catch(err => { 88 | console.log(`Error generating data item: ${err}`); 89 | }); 90 | } catch (err) { 91 | console.log(err); 92 | } 93 | } 94 | 95 | const finalSigner = signers[Math.floor(Math.random() * signers.length)]; 96 | bundleAndSignData(dataItems, finalSigner).then((bundle) => { 97 | bundle.verify().then(async ok => { 98 | await fs.writeFile(`res/gen_bundles/bundle_${i}`, bundle.getRaw()).then(() => console.info(`Generated bundle ${i} with ${bundle.getIds().length} dataItems in res/gen_bundles/bundle_${i}`)); 99 | }).catch(err => { 100 | console.log(`Invalid bundle: ${err}`) 101 | }); 102 | }).catch(err => { 103 | console.log(`Error generating bundle: ${err}`); 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /examples/balance.rs: -------------------------------------------------------------------------------- 1 | use irys_sdk::{bundler::get_balance, token::TokenType}; 2 | use reqwest::Url; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let url = Url::parse("https://uploader.irys.network").unwrap(); 7 | let token = TokenType::Solana; 8 | let address = "7y3tfYz8V3ui67XRJi1iiiS5GQ4zVyFoDfFAtouhB8gL"; 9 | let res = get_balance(&url, token, address, &reqwest::Client::new()).await; 10 | match res { 11 | Ok(ok) => println!("[ok] {}", ok), 12 | Err(err) => println!("[err] {}", err), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/fund.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use irys_sdk::{ 4 | bundler::BundlerClientBuilder, error::BundlerError, token::arweave::ArweaveBuilder, 5 | }; 6 | use reqwest::Url; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), BundlerError> { 10 | let url = Url::parse("https://uploader.irys.xyz").unwrap(); 11 | let wallet = PathBuf::from_str("res/test_wallet.json").unwrap(); 12 | let token = ArweaveBuilder::new() 13 | .keypair_path(wallet) 14 | .build() 15 | .expect("Could not create token instance"); 16 | let bundler_client = BundlerClientBuilder::new() 17 | .url(url) 18 | .token(token) 19 | .fetch_pub_info() 20 | .await? 21 | .build()?; 22 | 23 | let res = bundler_client 24 | .fund(10000, None) 25 | .await 26 | .map(|res| res.to_string()); 27 | match res { 28 | Ok(ok) => println!("[ok] {}", ok), 29 | Err(err) => println!("[err] {}", err), 30 | }; 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /examples/price.rs: -------------------------------------------------------------------------------- 1 | use irys_sdk::{bundler::get_price, token::TokenType}; 2 | use reqwest::Url; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let url = Url::parse("https://uploader.irys.xyz").unwrap(); 7 | let token = TokenType::Solana; 8 | 9 | let client = reqwest::Client::new(); 10 | let res = get_price(&url, token, &client, 256000).await; 11 | match res { 12 | Ok(ok) => println!("[ok] {}", ok), 13 | Err(err) => println!("[err] {}", err), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/upload.rs: -------------------------------------------------------------------------------- 1 | use irys_sdk::{bundler::BundlerClientBuilder, error::BundlerError, token::solana::SolanaBuilder}; 2 | use reqwest::Url; 3 | use std::{path::PathBuf, str::FromStr}; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<(), BundlerError> { 7 | let url = Url::parse("https://uploader.irys.xyz").unwrap(); 8 | let token = SolanaBuilder::new().wallet( 9 | "kNykCXNxgePDjFbDWjPNvXQRa8U12Ywc19dFVaQ7tebUj3m7H4sF4KKdJwM7yxxb3rqxchdjezX9Szh8bLcQAjb") 10 | .build() 11 | .expect("Could not create Solana instance"); 12 | let mut bundler_client = BundlerClientBuilder::new() 13 | .url(url) 14 | .token(token) 15 | .fetch_pub_info() 16 | .await? 17 | .build()?; 18 | 19 | let file = PathBuf::from_str("res/test_image.jpg").unwrap(); 20 | let res = bundler_client.upload_file(file).await; 21 | match res { 22 | Ok(res) => println!("Uploaded to https://uploader.irys.xyz/{}", &res.id), 23 | Err(err) => println!("[err] {}", err), 24 | } 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /examples/verify_receipt.rs: -------------------------------------------------------------------------------- 1 | use data_encoding::BASE64URL_NOPAD; 2 | use irys_sdk::{deep_hash::DeepHashChunk, deep_hash_sync::deep_hash_sync, ArweaveSigner, Verifier}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // =============================================================================================== 6 | // NOTE: this data structure will be included in further versions, along with a verify function. 7 | // For now, you can verify receipts with following example 8 | // =============================================================================================== 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct Receipt { 13 | pub id: String, 14 | pub timestamp: u64, 15 | pub version: String, 16 | pub public: String, 17 | pub signature: String, 18 | pub deadline_height: u64, 19 | pub block: u64, 20 | pub validator_signatures: Vec, 21 | } 22 | 23 | fn main() -> Result<(), irys_sdk::error::BundlerError> { 24 | let data = std::fs::read_to_string("res/test_receipt.json").expect("Unable to read file"); 25 | let receipt = serde_json::from_str::(&data).expect("Unable to parse json file"); 26 | 27 | let fields = DeepHashChunk::Chunks(vec![ 28 | DeepHashChunk::Chunk("Bundlr".into()), 29 | DeepHashChunk::Chunk(receipt.version.into()), 30 | DeepHashChunk::Chunk(receipt.id.into()), 31 | DeepHashChunk::Chunk(receipt.deadline_height.to_string().into()), 32 | DeepHashChunk::Chunk(receipt.timestamp.to_string().into()), 33 | ]); 34 | 35 | let pubk = BASE64URL_NOPAD 36 | .decode(&receipt.public.into_bytes()) 37 | .unwrap(); 38 | let msg = deep_hash_sync(fields).unwrap(); 39 | let sig = BASE64URL_NOPAD 40 | .decode(&receipt.signature.into_bytes()) 41 | .unwrap(); 42 | 43 | ArweaveSigner::verify(pubk.into(), msg, sig.into()) 44 | } 45 | -------------------------------------------------------------------------------- /examples/withdraw.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use irys_sdk::{ 4 | bundler::BundlerClientBuilder, error::BundlerError, token::arweave::ArweaveBuilder, 5 | }; 6 | use reqwest::Url; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<(), BundlerError> { 10 | let url = Url::parse("https://uploader.irys.xyz").unwrap(); 11 | let wallet = PathBuf::from_str("res/test_wallet.json").unwrap(); 12 | let token = ArweaveBuilder::new() 13 | .keypair_path(wallet) 14 | .build() 15 | .expect("Could not create token instance"); 16 | let bundler_client = BundlerClientBuilder::new() 17 | .url(url) 18 | .token(token) 19 | .fetch_pub_info() 20 | .await? 21 | .build()?; 22 | 23 | let res = bundler_client 24 | .withdraw(10000) 25 | .await 26 | .map(|res| res.to_string()); 27 | match res { 28 | Ok(ok) => println!("[ok] {}", ok), 29 | Err(err) => println!("[err] {}", err), 30 | } 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rust-sdk", 3 | "version": "1.0.0", 4 | "description": "Irys's rust sdk", 5 | "main": "bundles.js", 6 | "type": "module", 7 | "directories": { 8 | "example": "examples" 9 | }, 10 | "scripts": { 11 | "generate-bundles": "tsc && node dist/bundles.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Irys-xyz/rust-sdk.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/Irys-xyz/rust-sdk/issues" 21 | }, 22 | "homepage": "https://github.com/Irys-xyz/rust-sdk#readme", 23 | "dependencies": { 24 | "@bundlr-network/client": "^0.10.5", 25 | "@solana/web3.js": "^1.73.3", 26 | "aptos": "^1.7.1", 27 | "arbundles": "^0.9.6", 28 | "arweave": "^1.13.7", 29 | "bs58": "^5.0.0", 30 | "ethers": "^6.3.0" 31 | }, 32 | "devDependencies": { 33 | "@types/keythereum": "^1.2.1", 34 | "@types/node": "^18.14.6", 35 | "@types/node-fetch": "^2.6.2", 36 | "typescript": "^4.9.5" 37 | } 38 | } -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["master"] 2 | sign-commit = true 3 | -------------------------------------------------------------------------------- /res/gen_bundles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/gen_bundles/.gitkeep -------------------------------------------------------------------------------- /res/test_bundles/algorand_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/algorand_sig -------------------------------------------------------------------------------- /res/test_bundles/aptos_multisig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/aptos_multisig -------------------------------------------------------------------------------- /res/test_bundles/aptos_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/aptos_sig -------------------------------------------------------------------------------- /res/test_bundles/arbitrum_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/arbitrum_sig -------------------------------------------------------------------------------- /res/test_bundles/arweave_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/arweave_sig -------------------------------------------------------------------------------- /res/test_bundles/avalanche_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/avalanche_sig -------------------------------------------------------------------------------- /res/test_bundles/bnb_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/bnb_sig -------------------------------------------------------------------------------- /res/test_bundles/boba-eth_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/boba-eth_sig -------------------------------------------------------------------------------- /res/test_bundles/chainlink_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/chainlink_sig -------------------------------------------------------------------------------- /res/test_bundles/cosmos_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/cosmos_sig -------------------------------------------------------------------------------- /res/test_bundles/ethereum_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/ethereum_sig -------------------------------------------------------------------------------- /res/test_bundles/kyve_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/kyve_sig -------------------------------------------------------------------------------- /res/test_bundles/matic_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/matic_sig -------------------------------------------------------------------------------- /res/test_bundles/near_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/near_sig -------------------------------------------------------------------------------- /res/test_bundles/solana_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/solana_sig -------------------------------------------------------------------------------- /res/test_bundles/test_bundle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/test_bundle -------------------------------------------------------------------------------- /res/test_bundles/typedethereum_sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_bundles/typedethereum_sig -------------------------------------------------------------------------------- /res/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/res/test_image.jpg -------------------------------------------------------------------------------- /res/test_receipt.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "juLVTu4DrmE7hC9izySHX95gRApRoqSC7SKM75seUR4", 3 | "timestamp": 1683731921178, 4 | "version": "1.0.0", 5 | "public": "sq9JbppKLlAKtQwalfX5DagnGMlTirditXk7y4jgoeA7DEM0Z6cVPE5xMQ9kz_T9VppP6BFHtHyZCZODercEVWipzkr36tfQkR5EDGUQyLivdxUzbWgVkzw7D27PJEa4cd1Uy6r18rYLqERgbRvAZph5YJZmpSJk7r3MwnQquuktjvSpfCLFwSxP1w879-ss_JalM9ICzRi38henONio8gll6GV9-omrWwRMZer_15bspCK5txCwpY137nfKwKD5YBAuzxxcj424M7zlSHlsafBwaRwFbf8gHtW03iJER4lR4GxeY0WvnYaB3KDISHQp53a9nlbmiWO5WcHHYsR83OT2eJ0Pl3RWA-_imk_SNwGQTCjmA6tf_UVwL8HzYS2iyuu85b7iYK9ZQoh8nqbNC6qibICE4h9Fe3bN7AgitIe9XzCTOXDfMr4ahjC8kkqJ1z4zNAI6-Leei_Mgd8JtZh2vqFNZhXK0lSadFl_9Oh3AET7tUds2E7s-6zpRPd9oBZu6-kNuHDRJ6TQhZSwJ9ZO5HYsccb_G_1so72aXJymR9ggJgWr4J3bawAYYnqmvmzGklYOlE_5HVnMxf-UxpT7ztdsHbc9QEH6W2bzwxbpjTczEZs3JCCB3c-NewNHsj9PYM3b5tTlTNP9kNAwPZHWpt11t79LuNkNGt9LfOek", 6 | "signature": "kjgpJ8L6SYZQRqbieHY7FjHrlrlIv86IOIvm2IQgjLVRNMw1LWFHWpIhw31wy6-Q2U1VLLRZ7XyhlCK25RxDJJS8mfCKxuXuMS9JZoKWD5guW7YqfcTy9bf50mQY5ZUyN1I4hGXQ14PQsRDuAHlaPUZsCO0WyhJ75OQ1FF-s4mEcjwHtU1ccdy0YNnNIoA8mR3WuywiZ9xfSWLWgPsfHcImp-9nZw-9TZgKSb-PtVmDVS3nRsgVkb2cinj58lNUKq5E6akxLbV3z8DUYC82F071d_Lr7WBMBRnqo8iSj8S1gLEhGFygq0KxtZt7OxeAztrsD1RgLyXzVOKVeBgCNQic5dCueK6_RRuLcBzcg-4LIrPh6GDFik_fU-g59ZnoGzX7urmnS01yzyE_L9laFwV816ZfO59_pvSfHNyKfQ8pzwig9JTec-hvtNXRoXEoRNTElL4xj7tezEGi9zrP_aNjQ9pVrDY_x_xCEndD4GsLiDCGLVDHaWtWea5ytKE3S_KiPvtziV6MPEhWNJiyxcQW4Lvlx2MlotgRs34aw2NRutHteCs2poiES-Qw70fM9t52KeJt1S-a1N4OAN978MZTPw_K7zQUO-HhgKlrus2hoN356fdyBB6jA-aAXI40sgPjh-KB8zfHL1k7w_CCAoVEX5EGdZ8VErlTy6lgAWpk", 7 | "deadlineHeight": 1180043, 8 | "block": 1180043, 9 | "validatorSignatures": [] 10 | } 11 | -------------------------------------------------------------------------------- /res/test_wallet.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty":"RSA", 3 | "e":"AQAB", 4 | "n":"u9-0UVKdb64T_tpD-MbmwN4BqG3XJi0Kk1zooEKeWKiUzeTOqVmsV72DI3TawdiUmyoOfpIPiBeagGVRrirhC1YzwoYQwNpLl55l8nEJ8hCxd-Nw4YaZ1DW73vM1EhSbZ6zT5MdlRXEcfI0L5Ky7-JhQfLjDhkxao7uNwRJcXMfbGE3mGKrAMQmWszxWiThMBDOSeXBc48cFmvTdaSmHChvwj8GvJGEZtp92BhaNVnLos6RK7PztPzCoMdmOoDBf4HSJxokHiIKGaB95xn6ah8hl-1usI-60YHQCWApXUVLZBuT4FiIhR1OdTXqfmWy5ubTLvXtZkHa-YWAaMV-0enu3x5nI5EIKqB5wUvGzLF8RHIrBfZ-Gl7Xs8P3Vf9iP8BnHuhcm4mkwgAM11VRF91ts1dvG0Z-gn75o7jTTgkiwpSJy6-cXuSD8aKG39IhOjjIshN8Ehu6KyM70U-y07MXTMPQykMqZB-2qzMS1jUeu6px-hO7m-67gcK8Gbmc6j4_KmLb_vwKk-CAsRyqfpeOS5BIRs2jLvZuVKHS-176MAP9wf5-QHiJuqlN-vyTG_QhMja3INzrGwXVw0Ow24mf4761Som5gz7AkVZLcLVzsHhah5AKV9a37sWGK_oxYnNr0wHdv11otAmdTt65qP-6QVLQtcHYJc3mWex3duB8", 5 | "d":"KDiTw4dC0kp72GdE_wkiFK9SvorJcVBaP8M3wDuS59srhErUXZgnl98oKJqXTMG_yto9oSZG8fVPq-ZFDtr6M_GgAiiuhIMXhN2x6K3_mZWuUMLeA0JMQ4y_bFM4DOdhf9gVwxhXG39EQHv3zFyYn3eIX7M0lNda3kvEZM23_VsCEll3BdfkbnslirjDs_vb9lhOJvaBxHfywMJkdmOgF-90WGp4QvsQxLKt007XsmbGX9MwAT2C0zDpCUcQzEWe-RAqANmMRdLf0IHQkci_74rdHmt5VnX_otijOdL4752jfeDeJernofwKJLC4Y2HN_y5wIeA7A_0P9Avdf62EFwiN7XnOUOBWYsM6NZIM4QlPvxhZXQnBgzblvHeSEYvxWy-uBQ0s_ZRNgiENeNznQRHoltg4JEtH6ie2pmZAPOcNXBQLZU8DoPbqqqHDa-DymyDgHQ6bviSfcqsJZlQAGRdXf2mQ7U7Fs-pEeQawwo32JPI9EBTvdlt12eq0pu0jxa36Qbt8p1BWpnCj634ZCU5P5ZWCMAm7Y3LSvoMJ1NIzMaoKcqIhQ4CmoO5lmCYh1OyATsQdsYbvNiM6oeDo-iPMkWifo8kw2kKUTK0qjjE_2CHcamrxAb8v_Xr6iEHs1Elm0WtHdcu1CYURqYngkDpVruK8wNyB4TLwz3MAqlU", 6 | "p":"7xPxCrqSB0GNi87gYl24bTMxYPm0S3BcNkvPYKCjyN_sYOp9wJHJNIfx0aDbxFCTAh-uajErmx8EgIZxLhGkC0P5o9KR7kAOQSA2xo3iuqYTB67GkKzYtBkG_rislm5tbCTEYiAHPhSxm-9oqS6DFsAquEQS3zIW_YK2jE5bgSFn95KXOyKuKOU3BJKKYvKAiUQobPrkdg7vBkqowAJyh7TRGVfCm1U47vYkZ32NLj4H16oH-I6XfYqlMKY7tYsczFy_IMklMMcJ2xB6yjjmlIpoDz5lsiSsuRPJRhOLcP1rdKCniju4BkRKEl2eq9Fch7JB6IToGXEuWSIrVQRHRQ", 7 | "q":"ySvz1IgADl6_CjJsq7wMYueSinLDJmg69kGnE-VVGtQX9MxmSo68LrMTUwG2lWvMtniz2-dvkF3tdv-GV7-mACZWpL4fVxyCiCPo3Un8_wgwXfMTRf-l-XWvDDcj5264Eamz8ELRhDb_jRaR5I5qc-tGgNVPu0p1k4oO0r7njzBplfBuosjFVKeOGuQ5YaBLGz7svMR0UmuzSt8PfPhRU1cqscY3wXX5LD41INgyFiyBI7sRPsznHXWs1SOtCsFtz_aO4Ipk4FDO3YY74VkEm7JXlYFuEZ0pwKUml1aVl67BcDKZ3njhhDo4ewA2jwst2ONcWLJ06CBc55AxiUKWEw", 8 | "dp":"k9Wj7ntxwvThHvucupazbSsDtLCTsTZYfuaf5GNRA-ybUU6O1h4P6eDKQlMSsjEUxnJqupWPHkuuz_7SS6dy5dhxrMCPpmCLr-_Ijzy7X6lECTMk699F3Q_AyI_PgPHlzcJqgTbG47eOIBuJf1wYiV_oyqqldMiXeMl0Mgxd_tp2XJuT0mheg0FBpR0sX4UOwFKaINF_phUT7rMJ6DlbMtk3l1EyMDUhkHo6BpiXTWnkFZK5fRRHTsUF4rBYKalM9H3BXfejfKcHTCPDmfpXcrCnKg3uepq9B3DZu_BoAEVkRej10L-eypk9qF-ltx8t9Wbf5HUSxHC9NTawztBndQ", 9 | "dq":"g07OC6ZtqvOK7MP3DK1kxFX036uC15nnCn_V53WkdCuGK4ITLo02JaE7ds3CeItxhpUIiPvqZSf57Ndiv_nXB3f-y-9RE5IHXYda4r39hhW5REl6BhGVK1v2UxnWtKQfP2AulB95FDy110ipF68hquIiFcumDFDQp-nQPRzgliT6diUGgfhcXSI07jaTgbaX74PGEHyGlJm54J_yQxbuNmDJ_FmuYPd5GbjNAtA6-SA5drIO0pf6Ls91bXWo6EFTRQ_hOIz8xTHZpNyOT0vEZ0AkTPC7gIG-FAF4TAX3BJfLqUlOB9mNEbrSzO5ZrkqUtWHKEBcDmdQt7_O9caNlww", 10 | "qi":"vhENYg54JCDGyqNxAFD8aS5vDZE2rOVYQBox-g_1YhtmRJ0TYFnKmr015qGvmuHTwpk8YPjX6jsW-cUGUzf_L9hB4WKKkPkmzc-8CN1zWF6uErbMHM8fXs39UsuzfQGdkS8ZE00ww3THOtyAc3c_xchhQk8Ux1DLg5YGZYlO_u1cnKMNW5ZO8ThOnEL5S3BVVQlHy_K1mKqFuJwGLF1UcPlp7QpW_sqZpcxwxApBBY9Ayh0Wm0ckCNfdUptUimNskDtIr-RX5tLY0np-S2zazxr-tFnvUwULawAFop5qtz9fz4SOrZmPHC_L_jO0whr8_aEUwFrA-1KEhEA4nA6d3w" 11 | } 12 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Irys-xyz/rust-sdk/0d6333daa30bec8d3e74b5530364142ad321a6de/src/.DS_Store -------------------------------------------------------------------------------- /src/bundler.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | use std::str::FromStr; 5 | 6 | use crate::consts::DEFAULT_BUNDLER_URL; 7 | use crate::deep_hash::{deep_hash, DeepHashChunk}; 8 | use crate::error::{BuilderError, BundlerError}; 9 | use crate::tags::Tag; 10 | use crate::token; 11 | use crate::token::TokenType; 12 | use crate::upload::Uploader; 13 | use crate::utils::{check_and_return, get_nonce}; 14 | use crate::BundlerTx; 15 | use arweave_rs::crypto::base64::Base64; 16 | use bytes::Bytes; 17 | use num::BigUint; 18 | use num::FromPrimitive; 19 | use num_traits::Zero; 20 | use reqwest::Url; 21 | use serde::{Deserialize, Serialize}; 22 | use serde_json::Value; 23 | 24 | #[allow(unused)] 25 | pub struct BundlerClient { 26 | url: Url, 27 | token: Token, 28 | client: reqwest::Client, 29 | pub_info: PubInfo, 30 | uploader: Uploader, 31 | } 32 | #[allow(unused)] 33 | #[derive(Deserialize, Default)] 34 | pub struct PubInfo { 35 | version: String, 36 | gateway: String, 37 | addresses: HashMap, 38 | } 39 | #[derive(Deserialize, Default)] 40 | pub struct BalanceResData { 41 | balance: String, 42 | } 43 | 44 | #[derive(Serialize, Deserialize)] 45 | pub struct FundBody { 46 | tx_id: String, 47 | } 48 | 49 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 50 | #[serde(rename_all = "camelCase")] 51 | pub struct UploadReponse { 52 | pub block: u64, 53 | pub id: String, 54 | pub public: String, 55 | pub signature: String, 56 | pub timestamp: u64, 57 | pub version: String, 58 | } 59 | 60 | #[derive(Debug, Serialize, Deserialize)] 61 | #[serde(rename_all = "camelCase")] 62 | pub struct WithdrawBody { 63 | public_key: Base64, 64 | token: String, 65 | amount: String, 66 | nonce: u64, 67 | signature: Base64, 68 | sig_type: u16, 69 | } 70 | 71 | #[derive(Default)] 72 | 73 | pub struct BundlerClientBuilder { 74 | url: Option, 75 | token: Token, 76 | client: Option, 77 | pub_info: Option, 78 | } 79 | 80 | impl BundlerClientBuilder { 81 | pub fn new() -> BundlerClientBuilder { 82 | Default::default() 83 | } 84 | } 85 | 86 | impl BundlerClientBuilder { 87 | pub fn url(mut self, url: Url) -> BundlerClientBuilder { 88 | self.url = Some(url); 89 | self 90 | } 91 | 92 | pub fn client(mut self, client: reqwest::Client) -> BundlerClientBuilder { 93 | self.client = Some(client); 94 | self 95 | } 96 | 97 | pub async fn fetch_pub_info(mut self) -> Result, BuilderError> { 98 | if let Some(url) = &self.url { 99 | let pub_info = match get_pub_info(url).await { 100 | Ok(info) => info, 101 | Err(err) => { 102 | return Err(BuilderError::FetchPubInfoError(err.to_string())); 103 | } 104 | }; 105 | self.pub_info = Some(pub_info); 106 | Ok(self) 107 | } else { 108 | Err(BuilderError::MissingField("url".to_owned())) 109 | } 110 | } 111 | 112 | pub fn pub_info(mut self, pub_info: PubInfo) -> BundlerClientBuilder { 113 | self.pub_info = Some(pub_info); 114 | self 115 | } 116 | } 117 | 118 | impl BundlerClientBuilder<()> { 119 | pub fn token(self, token: Token) -> BundlerClientBuilder 120 | where 121 | Token: token::Token, 122 | { 123 | BundlerClientBuilder { 124 | token, 125 | url: self.url, 126 | client: self.client, 127 | pub_info: self.pub_info, 128 | } 129 | } 130 | } 131 | 132 | impl BundlerClientBuilder 133 | where 134 | Token: token::Token, 135 | { 136 | pub fn build(self) -> Result, BuilderError> { 137 | let url = self.url.unwrap_or(Url::parse(DEFAULT_BUNDLER_URL).unwrap()); 138 | 139 | let client = self.client.unwrap_or_else(reqwest::Client::new); 140 | 141 | let pub_info = match self.pub_info { 142 | Some(p) => p, 143 | None => return Err(BuilderError::MissingField("token".to_owned())), 144 | }; 145 | 146 | let uploader = Uploader::new(url.clone(), client.clone(), self.token.get_type()); 147 | 148 | Ok(BundlerClient { 149 | url, 150 | token: self.token, 151 | client, 152 | pub_info, 153 | uploader, 154 | }) 155 | } 156 | } 157 | 158 | /// Gets the public info from a Irys bundler node. 159 | /// 160 | /// # Examples 161 | /// 162 | /// ``` 163 | /// # use irys_sdk::bundler::get_pub_info; 164 | /// # use reqwest::Url; 165 | /// # tokio_test::block_on(async { 166 | /// let url = Url::parse("https://uploader.irys.xyz/").unwrap(); 167 | /// let res = get_pub_info(&url).await; 168 | /// # }); 169 | /// ``` 170 | pub async fn get_pub_info(url: &Url) -> Result { 171 | let client = reqwest::Client::new(); 172 | let response = client 173 | .get( 174 | url.join("info") 175 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 176 | ) 177 | .header("Content-Type", "application/json") 178 | .send() 179 | .await; 180 | 181 | check_and_return::(response).await 182 | } 183 | 184 | /// Get balance from address in a Irys bundler node 185 | pub async fn get_balance( 186 | url: &Url, 187 | token: TokenType, 188 | address: &str, 189 | client: &reqwest::Client, 190 | ) -> Result { 191 | let response = client 192 | .get( 193 | url.join(&format!( 194 | "account/balance/{}", 195 | token.to_string().to_lowercase() 196 | )) 197 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 198 | ) 199 | .query(&[("address", address)]) 200 | .header("Content-Type", "application/json") 201 | .send() 202 | .await; 203 | 204 | match check_and_return::(response).await { 205 | Ok(d) => match BigUint::from_str(&d.balance) { 206 | Ok(ok) => Ok(ok), 207 | Err(err) => Err(BundlerError::TypeParseError(err.to_string())), 208 | }, 209 | Err(err) => Err(BundlerError::TypeParseError(err.to_string())), 210 | } 211 | } 212 | 213 | /// Get the cost for determined amount of bytes, measured in the token's base units (i.e Winston for Arweave, or Lamport for Solana) 214 | pub async fn get_price( 215 | url: &Url, 216 | token: TokenType, 217 | client: &reqwest::Client, 218 | byte_amount: u64, 219 | ) -> Result { 220 | let response = client 221 | .get( 222 | url.join(&format!("/price/{}/{}", token, byte_amount)) 223 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 224 | ) 225 | .header("Content-Type", "application/json") 226 | .send() 227 | .await; 228 | 229 | match check_and_return::(response).await { 230 | Ok(d) => match BigUint::from_u64(d) { 231 | Some(ok) => Ok(ok), 232 | None => Err(BundlerError::TypeParseError( 233 | "Could not parse u64 to BigUInt".to_owned(), 234 | )), 235 | }, 236 | Err(err) => Err(err), 237 | } 238 | } 239 | 240 | impl BundlerClient 241 | where 242 | Token: token::Token, 243 | { 244 | /// Creates an unsigned transaction for posting. 245 | /// 246 | /// # Examples 247 | /// 248 | /// ``` 249 | /// # use irys_sdk::{ 250 | /// # token::TokenType, 251 | /// # BundlerClientBuilder, 252 | /// # token::arweave::ArweaveBuilder, 253 | /// # tags::Tag, 254 | /// # error::BuilderError 255 | /// # }; 256 | /// # use std::{path::PathBuf, str::FromStr}; 257 | /// # use reqwest::Url; 258 | /// # #[tokio::main] 259 | /// # async fn main() -> Result<(), BuilderError> { 260 | /// # let url = Url::parse("https://uploader.irys.xyz").unwrap(); 261 | /// # let wallet = PathBuf::from_str("res/test_wallet.json").expect("Invalid wallet path"); 262 | /// # let token = ArweaveBuilder::new() 263 | /// # .keypair_path(wallet) 264 | /// # .build() 265 | /// # .expect("Could not create token instance"); 266 | /// # let mut bundler_client = BundlerClientBuilder::new() 267 | /// # .url(url) 268 | /// # .token(token) 269 | /// # .fetch_pub_info() 270 | /// # .await? 271 | /// # .build()?; 272 | /// let data = b"Hello".to_vec(); 273 | /// let tags = vec![Tag::new("name", "value")]; 274 | /// let tx = bundler_client.create_transaction(data, tags).unwrap(); 275 | /// # Ok(()) 276 | /// # } 277 | /// ``` 278 | pub fn create_transaction( 279 | &self, 280 | data: Vec, 281 | additional_tags: Vec, 282 | ) -> Result { 283 | BundlerTx::new(vec![], data, additional_tags) 284 | } 285 | 286 | /// Signs a transaction 287 | /// 288 | /// # Examples 289 | /// 290 | /// ``` 291 | /// # use irys_sdk::{ 292 | /// # token::TokenType, 293 | /// # BundlerClientBuilder, 294 | /// # token::arweave::ArweaveBuilder, 295 | /// # tags::Tag, 296 | /// # error::BuilderError 297 | /// # }; 298 | /// # use std::{path::PathBuf, str::FromStr}; 299 | /// # use reqwest::Url; 300 | /// # #[tokio::main] 301 | /// # async fn main() -> Result<(), BuilderError> { 302 | /// # let url = Url::parse("https://uploader.irys.xyz").unwrap(); 303 | /// # let wallet = PathBuf::from_str("res/test_wallet.json").expect("Invalid wallet path"); 304 | /// # let token = ArweaveBuilder::new() 305 | /// # .keypair_path(wallet) 306 | /// # .build() 307 | /// # .expect("Could not create token instance"); 308 | /// # let mut bundler_client = BundlerClientBuilder::new() 309 | /// # .url(url) 310 | /// # .token(token) 311 | /// # .fetch_pub_info() 312 | /// # .await? 313 | /// # .build()?; 314 | /// let data = b"Hello".to_vec(); 315 | /// # let data = b"Hello".to_vec(); 316 | /// # let tags = vec![Tag::new("name", "value")]; 317 | /// let mut tx = bundler_client.create_transaction(data, tags).unwrap(); 318 | /// let sig = bundler_client.sign_transaction(&mut tx).await; 319 | /// # assert!(sig.is_ok()); 320 | /// # Ok(()) 321 | /// # } 322 | /// ``` 323 | pub async fn sign_transaction(&self, tx: &mut BundlerTx) -> Result<(), BundlerError> { 324 | tx.sign(self.token.get_signer()?).await 325 | } 326 | 327 | /// Sends a signed transaction 328 | /// 329 | /// # Examples 330 | /// 331 | /// ``` 332 | /// # use irys_sdk::{ 333 | /// # token::TokenType, 334 | /// # BundlerClientBuilder, 335 | /// # token::arweave::ArweaveBuilder, 336 | /// # tags::Tag, 337 | /// # error::BuilderError 338 | /// # }; 339 | /// # use std::{path::PathBuf, str::FromStr}; 340 | /// # use reqwest::Url; 341 | /// # #[tokio::main] 342 | /// # async fn main() -> Result<(), BuilderError> { 343 | /// # let url = Url::parse("https://uploader.irys.xyz").unwrap(); 344 | /// # let wallet = PathBuf::from_str("res/test_wallet.json").expect("Invalid wallet path"); 345 | /// # let token = ArweaveBuilder::new() 346 | /// # .keypair_path(wallet) 347 | /// # .build() 348 | /// # .expect("Could not create token instance"); 349 | /// # let mut bundler_client = BundlerClientBuilder::new() 350 | /// # .url(url) 351 | /// # .token(token) 352 | /// # .fetch_pub_info() 353 | /// # .await? 354 | /// # .build()?; 355 | /// let data = b"Hello".to_vec(); 356 | /// # let data = b"Hello".to_vec(); 357 | /// # let tags = vec![Tag::new("name", "value")]; 358 | /// let mut tx = bundler_client.create_transaction(data, tags).unwrap(); 359 | /// let sig = bundler_client.sign_transaction(&mut tx).await; 360 | /// assert!(sig.is_ok()); 361 | /// let result = bundler_client.send_transaction(tx).await; 362 | /// # Ok(()) 363 | /// # } 364 | /// ``` 365 | pub async fn send_transaction(&self, tx: BundlerTx) -> Result { 366 | let tx = tx.as_bytes()?; 367 | 368 | let response = self 369 | .client 370 | .post( 371 | self.url 372 | .join(&format!("tx/{}", self.token.get_type())) 373 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 374 | ) 375 | .header("Content-Type", "application/octet-stream") 376 | .body(tx) 377 | .send() 378 | .await; 379 | 380 | let checked_res = check_and_return::(response).await?; 381 | serde_json::from_value(checked_res).map_err(|e| BundlerError::Unknown(e.to_string())) 382 | } 383 | 384 | /// Sends determined amount to fund an account in the Irys bundler node 385 | /// # Example 386 | /// 387 | /// ``` 388 | /// # use irys_sdk::{ 389 | /// # token::TokenType, 390 | /// # BundlerClientBuilder, 391 | /// # token::arweave::ArweaveBuilder, 392 | /// # tags::Tag, 393 | /// # error::BuilderError 394 | /// # }; 395 | /// # use reqwest::Url; 396 | /// # use std::{path::PathBuf, str::FromStr}; 397 | /// # #[tokio::main] 398 | /// # async fn main() -> Result<(), BuilderError> { 399 | /// # let url = Url::parse("https://uploader.irys.xyz").unwrap(); 400 | /// # let wallet = PathBuf::from_str("res/test_wallet.json").expect("Invalid wallet path"); 401 | /// # let token = ArweaveBuilder::new() 402 | /// # .keypair_path(wallet) 403 | /// # .build() 404 | /// # .expect("Could not create token instance"); 405 | /// # let bundler_client = BundlerClientBuilder::new() 406 | /// # .url(url) 407 | /// # .token(token) 408 | /// # .fetch_pub_info() 409 | /// # .await? 410 | /// # .build()?; 411 | /// let data = b"Hello".to_vec(); 412 | /// let res = bundler_client.fund(data.len() as u64, None).await; 413 | /// # Ok(()) 414 | /// # } 415 | pub async fn fund(&self, amount: u64, multiplier: Option) -> Result { 416 | let multiplier = multiplier.unwrap_or(1.0); 417 | let curr_str = &self.token.get_type().to_string().to_lowercase(); 418 | let to = match self.pub_info.addresses.get(curr_str) { 419 | Some(ok) => ok, 420 | None => return Err(BundlerError::InvalidKey("No address found".to_owned())), 421 | }; 422 | let fee: u64 = match self.token.needs_fee() { 423 | true => self.token.get_fee(amount, to, multiplier).await?, 424 | false => Zero::zero(), 425 | }; 426 | 427 | let tx = self.token.create_tx(amount, to, fee).await?; 428 | let tx_res = self.token.send_tx(tx).await?; 429 | 430 | self.submit_fund_tx(tx_res.tx_id).await 431 | } 432 | 433 | pub async fn submit_fund_tx(&self, tx_id: String) -> Result { 434 | let post_tx_res = self 435 | .client 436 | .post( 437 | self.url 438 | .join(&format!("account/balance/{}", self.token.get_type())) 439 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 440 | ) 441 | .json(&FundBody { tx_id }) 442 | .send() 443 | .await; 444 | 445 | check_and_return::(post_tx_res).await.map(|_| true) 446 | } 447 | 448 | /// Sends a request for withdrawing an amount from Irys bundler node 449 | /// # Example 450 | /// 451 | /// ``` 452 | /// # use irys_sdk::{ 453 | /// # token::TokenType, 454 | /// # BundlerClientBuilder, 455 | /// # token::arweave::ArweaveBuilder, 456 | /// # tags::Tag, 457 | /// # error::BuilderError 458 | /// # }; 459 | /// # use reqwest::Url; 460 | /// # use std::{path::PathBuf, str::FromStr}; 461 | /// # #[tokio::main] 462 | /// # async fn main() -> Result<(), BuilderError> { 463 | /// # let url = Url::parse("https://uploader.irys.xyz").unwrap(); 464 | /// # let wallet = PathBuf::from_str("res/test_wallet.json").expect("Invalid wallet path"); 465 | /// # let token = ArweaveBuilder::new() 466 | /// # .keypair_path(wallet) 467 | /// # .build() 468 | /// # .expect("Could not create token instance"); 469 | /// # let bundler_client = BundlerClientBuilder::new() 470 | /// # .url(url) 471 | /// # .token(token) 472 | /// # .fetch_pub_info() 473 | /// # .await? 474 | /// # .build()?; 475 | /// let res = bundler_client.withdraw(10000).await; 476 | /// # Ok(()) 477 | /// # } 478 | pub async fn withdraw(&self, amount: u64) -> Result { 479 | let token_type = self.token.get_type().to_string().to_lowercase(); 480 | let public_key = Base64(self.token.get_pub_key()?.to_vec()); 481 | let wallet_address = self.token.wallet_address()?; 482 | let nonce = get_nonce(&self.client, &self.url, wallet_address, token_type.clone()).await?; 483 | 484 | let data = DeepHashChunk::Chunks(vec![ 485 | DeepHashChunk::Chunk(Bytes::copy_from_slice(token_type.as_bytes())), 486 | DeepHashChunk::Chunk(Bytes::copy_from_slice(amount.to_string().as_bytes())), 487 | DeepHashChunk::Chunk(Bytes::copy_from_slice(nonce.to_string().as_bytes())), 488 | ]); 489 | 490 | let dh = deep_hash(data).await?; 491 | let signature = Base64(self.token.sign_message(&dh)?); 492 | self.token.verify(&public_key.0, &dh, &signature.0)?; 493 | 494 | let data = WithdrawBody { 495 | public_key: Base64(public_key.to_string().into_bytes()), 496 | token: self.token.get_type().to_string().to_lowercase(), 497 | amount: amount.to_string(), 498 | nonce, 499 | signature: Base64(signature.to_string().into_bytes()), 500 | sig_type: self.token.get_type() as u16, 501 | }; 502 | 503 | let res = self 504 | .client 505 | .post( 506 | self.url 507 | .join("/account/withdraw") 508 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 509 | ) 510 | .json(&data) 511 | .send() 512 | .await; 513 | 514 | check_and_return::(res).await.map(|_| true) 515 | } 516 | 517 | /// Upload file on specified path 518 | /// 519 | /// # Example 520 | /// 521 | /// ``` 522 | /// # use irys_sdk::{ 523 | /// # token::TokenType, 524 | /// # BundlerClientBuilder, 525 | /// # token::arweave::ArweaveBuilder, 526 | /// # tags::Tag, 527 | /// # error::BuilderError 528 | /// # }; 529 | /// # use reqwest::Url; 530 | /// # use std::{path::PathBuf, str::FromStr}; 531 | /// # #[tokio::main] 532 | /// # async fn main() -> Result<(), BuilderError> { 533 | /// # let url = Url::parse("https://uploader.irys.xyz").unwrap(); 534 | /// # let wallet = PathBuf::from_str("res/test_wallet.json").expect("Invalid wallet path"); 535 | /// # let token = ArweaveBuilder::new() 536 | /// # .keypair_path(wallet) 537 | /// # .build() 538 | /// # .expect("Could not create token instance"); 539 | /// # let mut bundler_client = BundlerClientBuilder::new() 540 | /// # .url(url) 541 | /// # .token(token) 542 | /// # .fetch_pub_info() 543 | /// # .await? 544 | /// # .build()?; 545 | /// let file = PathBuf::from_str("res/test_image.jpg").expect("Invalid wallet path"); 546 | /// let result = bundler_client.upload_file(file).await; 547 | /// # Ok(()) 548 | /// # } 549 | /// ``` 550 | pub async fn upload_file(&mut self, file_path: PathBuf) -> Result { 551 | let mut tags = vec![]; 552 | if let Some(content_type) = mime_guess::from_path(file_path.clone()).first() { 553 | let content_tag: Tag = Tag::new("Content-Type", content_type.as_ref()); 554 | tags.push(content_tag); 555 | } 556 | 557 | let data = fs::read(&file_path)?; 558 | 559 | // self.uploader.upload(data).await 560 | let mut tx = self.create_transaction(data, tags)?; 561 | self.sign_transaction(&mut tx).await?; 562 | 563 | self.send_transaction(tx).await 564 | } 565 | 566 | /* 567 | pub async fn upload_directory( 568 | &self, 569 | directory_path: PathBuf, 570 | manifest_path: PathBuf, 571 | ) -> Result<(), BundlerError> { 572 | todo!(); 573 | } 574 | */ 575 | } 576 | 577 | #[cfg(test)] 578 | mod tests { 579 | use std::str::FromStr; 580 | 581 | use crate::{ 582 | bundler::{get_balance, get_price}, 583 | token::TokenType, 584 | }; 585 | use httpmock::{Method::GET, MockServer}; 586 | use num::BigUint; 587 | use reqwest::Url; 588 | 589 | #[tokio::test] 590 | async fn should_send_transactions_correctly() { 591 | //TODO: fix this test 592 | /* 593 | let server = MockServer::start(); 594 | let mock = server.mock(|when, then| { 595 | when.method(POST).path("/tx/arweave"); 596 | then.status(200) 597 | .header("Content-Type", "application/octet-stream") 598 | .body("{}"); 599 | }); 600 | let mock_2 = server.mock(|when, then| { 601 | when.method(GET) 602 | .path("/info"); 603 | then.status(200) 604 | .body("{ \"version\": \"0\", \"gateway\": \"gateway\", \"addresses\": { \"arweave\": \"address\" }}"); 605 | }); 606 | 607 | let url = Url::from_str(&server.url("")).unwrap(); 608 | let path = PathBuf::from_str("res/test_wallet.json").unwrap(); 609 | let token = Arweave::new(path, url.clone()); 610 | let bundler = &bundler::new(url, &token).await; 611 | let tx = bundler.create_transaction_with_tags( 612 | Vec::from("hello"), 613 | vec![Tag::new("name".to_string(), "value".to_string())], 614 | ); 615 | let value = bundler.send_transaction(tx).await.unwrap(); 616 | 617 | mock.assert(); 618 | mock_2.assert(); 619 | assert_eq!(value.to_string(), "{}"); 620 | */ 621 | } 622 | 623 | #[tokio::test] 624 | async fn should_fetch_balance_correctly() { 625 | let server = MockServer::start(); 626 | let mock = server.mock(|when, then| { 627 | when.method(GET) 628 | .path("/account/balance/arweave") 629 | .query_param("address", "address"); 630 | then.status(200) 631 | .header("content-type", "application/json") 632 | .body("{ \"balance\": \"10\" }"); 633 | }); 634 | 635 | let url = Url::from_str(&server.url("")).unwrap(); 636 | let address = "address"; 637 | 638 | let balance = get_balance(&url, TokenType::Arweave, address, &reqwest::Client::new()) 639 | .await 640 | .unwrap(); 641 | 642 | mock.assert(); 643 | assert_eq!(balance, "10".parse::().unwrap()); 644 | } 645 | 646 | #[tokio::test] 647 | async fn should_fetch_price_correctly() { 648 | let server = MockServer::start(); 649 | let mock = server.mock(|when, then| { 650 | when.method(GET).path("/price/arweave/123123123"); 651 | then.status(200) 652 | .header("content-type", "application/json") 653 | .body("321321321"); 654 | }); 655 | 656 | let url = Url::from_str(&server.url("")).unwrap(); 657 | let balance = get_price(&url, TokenType::Arweave, &reqwest::Client::new(), 123123123) 658 | .await 659 | .expect("Could not get price"); 660 | 661 | mock.assert(); 662 | assert_eq!(balance, "321321321".parse::().unwrap()); 663 | } 664 | 665 | #[tokio::test] 666 | async fn should_fund_address_correctly() {} 667 | } 668 | -------------------------------------------------------------------------------- /src/client/balance.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Url; 2 | 3 | use crate::{bundler::get_balance, error::BundlerError, token::TokenType}; 4 | 5 | pub async fn run_balance( 6 | url: Url, 7 | address: &str, 8 | token: TokenType, 9 | ) -> Result { 10 | let client = reqwest::Client::new(); 11 | get_balance(&url, token, address, &client) 12 | .await 13 | .map(|balance| balance.to_string()) 14 | } 15 | -------------------------------------------------------------------------------- /src/client/bin/cli.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use clap::{Parser, Subcommand}; 4 | use irys_sdk::{ 5 | client::{ 6 | balance::run_balance, fund::run_fund, price::run_price, upload::run_upload, 7 | withdraw::run_withdraw, 8 | }, 9 | token::TokenType, 10 | }; 11 | use reqwest::Url; 12 | 13 | const DEFAULT_BYTE_AMOUNT: u64 = 1; 14 | const DEFAULT_TIMEOUT: u64 = 1000 * 30; //30 secs 15 | const DEFAULT_TIMEOUT_FUND: u64 = 1000 * 60 * 30; //30 mins 16 | 17 | #[derive(Parser)] 18 | struct Args { 19 | #[clap(subcommand)] 20 | command: Command, 21 | } 22 | #[derive(Subcommand)] 23 | enum Command { 24 | ///Gets the specified user's balance for the current Irys bundler node 25 | Balance { 26 | //Address to query balance 27 | #[clap(value_parser)] 28 | address: String, 29 | 30 | //Timeout for operation 31 | #[clap(long = "timeout")] 32 | timeout: Option, 33 | 34 | //Host address 35 | #[clap(long = "host")] 36 | host: Url, 37 | 38 | //Token type 39 | #[clap(short = 't', long = "token")] 40 | token: TokenType, 41 | }, 42 | ///Funds your account with the specified amount of atomic units 43 | Fund { 44 | //Amounts, in winston, to send in funding 45 | #[clap(value_parser)] 46 | amount: u64, 47 | 48 | //Timeout for operation 49 | #[clap(long = "timeout")] 50 | timeout: Option, 51 | 52 | //Path to wallet 53 | #[clap(short = 'w', long = "wallet")] 54 | wallet: String, 55 | 56 | //Host address 57 | #[clap(long = "host")] 58 | host: Url, 59 | 60 | //Token type 61 | #[clap(short = 't', long = "token")] 62 | token: TokenType, 63 | }, 64 | ///Sends a fund withdrawal request 65 | Withdraw { 66 | //Amounts, in winston, to send in withdraw 67 | #[clap(value_parser)] 68 | amount: u64, 69 | 70 | //Timeout for operation 71 | #[clap(long = "timeout")] 72 | timeout: Option, 73 | 74 | //Path to wallet 75 | #[clap(short = 'w', long = "wallet")] 76 | wallet: String, 77 | 78 | //Host address 79 | #[clap(long = "host")] 80 | host: Url, 81 | 82 | //Token type 83 | #[clap(short = 't', long = "token")] 84 | token: TokenType, 85 | }, 86 | ///Uploads a specified file 87 | Upload { 88 | //Path to file that will be uploaded 89 | #[clap(value_parser)] 90 | file_path: String, 91 | 92 | //Timeout for operation 93 | #[clap(long = "timeout")] 94 | timeout: Option, 95 | 96 | //Path to wallet 97 | #[clap(short = 'w', long = "wallet")] 98 | wallet: String, 99 | 100 | //Host address 101 | #[clap(long = "host")] 102 | host: Url, 103 | 104 | //Token type 105 | #[clap(short = 't', long = "token")] 106 | token: TokenType, 107 | }, 108 | ///Uploads a folder (with a manifest) 109 | UploadDir {}, 110 | ///Check how much of a specific token is required for an upload of bytes 111 | Price { 112 | //Amounts of bytes to calculate pricing 113 | #[clap(value_parser)] 114 | byte_amount: Option, 115 | 116 | //Timeout for operation 117 | #[clap(long = "timeout")] 118 | timeout: Option, 119 | 120 | //Host address 121 | #[clap(long = "host")] 122 | host: Url, 123 | 124 | //Token type 125 | #[clap(short = 't', long = "token")] 126 | token: TokenType, 127 | }, 128 | } 129 | 130 | impl Command { 131 | async fn execute(self) { 132 | match self { 133 | Command::Balance { 134 | address, 135 | timeout, 136 | host, 137 | token, 138 | } => { 139 | let work = run_balance(host, &address, token); 140 | let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); 141 | match tokio::time::timeout(Duration::from_millis(timeout), work).await { 142 | Ok(res) => match res { 143 | Ok(ok) => println!("[Ok] {}", ok), 144 | Err(err) => println!("[Err] {}", err), 145 | }, 146 | Err(err) => println!("Error running task: {}", err), 147 | } 148 | } 149 | Command::Fund { 150 | amount, 151 | timeout, 152 | wallet, 153 | host, 154 | token, 155 | } => { 156 | let work = run_fund(amount, host, &wallet, token); 157 | let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT_FUND); 158 | match tokio::time::timeout(Duration::from_millis(timeout), work).await { 159 | Ok(res) => match res { 160 | Ok(ok) => println!("[Ok] {}", ok), 161 | Err(err) => println!("[Err] {}", err), 162 | }, 163 | Err(err) => println!("Error running task: {}", err), 164 | } 165 | } 166 | Command::Withdraw { 167 | amount, 168 | timeout, 169 | wallet, 170 | host, 171 | token, 172 | } => { 173 | let work = run_withdraw(amount, host, &wallet, token); 174 | let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); 175 | match tokio::time::timeout(Duration::from_millis(timeout), work).await { 176 | Ok(res) => match res { 177 | Ok(ok) => println!("[Ok] {}", ok), 178 | Err(err) => println!("[Err] {}", err), 179 | }, 180 | Err(err) => println!("Error running task: {}", err), 181 | } 182 | } 183 | Command::Upload { 184 | file_path, 185 | timeout, 186 | wallet, 187 | host, 188 | token, 189 | } => { 190 | let work = run_upload(file_path, host, &wallet, token); 191 | let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); 192 | match tokio::time::timeout(Duration::from_millis(timeout), work).await { 193 | Ok(res) => match res { 194 | Ok(ok) => println!("[Ok] {}", ok), 195 | Err(err) => println!("[Err] {}", err), 196 | }, 197 | Err(err) => println!("Error running task: {}", err), 198 | } 199 | } 200 | Command::UploadDir {} => todo!(), 201 | Command::Price { 202 | byte_amount, 203 | timeout, 204 | host, 205 | token, 206 | } => { 207 | let byte_amount = byte_amount.unwrap_or(DEFAULT_BYTE_AMOUNT); 208 | let work = run_price(host, token, byte_amount); 209 | let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT); 210 | match tokio::time::timeout(Duration::from_millis(timeout), work).await { 211 | Ok(res) => match res { 212 | Ok(ok) => println!("[Ok] {}", ok), 213 | Err(err) => println!("[Err] {}", err), 214 | }, 215 | Err(err) => println!("Error running task: {}", err), 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | #[tokio::main] 223 | async fn main() { 224 | let args = Args::parse(); 225 | 226 | args.command.execute().await; 227 | } 228 | -------------------------------------------------------------------------------- /src/client/fund.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use crate::{ 4 | bundler::BundlerClientBuilder, 5 | consts::USE_JS_SDK, 6 | error::BundlerError, 7 | token::{arweave::ArweaveBuilder, TokenType}, 8 | }; 9 | use num_traits::Zero; 10 | use reqwest::Url; 11 | 12 | pub async fn run_fund( 13 | amount: u64, 14 | url: Url, 15 | wallet: &str, 16 | token: TokenType, 17 | ) -> Result { 18 | if amount.is_zero() { 19 | return Err(BundlerError::InvalidAmount); 20 | } 21 | 22 | let wallet = PathBuf::from_str(wallet).expect("Invalid wallet path"); 23 | match token { 24 | TokenType::Arweave => { 25 | let token = ArweaveBuilder::new().keypair_path(wallet).build()?; 26 | let bundler_client = BundlerClientBuilder::new() 27 | .url(url) 28 | .token(token) 29 | .fetch_pub_info() 30 | .await? 31 | .build()?; 32 | bundler_client 33 | .fund(amount, None) 34 | .await 35 | .map(|res| res.to_string()) 36 | } 37 | TokenType::Solana => todo!("{}", USE_JS_SDK), 38 | TokenType::Ethereum => todo!("{}", USE_JS_SDK), 39 | TokenType::Erc20 => todo!("{}", USE_JS_SDK), 40 | TokenType::Cosmos => todo!("{}", USE_JS_SDK), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/client/method.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | 3 | #[derive(ValueEnum, Clone, Debug)] 4 | pub enum Method { 5 | Help = 0, 6 | Balance = 1, 7 | Withdraw = 2, 8 | Upload = 3, 9 | UploadDir = 4, 10 | Fund = 5, 11 | Price = 6, 12 | } 13 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod balance; 2 | pub mod fund; 3 | pub mod method; 4 | pub mod price; 5 | pub mod upload; 6 | pub mod withdraw; 7 | -------------------------------------------------------------------------------- /src/client/price.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Url; 2 | 3 | use crate::{bundler::get_price, error::BundlerError, token::TokenType}; 4 | 5 | pub async fn run_price( 6 | url: Url, 7 | token: TokenType, 8 | byte_amount: u64, 9 | ) -> Result { 10 | let client = reqwest::Client::new(); 11 | get_price(&url, token, &client, byte_amount) 12 | .await 13 | .map(|balance| { 14 | format!( 15 | "{} bytes in {} is {} base units", //TODO: refactor this to show base unit name 16 | byte_amount, token, balance, 17 | ) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/client/upload.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufReader, Read}, 4 | path::PathBuf, 5 | str::FromStr, 6 | }; 7 | 8 | use crate::{ 9 | bundler::BundlerClientBuilder, 10 | consts::VERSION, 11 | error::BundlerError, 12 | tags::Tag, 13 | token::{arweave::ArweaveBuilder, ethereum::EthereumBuilder, solana::SolanaBuilder, TokenType}, 14 | }; 15 | use reqwest::Url; 16 | 17 | pub async fn run_upload( 18 | file_path: String, 19 | url: Url, 20 | wallet: &str, 21 | token: TokenType, 22 | ) -> Result { 23 | let f = File::open(file_path.clone()).expect("Invalid file path"); 24 | let mut reader = BufReader::new(f); 25 | let mut buffer = Vec::new(); 26 | 27 | // Read file into vector. 28 | reader.read_to_end(&mut buffer)?; 29 | 30 | let base_tag = Tag::new("User-Agent", &format!("irys-bundler-sdk-rs/{}", VERSION)); 31 | 32 | match token { 33 | TokenType::Arweave => { 34 | let wallet = PathBuf::from_str(wallet) 35 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 36 | let token = ArweaveBuilder::new().keypair_path(wallet).build()?; 37 | let bundler_client = BundlerClientBuilder::new() 38 | .url(url) 39 | .token(token) 40 | .fetch_pub_info() 41 | .await? 42 | .build()?; 43 | let mut tx = bundler_client.create_transaction(buffer, vec![base_tag])?; 44 | let sig = bundler_client.sign_transaction(&mut tx).await; 45 | assert!(sig.is_ok()); 46 | match bundler_client.send_transaction(tx).await { 47 | Ok(res) => Ok(format!("File {} uploaded: {:?}", file_path, res)), 48 | Err(err) => Err(BundlerError::UploadError(err.to_string())), 49 | } 50 | } 51 | TokenType::Solana => { 52 | let token = SolanaBuilder::new().wallet(wallet).build()?; 53 | let bundler_client = BundlerClientBuilder::new() 54 | .url(url) 55 | .token(token) 56 | .fetch_pub_info() 57 | .await? 58 | .build()?; 59 | let mut tx = bundler_client.create_transaction(buffer, vec![base_tag])?; 60 | let sig = bundler_client.sign_transaction(&mut tx).await; 61 | assert!(sig.is_ok()); 62 | match bundler_client.send_transaction(tx).await { 63 | Ok(res) => Ok(format!("File {} uploaded: {:?}", file_path, res)), 64 | Err(err) => Err(BundlerError::UploadError(err.to_string())), 65 | } 66 | } 67 | TokenType::Ethereum => { 68 | let token = EthereumBuilder::new().wallet(wallet).build()?; 69 | let bundler_client = BundlerClientBuilder::new() 70 | .url(url) 71 | .token(token) 72 | .fetch_pub_info() 73 | .await? 74 | .build()?; 75 | let mut tx = bundler_client.create_transaction(buffer, vec![base_tag])?; 76 | let sig = bundler_client.sign_transaction(&mut tx).await; 77 | assert!(sig.is_ok()); 78 | match bundler_client.send_transaction(tx).await { 79 | Ok(res) => Ok(format!("File {} uploaded: {:?}", file_path, res)), 80 | Err(err) => Err(BundlerError::UploadError(err.to_string())), 81 | } 82 | } 83 | TokenType::Erc20 => todo!(), 84 | TokenType::Cosmos => todo!(), 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/client/withdraw.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use crate::{ 4 | bundler::BundlerClientBuilder, 5 | consts::USE_JS_SDK, 6 | error::BundlerError, 7 | token::{arweave::ArweaveBuilder, TokenType}, 8 | }; 9 | use num_traits::Zero; 10 | use reqwest::Url; 11 | 12 | pub async fn run_withdraw( 13 | amount: u64, 14 | url: Url, 15 | wallet: &str, 16 | token: TokenType, 17 | ) -> Result { 18 | if amount.is_zero() { 19 | return Err(BundlerError::InvalidAmount); 20 | } 21 | 22 | match token { 23 | TokenType::Arweave => { 24 | let wallet = PathBuf::from_str(wallet).expect("Invalid wallet path"); 25 | let token = ArweaveBuilder::new().keypair_path(wallet).build()?; 26 | let bundler_client = BundlerClientBuilder::new() 27 | .url(url) 28 | .token(token) 29 | .fetch_pub_info() 30 | .await? 31 | .build()?; 32 | bundler_client 33 | .withdraw(amount) 34 | .await 35 | .map(|res| res.to_string()) 36 | } 37 | TokenType::Solana => todo!("{}", USE_JS_SDK), 38 | TokenType::Ethereum => todo!("{}", USE_JS_SDK), 39 | TokenType::Erc20 => todo!("{}", USE_JS_SDK), 40 | TokenType::Cosmos => todo!("{}", USE_JS_SDK), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 2 | 3 | pub const DEFAULT_BUNDLER_URL: &str = "https://uploader.irys.xyz/"; 4 | pub const CHUNK_SIZE: u64 = 256u64 * 1024; 5 | /// Multiplier applied to the buffer argument from the cli to determine the maximum number 6 | /// of simultaneous request to the `chunk/ endpoint`. 7 | pub const CHUNKS_BUFFER_FACTOR: usize = 20; 8 | 9 | /// Number of times to retry posting chunks if not successful. 10 | pub const CHUNKS_RETRIES: u16 = 10; 11 | 12 | /// Number of seconds to wait between retying to post a failed chunk. 13 | pub const CHUNKS_RETRY_SLEEP: u64 = 1; 14 | 15 | /// Number of seconds to wait between retying to post a failed chunk. 16 | pub const RETRY_SLEEP: u64 = 10; 17 | 18 | /// Number of confirmations needed to consider a transaction funded 19 | pub const CONFIRMATIONS_NEEDED: u64 = 5; 20 | 21 | pub const USE_JS_SDK: &str = "Currently unsupported, please use the js-sdk (https://github.com/Irys-xyz/js-sdk) to perform this operation (PRs welcome!)"; 22 | 23 | pub const LIST_AS_BUFFER: &[u8] = "list".as_bytes(); 24 | pub const BLOB_AS_BUFFER: &[u8] = "blob".as_bytes(); 25 | pub const DATAITEM_AS_BUFFER: &[u8] = "dataitem".as_bytes(); 26 | pub const ONE_AS_BUFFER: &[u8] = "1".as_bytes(); 27 | -------------------------------------------------------------------------------- /src/deep_hash.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use async_recursion::async_recursion; 4 | use bytes::Bytes; 5 | use sha2::{Digest, Sha384}; 6 | 7 | use crate::{ 8 | consts::{BLOB_AS_BUFFER, LIST_AS_BUFFER}, 9 | error::BundlerError, 10 | }; 11 | use futures::{Stream, TryStream, TryStreamExt}; 12 | 13 | pub enum DeepHashChunk<'a> { 14 | Chunk(Bytes), 15 | Stream(&'a mut Pin>>>), 16 | Chunks(Vec>), 17 | } 18 | 19 | trait Foo: Stream> + TryStream {} 20 | 21 | pub async fn deep_hash(chunk: DeepHashChunk<'_>) -> Result { 22 | match chunk { 23 | DeepHashChunk::Chunk(b) => { 24 | let tag = [BLOB_AS_BUFFER, b.len().to_string().as_bytes()].concat(); 25 | let c = [sha384hash(tag.into()), sha384hash(b)].concat(); 26 | Ok(Bytes::copy_from_slice(&sha384hash(c.into()))) 27 | } 28 | DeepHashChunk::Stream(s) => { 29 | let mut hasher = Sha384::new(); 30 | let mut length = 0; 31 | while let Some(chunk) = s 32 | .as_mut() 33 | .try_next() 34 | .await 35 | .map_err(|_| BundlerError::NoBytesLeft)? 36 | { 37 | length += chunk.len(); 38 | hasher.update(&chunk); 39 | } 40 | 41 | let tag = [BLOB_AS_BUFFER, length.to_string().as_bytes()].concat(); 42 | 43 | let tagged_hash = [ 44 | sha384hash(tag.into()), 45 | Bytes::copy_from_slice(&hasher.finalize()), 46 | ] 47 | .concat(); 48 | 49 | Ok(sha384hash(tagged_hash.into())) 50 | } 51 | DeepHashChunk::Chunks(mut chunks) => { 52 | // Be careful of truncation 53 | let len = chunks.len() as f64; 54 | let tag = [LIST_AS_BUFFER, len.to_string().as_bytes()].concat(); 55 | 56 | let acc = sha384hash(tag.into()); 57 | 58 | deep_hash_chunks(&mut chunks, acc).await 59 | } 60 | } 61 | } 62 | 63 | #[async_recursion(?Send)] 64 | pub async fn deep_hash_chunks( 65 | chunks: &mut Vec>, 66 | acc: Bytes, 67 | ) -> Result { 68 | if chunks.is_empty() { 69 | return Ok(acc); 70 | }; 71 | 72 | let acc = Bytes::copy_from_slice(&acc); 73 | 74 | let hash_pair = [acc, deep_hash(chunks.remove(0)).await?].concat(); 75 | let new_acc = sha384hash(hash_pair.into()); 76 | deep_hash_chunks(chunks, new_acc).await 77 | } 78 | 79 | fn sha384hash(b: Bytes) -> Bytes { 80 | let mut hasher = Sha384::new(); 81 | hasher.update(&b); 82 | Bytes::copy_from_slice(&hasher.finalize()) 83 | } 84 | -------------------------------------------------------------------------------- /src/deep_hash_sync.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use sha2::{Digest, Sha384}; 3 | 4 | use crate::{ 5 | consts::{BLOB_AS_BUFFER, LIST_AS_BUFFER}, 6 | deep_hash::DeepHashChunk, 7 | error::BundlerError, 8 | }; 9 | use futures::{Stream, TryStream}; 10 | 11 | trait Foo: Stream> + TryStream {} 12 | 13 | pub fn deep_hash_sync(chunk: DeepHashChunk) -> Result { 14 | match chunk { 15 | DeepHashChunk::Chunk(b) => { 16 | let tag = [BLOB_AS_BUFFER, b.len().to_string().as_bytes()].concat(); 17 | let c = [sha384hash(tag.into()), sha384hash(b)].concat(); 18 | Ok(Bytes::copy_from_slice(&sha384hash(c.into()))) 19 | } 20 | DeepHashChunk::Chunks(chunks) => { 21 | // Be careful of truncation 22 | let len = chunks.len() as f64; 23 | let tag = [LIST_AS_BUFFER, len.to_string().as_bytes()].concat(); 24 | 25 | let acc = sha384hash(tag.into()); 26 | 27 | deep_hash_chunks_sync(chunks, acc) 28 | } 29 | _ => Err(BundlerError::Unsupported( 30 | "Streaming is not supported for sync".to_owned(), 31 | )), 32 | } 33 | } 34 | 35 | pub fn deep_hash_chunks_sync( 36 | mut chunks: Vec, 37 | acc: Bytes, 38 | ) -> Result { 39 | if chunks.is_empty() { 40 | return Ok(acc); 41 | }; 42 | 43 | let acc = Bytes::copy_from_slice(&acc); 44 | 45 | let hash_pair = [acc, deep_hash_sync(chunks.remove(0))?].concat(); 46 | let new_acc = sha384hash(hash_pair.into()); 47 | deep_hash_chunks_sync(chunks, new_acc) 48 | } 49 | 50 | fn sha384hash(b: Bytes) -> Bytes { 51 | let mut hasher = Sha384::new(); 52 | hasher.update(&b); 53 | Bytes::copy_from_slice(&hasher.finalize()) 54 | } 55 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use web3::signing::RecoveryError; 3 | 4 | use crate::utils::Eip712Error; 5 | 6 | #[derive(Debug, Error)] 7 | pub enum BundlerError { 8 | #[error("Invalid headers provided.")] 9 | InvalidHeaders, 10 | 11 | #[error("Invalid signer type used.")] 12 | InvalidSignerType, 13 | 14 | #[error("Invalid presence byte {0}")] 15 | InvalidPresenceByte(String), 16 | 17 | #[error("No bytes left.")] 18 | NoBytesLeft, 19 | 20 | #[error("Invalid tag encoding.")] 21 | InvalidTagEncoding, 22 | 23 | #[error("File system error: {0}")] 24 | FsError(String), 25 | 26 | #[error("Invalid signature.")] 27 | InvalidSignature, 28 | 29 | #[error("Invalid value for funding.")] 30 | InvalidFundingValue, 31 | 32 | #[error("Invalid amount, must be a integer bigger than zero")] 33 | InvalidAmount, 34 | 35 | #[error("Invalid wallet {0}")] 36 | InvalidKey(String), 37 | 38 | #[error("Invalid token: {0}")] 39 | InvalidToken(String), 40 | 41 | #[error("Response failed with the following error: {0}")] 42 | ResponseError(String), 43 | 44 | #[error("Failed to sign message: {0}")] 45 | SigningError(String), 46 | 47 | #[error("Request error: {0}.")] 48 | RequestError(String), 49 | 50 | #[error("Tx not found")] 51 | TxNotFound, 52 | 53 | #[error("Tx status not confirmed")] 54 | TxStatusNotConfirmed, 55 | 56 | #[error("Chunk size out of allowed range: {0} - {1}")] 57 | ChunkSizeOutOfRange(u64, u64), 58 | 59 | #[error("Error posting chunk: {0}")] 60 | PostChunkError(String), 61 | 62 | #[error("No signature present")] 63 | NoSignature, 64 | 65 | #[error("Cannot convert file stream to known bytes. Try using another method")] 66 | InvalidDataType, 67 | 68 | #[error("Arweave Sdk error: {0}")] 69 | ArweaveSdkError(arweave_rs::error::Error), 70 | 71 | #[error("Token error: {0}")] 72 | TokenError(String), 73 | 74 | #[error("Error reading/writting bytes: {0}")] 75 | BytesError(String), 76 | 77 | #[error("Error converting type: {0}")] 78 | TypeParseError(String), 79 | 80 | #[error("Parse error error: {0}")] 81 | ParseError(String), 82 | 83 | #[error("Upload error: {0}")] 84 | UploadError(String), 85 | 86 | #[error("Unknown: {0}")] 87 | Unknown(String), 88 | 89 | #[error("Unsupported: {0}")] 90 | Unsupported(String), 91 | 92 | #[error("ED25519 error: {0}")] 93 | ED25519Error(ed25519_dalek::ed25519::Error), 94 | 95 | #[error("Secp256k1 error: {0}")] 96 | Secp256k1Error(secp256k1::Error), 97 | 98 | #[error("Base64 error: {0}")] 99 | Base64Error(String), 100 | 101 | #[error("Io error: {0}")] 102 | IoError(std::io::Error), 103 | 104 | #[error("Builder error: {0}")] 105 | BuilderError(BuilderError), 106 | 107 | #[error("Eip712 error: {0}")] 108 | Eip712Error(Eip712Error), 109 | 110 | #[error("RecoveryError")] 111 | RecoveryError(RecoveryError), 112 | } 113 | 114 | impl From for BundlerError { 115 | fn from(value: BuilderError) -> Self { 116 | Self::BuilderError(value) 117 | } 118 | } 119 | 120 | impl From for BundlerError { 121 | fn from(value: arweave_rs::error::Error) -> Self { 122 | Self::ArweaveSdkError(value) 123 | } 124 | } 125 | 126 | #[derive(Debug, Error)] 127 | pub enum BuilderError { 128 | #[error("Bundler Error {0}")] 129 | BundlerError(String), 130 | 131 | #[error("Missing field {0}")] 132 | MissingField(String), 133 | 134 | #[error("Fetch pub info error: {0}")] 135 | FetchPubInfoError(String), 136 | 137 | #[error("Arweave Sdk error: {0}")] 138 | ArweaveSdkError(arweave_rs::error::Error), 139 | } 140 | 141 | impl From for BuilderError { 142 | fn from(value: arweave_rs::error::Error) -> Self { 143 | Self::ArweaveSdkError(value) 144 | } 145 | } 146 | 147 | impl From for BuilderError { 148 | fn from(value: BundlerError) -> Self { 149 | Self::BundlerError(value.to_string()) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use derive_more::Display; 3 | use num_derive::FromPrimitive; 4 | use std::panic; 5 | 6 | use crate::Verifier; 7 | 8 | #[cfg(feature = "arweave")] 9 | use crate::ArweaveSigner; 10 | 11 | #[cfg(any(feature = "solana", feature = "algorand"))] 12 | use crate::Ed25519Signer; 13 | 14 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 15 | use crate::Secp256k1Signer; 16 | 17 | #[cfg(feature = "cosmos")] 18 | use crate::CosmosSigner; 19 | 20 | #[cfg(feature = "aptos")] 21 | use crate::AptosSigner; 22 | 23 | #[cfg(feature = "aptos")] 24 | use crate::MultiAptosSigner; 25 | 26 | use crate::error::BundlerError; 27 | use crate::signers::typed_ethereum::TypedEthereumSigner; 28 | 29 | #[derive(FromPrimitive, Display, PartialEq, Eq, Debug, Clone)] 30 | pub enum SignerMap { 31 | None = -1, 32 | Arweave = 1, 33 | ED25519 = 2, 34 | Ethereum = 3, 35 | Solana = 4, 36 | InjectedAptos = 5, 37 | MultiAptos = 6, 38 | TypedEthereum = 7, 39 | Cosmos, //TODO: assign constant 40 | } 41 | 42 | pub struct Config { 43 | pub sig_length: usize, 44 | pub pub_length: usize, 45 | pub sig_name: String, 46 | } 47 | 48 | #[allow(unused)] 49 | impl Config { 50 | pub fn total_length(&self) -> u32 { 51 | self.sig_length as u32 + self.pub_length as u32 52 | } 53 | } 54 | 55 | impl From for SignerMap { 56 | fn from(t: u16) -> Self { 57 | match t { 58 | 1 => SignerMap::Arweave, 59 | 2 => SignerMap::ED25519, 60 | 3 => SignerMap::Ethereum, 61 | 4 => SignerMap::Solana, 62 | 5 => SignerMap::InjectedAptos, 63 | 6 => SignerMap::MultiAptos, 64 | 7 => SignerMap::TypedEthereum, 65 | _ => SignerMap::None, 66 | } 67 | } 68 | } 69 | 70 | impl SignerMap { 71 | pub fn as_u16(&self) -> u16 { 72 | match self { 73 | SignerMap::Arweave => 1, 74 | SignerMap::ED25519 => 2, 75 | SignerMap::Ethereum => 3, 76 | SignerMap::Solana => 4, 77 | SignerMap::InjectedAptos => 5, 78 | SignerMap::MultiAptos => 6, 79 | SignerMap::TypedEthereum => 7, 80 | _ => u16::MAX, 81 | } 82 | } 83 | 84 | pub fn get_config(&self) -> Config { 85 | match *self { 86 | #[cfg(feature = "arweave")] 87 | SignerMap::Arweave => Config { 88 | sig_length: 512, 89 | pub_length: 512, 90 | sig_name: "arweave".to_owned(), 91 | }, 92 | #[cfg(feature = "algorand")] 93 | SignerMap::ED25519 => Config { 94 | sig_length: ed25519_dalek::SIGNATURE_LENGTH, 95 | pub_length: ed25519_dalek::PUBLIC_KEY_LENGTH, 96 | sig_name: "ed25519".to_owned(), 97 | }, 98 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 99 | SignerMap::Ethereum => Config { 100 | sig_length: secp256k1::constants::COMPACT_SIGNATURE_SIZE + 1, 101 | pub_length: secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE, 102 | sig_name: "ethereum".to_owned(), 103 | }, 104 | #[cfg(feature = "solana")] 105 | SignerMap::Solana => Config { 106 | sig_length: ed25519_dalek::SIGNATURE_LENGTH, 107 | pub_length: ed25519_dalek::PUBLIC_KEY_LENGTH, 108 | sig_name: "solana".to_owned(), 109 | }, 110 | #[cfg(feature = "aptos")] 111 | SignerMap::InjectedAptos => Config { 112 | sig_length: ed25519_dalek::SIGNATURE_LENGTH, 113 | pub_length: ed25519_dalek::PUBLIC_KEY_LENGTH, 114 | sig_name: "injectedAptos".to_owned(), 115 | }, 116 | #[cfg(feature = "aptos")] 117 | SignerMap::MultiAptos => Config { 118 | sig_length: ed25519_dalek::SIGNATURE_LENGTH * 32 + 4, // max 32 64 byte signatures, +4 for 32-bit bitmap 119 | pub_length: ed25519_dalek::PUBLIC_KEY_LENGTH * 32 + 1, // max 64 32 byte keys, +1 for 8-bit threshold value 120 | sig_name: "multiAptos".to_owned(), 121 | }, 122 | #[cfg(feature = "cosmos")] 123 | SignerMap::Cosmos => Config { 124 | sig_length: secp256k1::constants::COMPACT_SIGNATURE_SIZE, 125 | pub_length: secp256k1::constants::PUBLIC_KEY_SIZE, 126 | sig_name: "cosmos".to_owned(), 127 | }, 128 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 129 | SignerMap::TypedEthereum => Config { 130 | sig_length: secp256k1::constants::COMPACT_SIGNATURE_SIZE + 1, 131 | pub_length: 42, 132 | sig_name: "typedEthereum".to_owned(), 133 | }, 134 | #[allow(unreachable_patterns)] 135 | _ => panic!("{:?} get_config has no", self), 136 | } 137 | } 138 | 139 | pub fn verify(&self, pk: &[u8], message: &[u8], signature: &[u8]) -> Result<(), BundlerError> { 140 | match *self { 141 | #[cfg(feature = "arweave")] 142 | SignerMap::Arweave => ArweaveSigner::verify( 143 | Bytes::copy_from_slice(pk), 144 | Bytes::copy_from_slice(message), 145 | Bytes::copy_from_slice(signature), 146 | ), 147 | #[cfg(feature = "algorand")] 148 | SignerMap::ED25519 => Ed25519Signer::verify( 149 | Bytes::copy_from_slice(pk), 150 | Bytes::copy_from_slice(message), 151 | Bytes::copy_from_slice(signature), 152 | ), 153 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 154 | SignerMap::Ethereum => Secp256k1Signer::verify( 155 | Bytes::copy_from_slice(pk), 156 | Bytes::copy_from_slice(message), 157 | Bytes::copy_from_slice(signature), 158 | ), 159 | #[cfg(feature = "solana")] 160 | SignerMap::Solana => Ed25519Signer::verify( 161 | Bytes::copy_from_slice(pk), 162 | Bytes::copy_from_slice(message), 163 | Bytes::copy_from_slice(signature), 164 | ), 165 | #[cfg(feature = "aptos")] 166 | SignerMap::InjectedAptos => AptosSigner::verify( 167 | Bytes::copy_from_slice(pk), 168 | Bytes::copy_from_slice(message), 169 | Bytes::copy_from_slice(signature), 170 | ), 171 | #[cfg(feature = "aptos")] 172 | SignerMap::MultiAptos => MultiAptosSigner::verify( 173 | Bytes::copy_from_slice(pk), 174 | Bytes::copy_from_slice(message), 175 | Bytes::copy_from_slice(signature), 176 | ), 177 | #[cfg(feature = "cosmos")] 178 | SignerMap::Cosmos => CosmosSigner::verify( 179 | Bytes::copy_from_slice(pk), 180 | Bytes::copy_from_slice(message), 181 | Bytes::copy_from_slice(signature), 182 | ), 183 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 184 | SignerMap::TypedEthereum => TypedEthereumSigner::verify( 185 | Bytes::copy_from_slice(pk), 186 | Bytes::copy_from_slice(message), 187 | Bytes::copy_from_slice(signature), 188 | ), 189 | #[allow(unreachable_patterns)] 190 | _ => panic!("{:?} verify not implemented in SignerMap yet", self), 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate derive_builder; 2 | 3 | mod signers; 4 | mod transaction; 5 | 6 | #[cfg(feature = "build-binary")] 7 | pub mod client; 8 | 9 | pub mod bundler; 10 | pub mod consts; 11 | pub mod deep_hash; 12 | pub mod deep_hash_sync; 13 | pub mod error; 14 | pub mod index; 15 | pub mod tags; 16 | pub mod token; 17 | pub mod upload; 18 | pub mod utils; 19 | pub mod verify; 20 | 21 | pub use bundler::{BundlerClient, BundlerClientBuilder}; 22 | pub use signers::Signer; 23 | pub use transaction::irys::BundlerTx; 24 | pub use verify::Verifier; 25 | 26 | #[cfg(feature = "arweave")] 27 | pub use signers::arweave::ArweaveSigner; 28 | 29 | #[cfg(any(feature = "solana", feature = "algorand"))] 30 | pub use signers::ed25519::Ed25519Signer; 31 | 32 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 33 | pub use signers::secp256k1::Secp256k1Signer; 34 | 35 | #[cfg(feature = "cosmos")] 36 | pub use signers::cosmos::CosmosSigner; 37 | 38 | #[cfg(feature = "aptos")] 39 | pub use signers::aptos::AptosSigner; 40 | 41 | #[cfg(feature = "aptos")] 42 | pub use signers::aptos::MultiAptosSigner; 43 | -------------------------------------------------------------------------------- /src/signers/aptos.rs: -------------------------------------------------------------------------------- 1 | use crate::error::BundlerError; 2 | use crate::Signer as SignerTrait; 3 | use crate::Verifier as VerifierTrait; 4 | use crate::{index::SignerMap, Ed25519Signer}; 5 | 6 | use bytes::Bytes; 7 | use ed25519_dalek::{Keypair, Verifier, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; 8 | use num::Integer; 9 | 10 | pub struct AptosSigner { 11 | signer: Ed25519Signer, 12 | } 13 | 14 | impl AptosSigner { 15 | pub fn new(keypair: Keypair) -> Self { 16 | Self { 17 | signer: Ed25519Signer::new(keypair), 18 | } 19 | } 20 | 21 | pub fn from_base58(s: &str) -> Result { 22 | Ok(Self { 23 | signer: Ed25519Signer::from_base58(s)?, 24 | }) 25 | } 26 | } 27 | 28 | const SIG_TYPE: SignerMap = SignerMap::InjectedAptos; 29 | const SIG_LENGTH: u16 = SIGNATURE_LENGTH as u16; 30 | const PUB_LENGTH: u16 = PUBLIC_KEY_LENGTH as u16; 31 | 32 | impl SignerTrait for AptosSigner { 33 | fn sign(&self, message: bytes::Bytes) -> Result { 34 | let aptos_message = 35 | Bytes::copy_from_slice(&[b"APTOS\nmessage: ".as_ref(), &message[..]].concat()); 36 | let nonce = Bytes::from(b"\nnonce: bundlr".to_vec()); 37 | let full_msg = Bytes::from([aptos_message, nonce].concat()); 38 | self.signer.sign(full_msg) 39 | } 40 | 41 | fn pub_key(&self) -> bytes::Bytes { 42 | self.signer.pub_key() 43 | } 44 | 45 | fn sig_type(&self) -> SignerMap { 46 | SIG_TYPE 47 | } 48 | fn get_sig_length(&self) -> u16 { 49 | SIG_LENGTH 50 | } 51 | fn get_pub_length(&self) -> u16 { 52 | PUB_LENGTH 53 | } 54 | } 55 | 56 | impl VerifierTrait for AptosSigner { 57 | fn verify( 58 | pk: Bytes, 59 | message: Bytes, 60 | signature: Bytes, 61 | ) -> Result<(), crate::error::BundlerError> { 62 | let public_key = 63 | ed25519_dalek::PublicKey::from_bytes(&pk).map_err(BundlerError::ED25519Error)?; 64 | let sig = 65 | ed25519_dalek::Signature::from_bytes(&signature).map_err(BundlerError::ED25519Error)?; 66 | let aptos_message = 67 | Bytes::copy_from_slice(&[b"APTOS\nmessage: ".as_ref(), &message[..]].concat()); 68 | let nonce = Bytes::from(b"\nnonce: bundlr".to_vec()); 69 | let full_msg = Bytes::from([aptos_message, nonce].concat()); 70 | 71 | public_key 72 | .verify(&full_msg, &sig) 73 | .map_err(|_err| BundlerError::InvalidSignature) 74 | } 75 | } 76 | 77 | const SIG_TYPE_M: SignerMap = SignerMap::MultiAptos; 78 | const SIG_LENGTH_M: u16 = (SIGNATURE_LENGTH * 32 + 4) as u16; // max 32 64 byte signatures, +4 for 32-bit bitmap 79 | const PUB_LENGTH_M: u16 = (PUBLIC_KEY_LENGTH * 32 + 1) as u16; // max 64 32 byte keys, +1 for 8-bit threshold value 80 | 81 | pub struct MultiAptosSigner { 82 | signer: Ed25519Signer, 83 | } 84 | 85 | impl MultiAptosSigner { 86 | pub fn collect_signatures( 87 | &self, 88 | _eamessage: bytes::Bytes, 89 | ) -> Result<(Vec, Vec), crate::error::BundlerError> { 90 | //TODO: implement 91 | todo!() 92 | } 93 | } 94 | 95 | impl MultiAptosSigner { 96 | pub fn new(keypair: Keypair) -> Self { 97 | Self { 98 | signer: Ed25519Signer::new(keypair), 99 | } 100 | } 101 | 102 | pub fn from_base58(s: &str) -> Result { 103 | Ok(Self { 104 | signer: Ed25519Signer::from_base58(s)?, 105 | }) 106 | } 107 | } 108 | 109 | impl SignerTrait for MultiAptosSigner { 110 | fn sign(&self, message: bytes::Bytes) -> Result { 111 | //TODO: implement 112 | let (_signatures, _bitmap) = self.collect_signatures(message)?; 113 | todo!() 114 | } 115 | 116 | fn pub_key(&self) -> bytes::Bytes { 117 | self.signer.pub_key() 118 | } 119 | 120 | fn sig_type(&self) -> SignerMap { 121 | SIG_TYPE_M 122 | } 123 | fn get_sig_length(&self) -> u16 { 124 | SIG_LENGTH_M 125 | } 126 | fn get_pub_length(&self) -> u16 { 127 | PUB_LENGTH_M 128 | } 129 | } 130 | 131 | impl VerifierTrait for MultiAptosSigner { 132 | fn verify( 133 | pk: Bytes, 134 | message: Bytes, 135 | signature: Bytes, 136 | ) -> Result<(), crate::error::BundlerError> { 137 | let sig_len = SIG_LENGTH_M; 138 | let bitmap_pos = sig_len - 4; 139 | let signatures = signature.slice(0..(bitmap_pos as usize)); 140 | let encode_bitmap = signature.slice((bitmap_pos as usize)..signature.len()); 141 | 142 | let mut one_false = false; 143 | for i in 0..32 { 144 | let bucket = i.div_floor(&8); 145 | let bucket_pos = i - bucket * 8; 146 | let sig_included = (encode_bitmap[bucket] & (128 >> bucket_pos)) != 0; 147 | 148 | if sig_included { 149 | let signature = signatures.slice((i * 64)..((i + 1) * 64)); 150 | let pub_key_slc = pk.slice((i * 32)..((i + 1) * 32)); 151 | let public_key = ed25519_dalek::PublicKey::from_bytes(&pub_key_slc) 152 | .map_err(BundlerError::ED25519Error)?; 153 | let sig = ed25519_dalek::Signature::from_bytes(&signature) 154 | .map_err(BundlerError::ED25519Error)?; 155 | match public_key.verify(&message, &sig) { 156 | Ok(()) => (), 157 | Err(_err) => one_false = false, 158 | } 159 | } 160 | } 161 | 162 | if one_false { 163 | Err(BundlerError::InvalidSignature) 164 | } else { 165 | Ok(()) 166 | } 167 | } 168 | } 169 | 170 | #[cfg(test)] 171 | mod tests { 172 | use crate::{AptosSigner, Signer, Verifier}; 173 | use bytes::Bytes; 174 | use ed25519_dalek::Keypair; 175 | 176 | #[test] 177 | fn should_sign_and_verify() { 178 | let msg = Bytes::from(b"Message".to_vec()); 179 | 180 | let base58_secret_key = "kNykCXNxgePDjFbDWjPNvXQRa8U12Ywc19dFVaQ7tebUj3m7H4sF4KKdJwM7yxxb3rqxchdjezX9Szh8bLcQAjb"; 181 | let signer = AptosSigner::from_base58(base58_secret_key).unwrap(); 182 | let sig = signer.sign(msg.clone()).unwrap(); 183 | let pub_key = signer.pub_key(); 184 | println!("{:?}", pub_key.to_vec()); 185 | assert!(AptosSigner::verify(pub_key, msg.clone(), sig).is_ok()); 186 | 187 | let keypair = Keypair::from_bytes(&[ 188 | 237, 158, 92, 107, 132, 192, 1, 57, 8, 20, 213, 108, 29, 227, 37, 8, 3, 105, 196, 244, 189 | 8, 221, 184, 199, 62, 253, 98, 131, 33, 165, 165, 215, 14, 7, 46, 23, 221, 242, 240, 190 | 226, 94, 79, 161, 31, 192, 163, 13, 25, 106, 53, 34, 215, 83, 124, 162, 156, 8, 97, 191 | 194, 180, 213, 179, 33, 68, 192 | ]) 193 | .unwrap(); 194 | let signer = AptosSigner::new(keypair); 195 | let sig = signer.sign(msg.clone()).unwrap(); 196 | let pub_key = signer.pub_key(); 197 | 198 | assert!(AptosSigner::verify(pub_key, msg, sig).is_ok()); 199 | } 200 | 201 | #[test] 202 | fn should_sign_and_verify_multisig() { 203 | //TODO: implement 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/signers/arweave.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::{error::BundlerError, index::SignerMap, Verifier}; 4 | use arweave_rs::ArweaveSigner as SdkSigner; 5 | use bytes::Bytes; 6 | 7 | use super::Signer; 8 | 9 | pub struct ArweaveSigner { 10 | sdk: SdkSigner, 11 | } 12 | 13 | #[allow(unused)] 14 | impl ArweaveSigner { 15 | pub fn from_keypair_path(keypair_path: PathBuf) -> Result { 16 | let sdk = 17 | SdkSigner::from_keypair_path(keypair_path).map_err(BundlerError::ArweaveSdkError)?; 18 | let pub_key = sdk.get_public_key().0; 19 | if pub_key.len() as u16 == PUB_LENGTH { 20 | Ok(Self { sdk }) 21 | } else { 22 | Err(BundlerError::InvalidKey(format!( 23 | "Public key length should be of {}", 24 | PUB_LENGTH 25 | ))) 26 | } 27 | } 28 | } 29 | 30 | const SIG_TYPE: SignerMap = SignerMap::Arweave; 31 | const SIG_LENGTH: u16 = 512; 32 | const PUB_LENGTH: u16 = 512; 33 | 34 | impl Signer for ArweaveSigner { 35 | fn sign(&self, message: Bytes) -> Result { 36 | Ok(Bytes::copy_from_slice(&self.sdk.sign(&message)?.0)) 37 | } 38 | 39 | fn pub_key(&self) -> Bytes { 40 | Bytes::copy_from_slice(&self.sdk.get_public_key().0) 41 | } 42 | 43 | fn sig_type(&self) -> SignerMap { 44 | SIG_TYPE 45 | } 46 | fn get_sig_length(&self) -> u16 { 47 | SIG_LENGTH 48 | } 49 | fn get_pub_length(&self) -> u16 { 50 | PUB_LENGTH 51 | } 52 | } 53 | 54 | impl Verifier for ArweaveSigner { 55 | fn verify(pk: Bytes, message: Bytes, signature: Bytes) -> Result<(), BundlerError> { 56 | SdkSigner::verify(&pk, &message, &signature).map_err(|err| match err { 57 | arweave_rs::error::Error::InvalidSignature => BundlerError::InvalidSignature, 58 | _ => BundlerError::ArweaveSdkError(err), 59 | }) 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use std::{path::PathBuf, str::FromStr}; 66 | 67 | use crate::{ 68 | deep_hash::DeepHashChunk, deep_hash_sync::deep_hash_sync, ArweaveSigner, Signer, Verifier, 69 | }; 70 | use bytes::Bytes; 71 | use data_encoding::BASE64URL_NOPAD; 72 | use serde::{Deserialize, Serialize}; 73 | 74 | //TODO: remove this when receipt included 75 | #[derive(Serialize, Deserialize)] 76 | #[serde(rename_all = "camelCase")] 77 | pub struct Receipt { 78 | pub id: String, 79 | pub timestamp: u64, 80 | pub version: String, 81 | pub public: String, 82 | pub signature: String, 83 | pub deadline_height: u64, 84 | pub block: u64, 85 | pub validator_signatures: Vec, 86 | } 87 | 88 | #[test] 89 | fn should_sign_and_verify() { 90 | let msg = Bytes::copy_from_slice(b"Hello, Irys!"); 91 | let path = PathBuf::from_str("res/test_wallet.json").unwrap(); 92 | let signer = ArweaveSigner::from_keypair_path(path).unwrap(); 93 | 94 | let sig = signer.sign(msg.clone()).unwrap(); 95 | let pub_key = signer.pub_key(); 96 | 97 | println!("{:?}", sig.to_vec()); 98 | println!("{:?}", pub_key.to_vec()); 99 | 100 | assert!(ArweaveSigner::verify(pub_key, msg, sig).is_ok()); 101 | } 102 | 103 | #[test] 104 | fn should_verify_receipt() { 105 | let data = std::fs::read_to_string("res/test_receipt.json").expect("Unable to read file"); 106 | let receipt = serde_json::from_str::(&data).expect("Unable to parse json file"); 107 | 108 | let fields = DeepHashChunk::Chunks(vec![ 109 | DeepHashChunk::Chunk("Bundlr".into()), 110 | DeepHashChunk::Chunk(receipt.version.into()), 111 | DeepHashChunk::Chunk(receipt.id.into()), 112 | DeepHashChunk::Chunk(receipt.deadline_height.to_string().into()), 113 | DeepHashChunk::Chunk(receipt.timestamp.to_string().into()), 114 | ]); 115 | 116 | let pubk = BASE64URL_NOPAD 117 | .decode(&receipt.public.into_bytes()) 118 | .unwrap(); 119 | let msg = deep_hash_sync(fields).unwrap(); 120 | let sig = BASE64URL_NOPAD 121 | .decode(&receipt.signature.into_bytes()) 122 | .unwrap(); 123 | 124 | assert!(ArweaveSigner::verify(pubk.into(), msg, sig.into()).is_ok()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/signers/cosmos.rs: -------------------------------------------------------------------------------- 1 | use std::array::TryFromSliceError; 2 | 3 | use crate::{error::BundlerError, index::SignerMap, Signer, Verifier}; 4 | use bytes::Bytes; 5 | use secp256k1::{ 6 | constants::{COMPACT_SIGNATURE_SIZE, PUBLIC_KEY_SIZE}, 7 | Message, PublicKey, Secp256k1, SecretKey, 8 | }; 9 | use sha2::Digest; 10 | 11 | pub struct CosmosSigner { 12 | sec_key: SecretKey, 13 | pub_key: PublicKey, 14 | } 15 | 16 | impl CosmosSigner { 17 | pub fn new(sec_key: SecretKey) -> Result { 18 | let secp = Secp256k1::new(); 19 | let pub_key = PublicKey::from_secret_key(&secp, &sec_key); 20 | if pub_key.serialize().len() == PUBLIC_KEY_SIZE { 21 | Ok(Self { sec_key, pub_key }) 22 | } else { 23 | Err(BundlerError::InvalidKey(format!( 24 | "Public key length should be of {}", 25 | PUB_LENGTH 26 | ))) 27 | } 28 | } 29 | 30 | pub fn from_base58(s: &str) -> Result { 31 | let k = bs58::decode(s) 32 | .into_vec() 33 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 34 | let key: &[u8; 64] = k 35 | .as_slice() 36 | .try_into() 37 | .map_err(|err: TryFromSliceError| BundlerError::ParseError(err.to_string()))?; 38 | 39 | let sec_key = SecretKey::from_slice(&key[..32]) 40 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 41 | 42 | Self::new(sec_key) 43 | } 44 | 45 | pub fn sha256_digest(msg: &[u8]) -> [u8; 32] { 46 | let mut hasher = sha2::Sha256::new(); 47 | hasher.update(msg); 48 | let result = hasher.finalize(); 49 | result.into() 50 | } 51 | } 52 | 53 | const SIG_TYPE: SignerMap = SignerMap::Cosmos; 54 | const SIG_LENGTH: u16 = COMPACT_SIGNATURE_SIZE as u16; 55 | const PUB_LENGTH: u16 = PUBLIC_KEY_SIZE as u16; 56 | 57 | impl Signer for CosmosSigner { 58 | fn pub_key(&self) -> bytes::Bytes { 59 | let pub_key = &self.pub_key.serialize(); 60 | assert!(pub_key.len() == PUBLIC_KEY_SIZE); 61 | Bytes::copy_from_slice(pub_key) 62 | } 63 | 64 | fn sign(&self, message: bytes::Bytes) -> Result { 65 | let msg = Message::from_slice(&CosmosSigner::sha256_digest(&message[..])) 66 | .map_err(BundlerError::Secp256k1Error)?; 67 | let signature = secp256k1::Secp256k1::signing_only() 68 | .sign_ecdsa(&msg, &self.sec_key) 69 | .serialize_compact(); 70 | 71 | Ok(Bytes::copy_from_slice(&signature)) 72 | } 73 | 74 | fn sig_type(&self) -> SignerMap { 75 | SIG_TYPE 76 | } 77 | fn get_sig_length(&self) -> u16 { 78 | SIG_LENGTH 79 | } 80 | fn get_pub_length(&self) -> u16 { 81 | PUB_LENGTH 82 | } 83 | } 84 | 85 | impl Verifier for CosmosSigner { 86 | fn verify( 87 | public_key: Bytes, 88 | message: Bytes, 89 | signature: Bytes, 90 | ) -> Result<(), crate::error::BundlerError> { 91 | let msg = secp256k1::Message::from_slice(&CosmosSigner::sha256_digest(&message)) 92 | .map_err(BundlerError::Secp256k1Error)?; 93 | let sig = secp256k1::ecdsa::Signature::from_compact(&signature) 94 | .map_err(BundlerError::Secp256k1Error)?; 95 | let pk = 96 | secp256k1::PublicKey::from_slice(&public_key).map_err(BundlerError::Secp256k1Error)?; 97 | 98 | secp256k1::Secp256k1::verification_only() 99 | .verify_ecdsa(&msg, &sig, &pk) 100 | .map_err(|_| BundlerError::InvalidSignature) 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use bytes::Bytes; 107 | use secp256k1::SecretKey; 108 | 109 | use crate::{CosmosSigner, Signer, Verifier}; 110 | 111 | #[test] 112 | fn should_hash_message_correctly() { 113 | let expected: [u8; 32] = [ 114 | 242, 32, 241, 161, 45, 243, 110, 65, 225, 215, 2, 87, 174, 67, 33, 202, 65, 130, 179, 115 | 30, 170, 249, 140, 26, 152, 240, 228, 194, 31, 206, 193, 109, 116 | ]; 117 | let hashed_message = CosmosSigner::sha256_digest(b"Hello, Bundlr!"); 118 | assert_eq!(expected, hashed_message); 119 | } 120 | 121 | #[test] 122 | fn should_sign_and_verify() { 123 | let msg = Bytes::from("Hello, Bundlr!"); 124 | 125 | let secret_key = SecretKey::from_slice(b"00000000000000000000000000000000").unwrap(); 126 | let signer = CosmosSigner::new(secret_key).unwrap(); 127 | let sig = signer.sign(msg.clone()).unwrap(); 128 | let pub_key = signer.pub_key(); 129 | assert!(CosmosSigner::verify(pub_key, msg.clone(), sig).is_ok()); 130 | 131 | let base58_secret_key = "28PmkjeZqLyfRQogb3FU4E1vJh68dXpbojvS2tcPwezZmVQp8zs8ebGmYg1hNRcjX4DkUALf3SkZtytGWPG3vYhs"; 132 | let signer = CosmosSigner::from_base58(base58_secret_key).unwrap(); 133 | let sig = signer.sign(msg.clone()).unwrap(); 134 | let pub_key = signer.pub_key(); 135 | assert!(CosmosSigner::verify(pub_key, msg, sig).is_ok()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/signers/ed25519.rs: -------------------------------------------------------------------------------- 1 | use std::array::TryFromSliceError; 2 | 3 | use crate::error::BundlerError; 4 | use crate::index::SignerMap; 5 | use crate::Signer as SignerTrait; 6 | use crate::Verifier as VerifierTrait; 7 | 8 | use bytes::Bytes; 9 | use ed25519_dalek::{Keypair, Signer, Verifier, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; 10 | 11 | pub struct Ed25519Signer { 12 | keypair: Keypair, 13 | } 14 | 15 | //TODO: add validation for secret keys 16 | impl Ed25519Signer { 17 | pub fn new(keypair: Keypair) -> Ed25519Signer { 18 | Ed25519Signer { keypair } 19 | } 20 | 21 | pub fn from_base58(s: &str) -> Result { 22 | let k = bs58::decode(s) 23 | .into_vec() 24 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 25 | let key: &[u8; 64] = k 26 | .as_slice() 27 | .try_into() 28 | .map_err(|err: TryFromSliceError| BundlerError::ParseError(err.to_string()))?; 29 | 30 | Ok(Self { 31 | keypair: Keypair::from_bytes(key).map_err(BundlerError::ED25519Error)?, 32 | }) 33 | } 34 | } 35 | 36 | const SIG_TYPE: SignerMap = SignerMap::ED25519; 37 | const SIG_LENGTH: u16 = SIGNATURE_LENGTH as u16; 38 | const PUB_LENGTH: u16 = PUBLIC_KEY_LENGTH as u16; 39 | 40 | impl SignerTrait for Ed25519Signer { 41 | fn sign(&self, message: bytes::Bytes) -> Result { 42 | Ok(Bytes::copy_from_slice( 43 | &self.keypair.sign(&message).to_bytes(), 44 | )) 45 | } 46 | 47 | fn pub_key(&self) -> bytes::Bytes { 48 | Bytes::copy_from_slice(&self.keypair.public.to_bytes()) 49 | } 50 | 51 | fn sig_type(&self) -> SignerMap { 52 | SIG_TYPE 53 | } 54 | fn get_sig_length(&self) -> u16 { 55 | SIG_LENGTH 56 | } 57 | fn get_pub_length(&self) -> u16 { 58 | PUB_LENGTH 59 | } 60 | } 61 | 62 | impl VerifierTrait for Ed25519Signer { 63 | fn verify( 64 | pk: Bytes, 65 | message: Bytes, 66 | signature: Bytes, 67 | ) -> Result<(), crate::error::BundlerError> { 68 | let public_key = 69 | ed25519_dalek::PublicKey::from_bytes(&pk).map_err(BundlerError::ED25519Error)?; 70 | let sig = 71 | ed25519_dalek::Signature::from_bytes(&signature).map_err(BundlerError::ED25519Error)?; 72 | public_key 73 | .verify(&message, &sig) 74 | .map_err(|_| BundlerError::InvalidSignature) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use crate::{Ed25519Signer, Signer, Verifier}; 81 | use bytes::Bytes; 82 | use ed25519_dalek::Keypair; 83 | 84 | #[test] 85 | fn should_sign_and_verify() { 86 | let msg = Bytes::from(b"Message".to_vec()); 87 | 88 | let base58_secret_key = "kNykCXNxgePDjFbDWjPNvXQRa8U12Ywc19dFVaQ7tebUj3m7H4sF4KKdJwM7yxxb3rqxchdjezX9Szh8bLcQAjb"; 89 | let signer = Ed25519Signer::from_base58(base58_secret_key).unwrap(); 90 | let sig = signer.sign(msg.clone()).unwrap(); 91 | let pub_key = signer.pub_key(); 92 | println!("{:?}", pub_key.to_vec()); 93 | assert!(Ed25519Signer::verify(pub_key, msg.clone(), sig).is_ok()); 94 | 95 | let keypair = Keypair::from_bytes(&[ 96 | 237, 158, 92, 107, 132, 192, 1, 57, 8, 20, 213, 108, 29, 227, 37, 8, 3, 105, 196, 244, 97 | 8, 221, 184, 199, 62, 253, 98, 131, 33, 165, 165, 215, 14, 7, 46, 23, 221, 242, 240, 98 | 226, 94, 79, 161, 31, 192, 163, 13, 25, 106, 53, 34, 215, 83, 124, 162, 156, 8, 97, 99 | 194, 180, 213, 179, 33, 68, 100 | ]) 101 | .unwrap(); 102 | let signer = Ed25519Signer::new(keypair); 103 | let sig = signer.sign(msg.clone()).unwrap(); 104 | let pub_key = signer.pub_key(); 105 | 106 | assert!(Ed25519Signer::verify(pub_key, msg, sig).is_ok()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/signers/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::BundlerError, index::SignerMap}; 2 | use bytes::Bytes; 3 | 4 | #[cfg(feature = "aptos")] 5 | pub mod aptos; 6 | #[cfg(feature = "arweave")] 7 | pub mod arweave; 8 | #[cfg(feature = "cosmos")] 9 | pub mod cosmos; 10 | #[cfg(any(feature = "solana", feature = "algorand", feature = "aptos"))] 11 | pub mod ed25519; 12 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 13 | pub mod secp256k1; 14 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 15 | pub mod typed_ethereum; 16 | 17 | pub trait ToPem {} 18 | 19 | pub trait Signer: Send + Sync { 20 | fn sign(&self, message: Bytes) -> Result; 21 | fn sig_type(&self) -> SignerMap; 22 | fn get_sig_length(&self) -> u16; 23 | fn get_pub_length(&self) -> u16; 24 | fn pub_key(&self) -> Bytes; 25 | } 26 | -------------------------------------------------------------------------------- /src/signers/secp256k1.rs: -------------------------------------------------------------------------------- 1 | use std::array::TryFromSliceError; 2 | 3 | use crate::{error::BundlerError, index::SignerMap, Signer, Verifier}; 4 | use bytes::Bytes; 5 | use secp256k1::{ 6 | constants::{COMPACT_SIGNATURE_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE}, 7 | Message, PublicKey, Secp256k1, SecretKey, 8 | }; 9 | use web3::{ 10 | signing::{keccak256, recover}, 11 | types::{Address, H256}, 12 | }; 13 | 14 | pub struct Secp256k1Signer { 15 | sec_key: SecretKey, 16 | pub_key: PublicKey, 17 | } 18 | 19 | impl Secp256k1Signer { 20 | pub fn new(sec_key: SecretKey) -> Secp256k1Signer { 21 | let secp = Secp256k1::new(); 22 | let pub_key = PublicKey::from_secret_key(&secp, &sec_key); 23 | Secp256k1Signer { sec_key, pub_key } 24 | } 25 | 26 | pub fn from_base58(s: &str) -> Result { 27 | let k = bs58::decode(s) 28 | .into_vec() 29 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 30 | let key: &[u8; 64] = k 31 | .as_slice() 32 | .try_into() 33 | .map_err(|err: TryFromSliceError| BundlerError::ParseError(err.to_string()))?; 34 | 35 | let sec_key = SecretKey::from_slice(&key[..32]) 36 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 37 | 38 | Ok(Self::new(sec_key)) 39 | } 40 | 41 | pub fn eth_hash_message(msg: &[u8]) -> [u8; 32] { 42 | let data = &[ 43 | b"\x19Ethereum Signed Message:\n", 44 | msg.len().to_string().as_bytes(), 45 | msg, 46 | ] 47 | .concat(); 48 | keccak256(data) 49 | } 50 | } 51 | 52 | const SIG_TYPE: SignerMap = SignerMap::Ethereum; 53 | const SIG_LENGTH: u16 = (COMPACT_SIGNATURE_SIZE + 1) as u16; 54 | const PUB_LENGTH: u16 = UNCOMPRESSED_PUBLIC_KEY_SIZE as u16; 55 | 56 | impl Signer for Secp256k1Signer { 57 | fn pub_key(&self) -> bytes::Bytes { 58 | Bytes::copy_from_slice(&self.pub_key.serialize_uncompressed()) 59 | } 60 | 61 | fn sign(&self, message: bytes::Bytes) -> Result { 62 | let msg = Message::from_slice(&Secp256k1Signer::eth_hash_message(&message[..])) 63 | .map_err(BundlerError::Secp256k1Error)?; 64 | let (recovery_id, signature) = secp256k1::Secp256k1::signing_only() 65 | .sign_ecdsa_recoverable(&msg, &self.sec_key) 66 | .serialize_compact(); 67 | 68 | let standard_v = recovery_id.to_i32() as u8; 69 | let r = H256::from_slice(&signature[..32]); 70 | let s = H256::from_slice(&signature[32..]); 71 | let v: u8 = standard_v + 27; 72 | let data = &[r.as_bytes(), s.as_bytes(), &[v]].concat(); 73 | 74 | Ok(Bytes::copy_from_slice(data)) 75 | } 76 | 77 | fn sig_type(&self) -> SignerMap { 78 | SIG_TYPE 79 | } 80 | fn get_sig_length(&self) -> u16 { 81 | SIG_LENGTH 82 | } 83 | fn get_pub_length(&self) -> u16 { 84 | PUB_LENGTH 85 | } 86 | } 87 | 88 | impl Verifier for Secp256k1Signer { 89 | fn verify( 90 | public_key: Bytes, 91 | message: Bytes, 92 | signature: Bytes, 93 | ) -> Result<(), crate::error::BundlerError> { 94 | let msg = Secp256k1Signer::eth_hash_message(&message); 95 | 96 | let recovery_address = recover(&msg, &signature[0..64], signature[64] as i32 - 27) 97 | .map_err(BundlerError::RecoveryError)?; 98 | 99 | let pubkey = PublicKey::from_slice(&public_key) 100 | .map_err(BundlerError::Secp256k1Error)? 101 | .serialize_uncompressed(); 102 | assert_eq!(pubkey[0], 0x04); 103 | let pubkey_hash = keccak256(&public_key[1..]); 104 | let address = Address::from_slice(&pubkey_hash[12..]); 105 | 106 | if address.eq(&recovery_address) { 107 | return Ok(()); 108 | } 109 | 110 | Err(BundlerError::InvalidSignature) 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use bytes::Bytes; 117 | use secp256k1::SecretKey; 118 | 119 | use crate::{Secp256k1Signer, Signer, Verifier}; 120 | 121 | #[test] 122 | fn should_hash_message_correctly() { 123 | let expected: [u8; 32] = [ 124 | 115, 94, 155, 26, 251, 67, 239, 226, 251, 85, 181, 193, 50, 136, 70, 88, 238, 217, 84, 125 | 244, 92, 5, 82, 24, 227, 189, 141, 69, 122, 231, 149, 229, 126 | ]; 127 | let hashed_message = Secp256k1Signer::eth_hash_message(b"Hello, Bundlr!"); 128 | assert_eq!(expected, hashed_message); 129 | } 130 | 131 | #[test] 132 | fn should_sign_and_verify() { 133 | let msg = Bytes::from("Hello, Bundlr!"); 134 | 135 | let secret_key = SecretKey::from_slice(b"00000000000000000000000000000000").unwrap(); 136 | let signer = Secp256k1Signer::new(secret_key); 137 | let sig = signer.sign(msg.clone()).unwrap(); 138 | let pub_key = signer.pub_key(); 139 | assert!(Secp256k1Signer::verify(pub_key, msg.clone(), sig).is_ok()); 140 | 141 | let base58_secret_key = "28PmkjeZqLyfRQogb3FU4E1vJh68dXpbojvS2tcPwezZmVQp8zs8ebGmYg1hNRcjX4DkUALf3SkZtytGWPG3vYhs"; 142 | let signer = Secp256k1Signer::from_base58(base58_secret_key).unwrap(); 143 | let sig = signer.sign(msg.clone()).unwrap(); 144 | let pub_key = signer.pub_key(); 145 | assert!(Secp256k1Signer::verify(pub_key, msg, sig).is_ok()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/signers/typed_ethereum.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::BundlerError, 3 | index::SignerMap, 4 | utils::{hash_structured_data, EIP712}, 5 | Signer, Verifier, 6 | }; 7 | use bytes::Bytes; 8 | use secp256k1::constants::COMPACT_SIGNATURE_SIZE; 9 | use serde_json::{from_str, json}; 10 | use web3::signing::recover; 11 | 12 | pub struct TypedEthereumSigner { 13 | //signer: Secp256k1Signer, 14 | //address: Vec, 15 | } 16 | 17 | const SIG_TYPE: SignerMap = SignerMap::Ethereum; 18 | const SIG_LENGTH: u16 = (COMPACT_SIGNATURE_SIZE + 1) as u16; 19 | const PUB_LENGTH: u16 = 42; 20 | 21 | impl Signer for TypedEthereumSigner { 22 | fn pub_key(&self) -> bytes::Bytes { 23 | todo!(); 24 | } 25 | 26 | fn sign(&self, _message: bytes::Bytes) -> Result { 27 | todo!(); 28 | } 29 | 30 | fn sig_type(&self) -> SignerMap { 31 | SIG_TYPE 32 | } 33 | fn get_sig_length(&self) -> u16 { 34 | SIG_LENGTH 35 | } 36 | fn get_pub_length(&self) -> u16 { 37 | PUB_LENGTH 38 | } 39 | } 40 | 41 | impl Verifier for TypedEthereumSigner { 42 | fn verify( 43 | public_key: Bytes, 44 | message: Bytes, 45 | signature: Bytes, 46 | ) -> Result<(), crate::error::BundlerError> { 47 | let address = String::from_utf8(public_key.to_vec()).map_err(|err| { 48 | BundlerError::ParseError(format!( 49 | "Error parsing address from bytes to string: {}", 50 | err 51 | )) 52 | })?; 53 | 54 | let mut hex_message: String = "0x".to_owned(); 55 | for i in 0..message.len() { 56 | let byte = message[i]; 57 | hex_message += &format!("{:02X}", byte); 58 | } 59 | 60 | let json = json!({ 61 | "primaryType": "Bundlr", 62 | "domain": { 63 | "name": "Bundlr", 64 | "version": "1" 65 | }, 66 | "types": { 67 | "EIP712Domain": [ 68 | { "name": "name", "type": "string" }, 69 | { "name": "version", "type": "string" } 70 | ], 71 | "Bundlr": [ 72 | { "name": "Transaction hash", "type": "bytes" }, 73 | { "name": "address", "type": "address" } 74 | ] 75 | }, 76 | "message": { 77 | "address": address, 78 | "Transaction hash": hex_message 79 | } 80 | }); 81 | 82 | let typed_data = from_str::(&json.to_string()).map_err(|err| { 83 | BundlerError::ParseError(format!("Error parsing EIP712 json object: {}", err)) 84 | })?; 85 | let data = hash_structured_data(typed_data).map_err(BundlerError::Eip712Error)?; 86 | let recovered_address = recover(&data, &signature[0..64], signature[64] as i32 - 27) 87 | .map_err(BundlerError::RecoveryError)?; 88 | 89 | // Somehow, recovered_address.to_string() returns 0x0000..0000 instead of full address ¬¬ 90 | let recovered_address = format!("{:?}", recovered_address); 91 | if recovered_address == address { 92 | Ok(()) 93 | } else { 94 | Err(BundlerError::InvalidSignature) 95 | } 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | //TODO: implement sign and tests 102 | } 103 | -------------------------------------------------------------------------------- /src/tags.rs: -------------------------------------------------------------------------------- 1 | use avro_rs::{from_avro_datum, to_avro_datum, Schema}; 2 | use bytes::Bytes; 3 | use lazy_static::lazy_static; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::error::BundlerError; 7 | 8 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 9 | pub struct Tag { 10 | pub name: String, 11 | pub value: String, 12 | } 13 | 14 | impl Tag { 15 | pub fn new(name: &str, value: &str) -> Self { 16 | Tag { 17 | name: name.to_string(), 18 | value: value.to_string(), 19 | } 20 | } 21 | } 22 | 23 | const SCHEMA_STR: &str = r#"{ 24 | "type": "array", 25 | "items": { 26 | "type": "record", 27 | "name": "Tag", 28 | "fields": [ 29 | { "name": "name", "type": "string" }, 30 | { "name": "value", "type": "string" } 31 | ] 32 | } 33 | }"#; 34 | 35 | lazy_static! { 36 | pub static ref TAGS_SCHEMA: Schema = Schema::parse_str(SCHEMA_STR).unwrap(); 37 | } 38 | 39 | // const TAGS_READER: Reader<'static, Vec> = Reader::with_schema(&TAGS_SCHEMA, Vec::::new()); 40 | // const TAGS_WRITER: Writer<'static, Vec> = Writer::new(&TAGS_SCHEMA, Vec::new()); 41 | 42 | pub trait AvroEncode { 43 | fn encode(&self) -> Result; 44 | } 45 | 46 | pub trait AvroDecode { 47 | fn decode(&mut self) -> Result, BundlerError>; 48 | } 49 | 50 | impl AvroEncode for Vec { 51 | fn encode(&self) -> Result { 52 | let v = avro_rs::to_value(self)?; 53 | to_avro_datum(&TAGS_SCHEMA, v) 54 | .map(|v| v.into()) 55 | .map_err(|_| BundlerError::NoBytesLeft) 56 | } 57 | } 58 | 59 | impl AvroDecode for &mut [u8] { 60 | fn decode(&mut self) -> Result, BundlerError> { 61 | let x = self.to_vec(); 62 | let v = from_avro_datum(&TAGS_SCHEMA, &mut x.as_slice(), Some(&TAGS_SCHEMA)) 63 | .map_err(|_| BundlerError::InvalidTagEncoding)?; 64 | avro_rs::from_value(&v).map_err(|_| BundlerError::InvalidTagEncoding) 65 | } 66 | } 67 | 68 | impl From for BundlerError { 69 | fn from(_: avro_rs::DeError) -> Self { 70 | BundlerError::InvalidTagEncoding 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | 77 | use crate::tags::{AvroDecode, AvroEncode}; 78 | 79 | use super::Tag; 80 | 81 | #[test] 82 | fn test_bytes() { 83 | let b = &[2u8, 8, 110, 97, 109, 101, 10, 118, 97, 108, 117, 101, 0]; 84 | 85 | let mut sli = &mut b.clone()[..]; 86 | 87 | dbg!((sli).decode()).unwrap(); 88 | } 89 | 90 | #[test] 91 | fn test_tags() { 92 | let tags = vec![Tag { 93 | name: "name".to_string(), 94 | value: "value".to_string(), 95 | }]; 96 | 97 | dbg!(tags.encode().unwrap().to_vec()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/token/arweave.rs: -------------------------------------------------------------------------------- 1 | use arweave_rs::{crypto::base64::Base64, Arweave as ArweaveSdk}; 2 | use bytes::Bytes; 3 | use num::ToPrimitive; 4 | use reqwest::{StatusCode, Url}; 5 | use std::{ops::Mul, path::PathBuf, str::FromStr}; 6 | 7 | use crate::{ 8 | error::{BuilderError, BundlerError}, 9 | transaction::{Tx, TxStatus}, 10 | ArweaveSigner, Signer, Verifier, 11 | }; 12 | 13 | use super::{Token, TokenType, TxResponse}; 14 | 15 | const ARWEAVE_TICKER: &str = "AR"; 16 | const ARWEAVE_BASE_UNIT: &str = "winston"; 17 | const ARWEAVE_BASE_URL: &str = "https://arweave.net/"; 18 | 19 | #[allow(unused)] 20 | pub struct Arweave { 21 | sdk: ArweaveSdk, 22 | signer: Option, 23 | is_slow: bool, 24 | needs_fee: bool, 25 | base: (String, i64), 26 | name: TokenType, 27 | ticker: String, 28 | min_confirm: i16, 29 | client: reqwest::Client, 30 | } 31 | 32 | #[derive(Default)] 33 | pub struct ArweaveBuilder { 34 | base_url: Option, 35 | keypair_path: Option, 36 | } 37 | 38 | impl ArweaveBuilder { 39 | pub fn new() -> ArweaveBuilder { 40 | Default::default() 41 | } 42 | 43 | pub fn base_url(mut self, base_url: Url) -> ArweaveBuilder { 44 | self.base_url = Some(base_url); 45 | self 46 | } 47 | 48 | pub fn keypair_path(mut self, keypair_path: PathBuf) -> ArweaveBuilder { 49 | self.keypair_path = Some(keypair_path); 50 | self 51 | } 52 | 53 | pub fn build(self) -> Result { 54 | let base_url = self 55 | .base_url 56 | .unwrap_or_else(|| Url::from_str(ARWEAVE_BASE_URL).unwrap()); 57 | 58 | let sdk = match &self.keypair_path { 59 | // With signer 60 | Some(keypair_path) => arweave_rs::ArweaveBuilder::new() 61 | .base_url(base_url) 62 | .keypair_path(keypair_path.clone()) 63 | .build()?, 64 | // Without signer 65 | None => arweave_rs::ArweaveBuilder::new() 66 | .base_url(base_url) 67 | .build()?, 68 | }; 69 | 70 | let signer = match self.keypair_path { 71 | Some(p) => Some(ArweaveSigner::from_keypair_path(p)?), 72 | None => None, 73 | }; 74 | 75 | Ok(Arweave { 76 | sdk, 77 | signer, 78 | is_slow: Default::default(), 79 | needs_fee: true, 80 | base: (ARWEAVE_BASE_UNIT.to_string(), 0), 81 | name: TokenType::Arweave, 82 | ticker: ARWEAVE_TICKER.to_string(), 83 | min_confirm: 5, 84 | client: reqwest::Client::new(), 85 | }) 86 | } 87 | } 88 | 89 | #[async_trait::async_trait] 90 | impl Token for Arweave { 91 | fn get_min_unit_name(&self) -> String { 92 | ARWEAVE_BASE_UNIT.to_string() 93 | } 94 | 95 | fn get_type(&self) -> TokenType { 96 | self.name 97 | } 98 | 99 | fn needs_fee(&self) -> bool { 100 | self.needs_fee 101 | } 102 | 103 | async fn get_tx(&self, tx_id: String) -> Result { 104 | let (status, tx) = self 105 | .sdk 106 | .get_tx( 107 | Base64::from_str(&tx_id) 108 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 109 | ) 110 | .await 111 | .map_err(BundlerError::ArweaveSdkError)?; 112 | 113 | if status == 200 { 114 | match tx { 115 | Some(tx) => Ok(Tx { 116 | id: tx.id.to_string(), 117 | from: tx.owner.to_string(), 118 | to: tx.target.to_string(), 119 | amount: u64::from_str(&tx.quantity.to_string()) 120 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 121 | fee: tx.reward, 122 | block_height: 1, 123 | pending: false, 124 | confirmed: true, 125 | }), 126 | None => Err(BundlerError::TxNotFound), 127 | } 128 | } else { 129 | Err(BundlerError::TxNotFound) 130 | } 131 | } 132 | 133 | async fn get_tx_status( 134 | &self, 135 | tx_id: String, 136 | ) -> Result<(StatusCode, Option), BundlerError> { 137 | let res = self 138 | .sdk 139 | .get_tx_status( 140 | Base64::from_str(&tx_id) 141 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 142 | ) 143 | .await; 144 | 145 | if let Ok((status, tx_status)) = res { 146 | if status == StatusCode::OK { 147 | match tx_status { 148 | Some(tx_status) => Ok(( 149 | status, 150 | Some(TxStatus { 151 | confirmations: tx_status.number_of_confirmations, 152 | height: tx_status.block_height, 153 | block_hash: tx_status.block_indep_hash.to_string(), 154 | }), 155 | )), 156 | None => Ok((status, None)), 157 | } 158 | } else { 159 | //Tx is pending 160 | Ok((status, None)) 161 | } 162 | } else { 163 | Err(BundlerError::TxStatusNotConfirmed) 164 | } 165 | } 166 | 167 | fn sign_message(&self, message: &[u8]) -> Result, BundlerError> { 168 | match &self.signer { 169 | Some(signer) => Ok(signer.sign(Bytes::copy_from_slice(message))?.to_vec()), 170 | None => Err(BundlerError::TokenError( 171 | "No private key present".to_string(), 172 | )), 173 | } 174 | } 175 | 176 | fn verify(&self, pub_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), BundlerError> { 177 | ArweaveSigner::verify( 178 | Bytes::copy_from_slice(pub_key), 179 | Bytes::copy_from_slice(message), 180 | Bytes::copy_from_slice(signature), 181 | ) 182 | .map(|_| ()) 183 | } 184 | 185 | fn get_pub_key(&self) -> Result { 186 | match &self.signer { 187 | Some(signer) => Ok(signer.pub_key()), 188 | None => Err(BundlerError::TokenError( 189 | "No private key present".to_string(), 190 | )), 191 | } 192 | } 193 | 194 | fn wallet_address(&self) -> Result { 195 | if self.signer.is_none() { 196 | return Err(BundlerError::TokenError( 197 | "No private key present".to_string(), 198 | )); 199 | } 200 | Ok(self.sdk.get_wallet_address()?) 201 | } 202 | 203 | fn get_signer(&self) -> Result<&dyn Signer, BundlerError> { 204 | match &self.signer { 205 | Some(signer) => Ok(signer), 206 | None => Err(BundlerError::TokenError( 207 | "No private key present".to_string(), 208 | )), 209 | } 210 | } 211 | 212 | async fn get_id(&self, _item: ()) -> String { 213 | todo!(); 214 | } 215 | 216 | async fn price(&self) -> String { 217 | todo!(); 218 | } 219 | 220 | async fn get_current_height(&self) -> u128 { 221 | todo!(); 222 | } 223 | 224 | async fn get_fee(&self, _amount: u64, to: &str, multiplier: f64) -> Result { 225 | let base64_address = 226 | Base64::from_str(to).map_err(|err| BundlerError::ParseError(err.to_string()))?; 227 | let base_fee = self 228 | .sdk 229 | .get_fee(base64_address, vec![]) 230 | .await 231 | .map_err(BundlerError::ArweaveSdkError)?; 232 | 233 | let fee = match base_fee.to_f64() { 234 | Some(ok) => ok, 235 | None => { 236 | return Err(BundlerError::TypeParseError( 237 | "Could not convert to f64".to_string(), 238 | )) 239 | } 240 | }; 241 | let final_fee = match multiplier.mul(fee).ceil().to_u64() { 242 | Some(fee) => fee, 243 | None => { 244 | return Err(BundlerError::TypeParseError( 245 | "Could not convert fee to u64".to_string(), 246 | )) 247 | } 248 | }; 249 | Ok(final_fee) 250 | } 251 | 252 | async fn create_tx(&self, amount: u64, to: &str, fee: u64) -> Result { 253 | let tx = self 254 | .sdk 255 | .create_transaction( 256 | Base64::from_str(to).map_err(|err| BundlerError::Base64Error(err.to_string()))?, 257 | vec![], 258 | vec![], 259 | amount.into(), 260 | fee, 261 | false, 262 | ) 263 | .await 264 | .map_err(BundlerError::ArweaveSdkError)?; 265 | 266 | Ok(Tx { 267 | id: tx.id.to_string(), 268 | from: tx.owner.to_string(), 269 | to: tx.target.to_string(), 270 | amount: u64::from_str(&tx.quantity.to_string()) 271 | .map_err(|err| BundlerError::Base64Error(err.to_string()))?, 272 | fee: tx.reward, 273 | block_height: Default::default(), 274 | pending: true, 275 | confirmed: false, 276 | }) 277 | } 278 | 279 | async fn send_tx(&self, data: Tx) -> Result { 280 | let tx = self 281 | .sdk 282 | .create_transaction( 283 | Base64::from_str(&data.to) 284 | .map_err(|err| BundlerError::Base64Error(err.to_string()))?, 285 | vec![], 286 | vec![], 287 | data.amount.into(), 288 | data.fee, 289 | false, 290 | ) 291 | .await 292 | .map_err(BundlerError::ArweaveSdkError)?; 293 | 294 | let signed_tx = self 295 | .sdk 296 | .sign_transaction(tx) 297 | .map_err(BundlerError::ArweaveSdkError)?; 298 | let (tx_id, _r) = self 299 | .sdk 300 | .post_transaction(&signed_tx) 301 | .await 302 | .map_err(BundlerError::ArweaveSdkError)?; 303 | 304 | Ok(TxResponse { tx_id }) 305 | } 306 | } 307 | 308 | #[cfg(test)] 309 | mod tests { 310 | use std::{path::PathBuf, str::FromStr}; 311 | 312 | use crate::token::{arweave::ArweaveBuilder, Token}; 313 | 314 | #[test] 315 | fn should_sign_and_verify() { 316 | let msg = [ 317 | 9, 214, 233, 210, 242, 45, 194, 247, 28, 234, 14, 86, 105, 40, 41, 251, 52, 39, 236, 318 | 214, 54, 13, 53, 254, 179, 53, 220, 205, 129, 37, 244, 142, 230, 32, 209, 103, 68, 75, 319 | 39, 178, 10, 186, 24, 160, 179, 143, 211, 151, 320 | ]; 321 | let wallet = PathBuf::from_str("res/test_wallet.json").expect("Could not load path"); 322 | let c = ArweaveBuilder::new() 323 | .keypair_path(wallet) 324 | .build() 325 | .expect("Could not build arweave"); 326 | 327 | let sig = c.sign_message(&msg).unwrap(); 328 | let pub_key = c.get_pub_key().unwrap(); 329 | 330 | assert!(c.verify(&pub_key, &msg, &sig).is_ok()); 331 | } 332 | 333 | #[tokio::test] 334 | async fn should_get_fee_correctly() {} 335 | } 336 | -------------------------------------------------------------------------------- /src/token/ethereum.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use reqwest::{StatusCode, Url}; 3 | 4 | use crate::{ 5 | error::{BuilderError, BundlerError}, 6 | transaction::{Tx, TxStatus}, 7 | Ed25519Signer, Secp256k1Signer, Signer, Verifier, 8 | }; 9 | 10 | use super::{Token, TokenType, TxResponse}; 11 | 12 | const ETHEREUM_TICKER: &str = "ETH"; 13 | const ETHEREUM_BASE_UNIT: &str = "wei"; 14 | const ETHEREUM_BASE_URL: &str = "https://etherscan.io/"; 15 | 16 | #[allow(unused)] 17 | pub struct Ethereum { 18 | signer: Option, 19 | is_slow: bool, 20 | needs_fee: bool, 21 | base: (String, i64), 22 | name: TokenType, 23 | ticker: String, 24 | min_confirm: i16, 25 | client: reqwest::Client, 26 | url: Url, 27 | } 28 | 29 | impl Default for Ethereum { 30 | fn default() -> Self { 31 | let url = Url::parse(ETHEREUM_BASE_URL).unwrap(); 32 | Self { 33 | signer: None, 34 | needs_fee: true, 35 | is_slow: false, 36 | base: (ETHEREUM_BASE_UNIT.to_string(), 0), 37 | name: TokenType::Ethereum, 38 | ticker: ETHEREUM_TICKER.to_string(), 39 | min_confirm: 10, 40 | client: reqwest::Client::new(), 41 | url, 42 | } 43 | } 44 | } 45 | 46 | #[derive(Default)] 47 | pub struct EthereumBuilder { 48 | base_url: Option, 49 | wallet: Option, 50 | } 51 | 52 | impl EthereumBuilder { 53 | pub fn new() -> EthereumBuilder { 54 | Default::default() 55 | } 56 | 57 | pub fn base_url(mut self, base_url: Url) -> EthereumBuilder { 58 | self.base_url = Some(base_url); 59 | self 60 | } 61 | 62 | pub fn wallet(mut self, wallet: &str) -> EthereumBuilder { 63 | self.wallet = Some(wallet.into()); 64 | self 65 | } 66 | 67 | pub fn build(self) -> Result { 68 | let signer = if let Some(wallet) = self.wallet { 69 | Some(Secp256k1Signer::from_base58(&wallet)?) 70 | } else { 71 | None 72 | }; 73 | Ok(Ethereum { 74 | url: self 75 | .base_url 76 | .unwrap_or_else(|| Url::parse(ETHEREUM_BASE_URL).unwrap()), 77 | signer, 78 | ..Ethereum::default() 79 | }) 80 | } 81 | } 82 | 83 | #[allow(unused)] 84 | #[async_trait::async_trait] 85 | impl Token for Ethereum { 86 | fn get_min_unit_name(&self) -> String { 87 | ETHEREUM_BASE_UNIT.to_string() 88 | } 89 | 90 | fn get_type(&self) -> TokenType { 91 | self.name 92 | } 93 | 94 | fn needs_fee(&self) -> bool { 95 | self.needs_fee 96 | } 97 | 98 | async fn get_tx(&self, tx_id: String) -> Result { 99 | todo!() 100 | } 101 | 102 | async fn get_tx_status( 103 | &self, 104 | tx_id: String, 105 | ) -> Result<(StatusCode, Option), BundlerError> { 106 | todo!() 107 | } 108 | 109 | fn sign_message(&self, message: &[u8]) -> Result, BundlerError> { 110 | match &self.signer { 111 | Some(signer) => Ok(signer.sign(Bytes::copy_from_slice(message))?.to_vec()), 112 | None => Err(BundlerError::TokenError( 113 | "No private key present".to_string(), 114 | )), 115 | } 116 | } 117 | 118 | fn verify(&self, pub_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), BundlerError> { 119 | Ed25519Signer::verify( 120 | Bytes::copy_from_slice(pub_key), 121 | Bytes::copy_from_slice(message), 122 | Bytes::copy_from_slice(signature), 123 | ) 124 | .map(|_| ()) 125 | } 126 | 127 | fn get_pub_key(&self) -> Result { 128 | match &self.signer { 129 | Some(signer) => Ok(signer.pub_key()), 130 | None => Err(BundlerError::TokenError( 131 | "No private key present".to_string(), 132 | )), 133 | } 134 | } 135 | 136 | fn wallet_address(&self) -> Result { 137 | todo!(); 138 | } 139 | 140 | fn get_signer(&self) -> Result<&dyn Signer, BundlerError> { 141 | match &self.signer { 142 | Some(signer) => Ok(signer), 143 | None => Err(BundlerError::TokenError( 144 | "No private key present".to_string(), 145 | )), 146 | } 147 | } 148 | 149 | async fn get_id(&self, _item: ()) -> String { 150 | todo!(); 151 | } 152 | 153 | async fn price(&self) -> String { 154 | todo!(); 155 | } 156 | 157 | async fn get_current_height(&self) -> u128 { 158 | todo!(); 159 | } 160 | 161 | async fn get_fee(&self, _amount: u64, to: &str, multiplier: f64) -> Result { 162 | todo!(); 163 | } 164 | 165 | async fn create_tx(&self, amount: u64, to: &str, fee: u64) -> Result { 166 | todo!(); 167 | } 168 | 169 | async fn send_tx(&self, data: Tx) -> Result { 170 | todo!() 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/token/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "arweave")] 2 | pub mod arweave; 3 | #[cfg(feature = "solana")] 4 | pub mod solana; 5 | 6 | #[cfg(feature = "ethereum")] 7 | pub mod ethereum; 8 | 9 | use core::fmt; 10 | 11 | use bytes::Bytes; 12 | use num_derive::FromPrimitive; 13 | use reqwest::StatusCode; 14 | use serde::{Deserialize, Serialize}; 15 | use std::str::FromStr; 16 | 17 | #[cfg(feature = "build-binary")] 18 | use clap::ValueEnum; 19 | 20 | use crate::{ 21 | error::BundlerError, 22 | transaction::{Tx, TxStatus}, 23 | Signer, 24 | }; 25 | 26 | #[derive(FromPrimitive, Debug, Copy, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)] 27 | #[cfg_attr(feature = "build-binary", derive(ValueEnum))] 28 | pub enum TokenType { 29 | Arweave = 1, 30 | Solana = 2, 31 | Ethereum = 3, 32 | Erc20 = 4, 33 | Cosmos = 5, 34 | } 35 | 36 | #[derive(Deserialize)] 37 | pub struct TxResponse { 38 | pub tx_id: String, 39 | } 40 | 41 | impl fmt::Display for TokenType { 42 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 43 | write!(formatter, "{}", format!("{:?}", self).to_lowercase()) 44 | } 45 | } 46 | 47 | impl FromStr for TokenType { 48 | type Err = anyhow::Error; 49 | fn from_str(input: &str) -> Result { 50 | match input { 51 | "arweave" => Ok(TokenType::Arweave), 52 | "solana" => Ok(TokenType::Solana), 53 | "ethereum" => Ok(TokenType::Ethereum), 54 | "erc20" => Ok(TokenType::Erc20), 55 | "cosmos" => Ok(TokenType::Cosmos), 56 | _ => Err(anyhow::Error::msg("Invalid or unsupported token")), 57 | } 58 | } 59 | } 60 | 61 | #[async_trait::async_trait] 62 | pub trait Token { 63 | /// Gets the base unit name, such as "winston" for Arweave 64 | fn get_min_unit_name(&self) -> String; 65 | 66 | /// Gets token type 67 | fn get_type(&self) -> TokenType; 68 | 69 | /// Returns if the token needs fee for transacting 70 | fn needs_fee(&self) -> bool; 71 | 72 | /// Gets transaction based on transaction id 73 | async fn get_tx(&self, tx_id: String) -> Result; 74 | 75 | /// Gets the transaction status, including height, included block's hash and height 76 | async fn get_tx_status( 77 | &self, 78 | tx_id: String, 79 | ) -> Result<(StatusCode, Option), BundlerError>; 80 | 81 | /// Gets public key 82 | fn get_pub_key(&self) -> Result; 83 | 84 | /// Gets wallet address, usually a hash from public key 85 | fn wallet_address(&self) -> Result; 86 | 87 | /// Signs a given message 88 | fn sign_message(&self, message: &[u8]) -> Result, BundlerError>; 89 | 90 | /// Verifies if public key, message and signature matches 91 | fn verify(&self, pub_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), BundlerError>; 92 | 93 | /// Gets signer for more specific operations 94 | fn get_signer(&self) -> Result<&dyn Signer, BundlerError>; 95 | 96 | /// Gets token Id 97 | async fn get_id(&self, item: ()) -> String; 98 | 99 | /// Get price of token in USD 100 | async fn price(&self) -> String; 101 | 102 | /// Get given token network's block height 103 | async fn get_current_height(&self) -> u128; 104 | 105 | /// Get fee for transaction 106 | async fn get_fee(&self, amount: u64, to: &str, multiplier: f64) -> Result; 107 | 108 | /// Creates a new transaction 109 | async fn create_tx(&self, amount: u64, to: &str, fee: u64) -> Result; 110 | 111 | /// Send a signed transaction 112 | async fn send_tx(&self, data: Tx) -> Result; 113 | } 114 | -------------------------------------------------------------------------------- /src/token/solana.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use reqwest::{StatusCode, Url}; 3 | 4 | use crate::{ 5 | error::{BuilderError, BundlerError}, 6 | transaction::{Tx, TxStatus}, 7 | Ed25519Signer, Signer, Verifier, 8 | }; 9 | 10 | use super::{Token, TokenType, TxResponse}; 11 | 12 | const SOLANA_TICKER: &str = "SOL"; 13 | const SOLANA_BASE_UNIT: &str = "lamport"; 14 | const SOLANA_BASE_URL: &str = "https://explorer.solana.com/"; 15 | 16 | #[allow(unused)] 17 | pub struct Solana { 18 | signer: Option, 19 | is_slow: bool, 20 | needs_fee: bool, 21 | base: (String, i64), 22 | name: TokenType, 23 | ticker: String, 24 | min_confirm: i16, 25 | client: reqwest::Client, 26 | url: Url, 27 | } 28 | 29 | impl Default for Solana { 30 | fn default() -> Self { 31 | let url = Url::parse(SOLANA_BASE_URL).unwrap(); 32 | Self { 33 | signer: None, 34 | needs_fee: true, 35 | is_slow: false, 36 | base: (SOLANA_BASE_UNIT.to_string(), 0), 37 | name: TokenType::Solana, 38 | ticker: SOLANA_TICKER.to_string(), 39 | min_confirm: 10, 40 | client: reqwest::Client::new(), 41 | url, 42 | } 43 | } 44 | } 45 | 46 | #[derive(Default)] 47 | pub struct SolanaBuilder { 48 | base_url: Option, 49 | wallet: Option, 50 | } 51 | 52 | impl SolanaBuilder { 53 | pub fn new() -> SolanaBuilder { 54 | Default::default() 55 | } 56 | 57 | pub fn base_url(mut self, base_url: Url) -> SolanaBuilder { 58 | self.base_url = Some(base_url); 59 | self 60 | } 61 | 62 | pub fn wallet(mut self, wallet: &str) -> SolanaBuilder { 63 | self.wallet = Some(wallet.into()); 64 | self 65 | } 66 | 67 | pub fn build(self) -> Result { 68 | let signer = if let Some(wallet) = self.wallet { 69 | Some(Ed25519Signer::from_base58(&wallet)?) 70 | } else { 71 | None 72 | }; 73 | Ok(Solana { 74 | signer, 75 | url: self 76 | .base_url 77 | .unwrap_or_else(|| Url::parse(SOLANA_BASE_URL).unwrap()), 78 | ..Solana::default() 79 | }) 80 | } 81 | } 82 | 83 | #[allow(unused)] 84 | #[async_trait::async_trait] 85 | impl Token for Solana { 86 | fn get_min_unit_name(&self) -> String { 87 | SOLANA_BASE_UNIT.to_string() 88 | } 89 | 90 | fn get_type(&self) -> TokenType { 91 | self.name 92 | } 93 | 94 | fn needs_fee(&self) -> bool { 95 | self.needs_fee 96 | } 97 | 98 | async fn get_tx(&self, tx_id: String) -> Result { 99 | todo!() 100 | } 101 | 102 | async fn get_tx_status( 103 | &self, 104 | tx_id: String, 105 | ) -> Result<(StatusCode, Option), BundlerError> { 106 | todo!() 107 | } 108 | 109 | fn sign_message(&self, message: &[u8]) -> Result, BundlerError> { 110 | match &self.signer { 111 | Some(signer) => Ok(signer.sign(Bytes::copy_from_slice(message))?.to_vec()), 112 | None => Err(BundlerError::TokenError( 113 | "No private key present".to_string(), 114 | )), 115 | } 116 | } 117 | 118 | fn verify(&self, pub_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), BundlerError> { 119 | Ed25519Signer::verify( 120 | Bytes::copy_from_slice(pub_key), 121 | Bytes::copy_from_slice(message), 122 | Bytes::copy_from_slice(signature), 123 | ) 124 | .map(|_| ()) 125 | } 126 | 127 | fn get_pub_key(&self) -> Result { 128 | match &self.signer { 129 | Some(signer) => Ok(signer.pub_key()), 130 | None => Err(BundlerError::TokenError( 131 | "No private key present".to_string(), 132 | )), 133 | } 134 | } 135 | 136 | fn wallet_address(&self) -> Result { 137 | todo!(); 138 | } 139 | 140 | fn get_signer(&self) -> Result<&dyn Signer, BundlerError> { 141 | match &self.signer { 142 | Some(signer) => Ok(signer), 143 | None => Err(BundlerError::TokenError( 144 | "No private key present".to_string(), 145 | )), 146 | } 147 | } 148 | 149 | async fn get_id(&self, _item: ()) -> String { 150 | todo!(); 151 | } 152 | 153 | async fn price(&self) -> String { 154 | todo!(); 155 | } 156 | 157 | async fn get_current_height(&self) -> u128 { 158 | todo!(); 159 | } 160 | 161 | async fn get_fee(&self, _amount: u64, to: &str, multiplier: f64) -> Result { 162 | todo!(); 163 | } 164 | 165 | async fn create_tx(&self, amount: u64, to: &str, fee: u64) -> Result { 166 | todo!(); 167 | } 168 | 169 | async fn send_tx(&self, data: Tx) -> Result { 170 | todo!() 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/transaction/irys.rs: -------------------------------------------------------------------------------- 1 | use async_stream::try_stream; 2 | use bytes::{BufMut, Bytes}; 3 | use futures::Stream; 4 | use ring::rand::SecureRandom; 5 | use std::cmp; 6 | use std::fs::File; 7 | use std::pin::Pin; 8 | 9 | use crate::consts::{CHUNK_SIZE, DATAITEM_AS_BUFFER, ONE_AS_BUFFER}; 10 | use crate::deep_hash::{deep_hash, DeepHashChunk}; 11 | use crate::deep_hash_sync::deep_hash_sync; 12 | use crate::error::BundlerError; 13 | use crate::index::{Config, SignerMap}; 14 | use crate::signers::Signer; 15 | use crate::tags::{AvroDecode, AvroEncode, Tag}; 16 | use crate::utils::read_offset; 17 | 18 | enum Data { 19 | None, 20 | Bytes(Vec), 21 | Stream(Pin>>>), 22 | } 23 | 24 | pub struct BundlerTx { 25 | signature_type: SignerMap, 26 | signature: Vec, 27 | owner: Vec, 28 | target: Vec, 29 | anchor: Vec, 30 | tags: Vec, 31 | data: Data, 32 | } 33 | 34 | impl BundlerTx { 35 | pub fn new(target: Vec, data: Vec, tags: Vec) -> Result { 36 | let mut randoms: [u8; 32] = [0; 32]; 37 | let sr = ring::rand::SystemRandom::new(); 38 | match sr.fill(&mut randoms) { 39 | Ok(()) => (), 40 | Err(err) => return Err(BundlerError::Unknown(err.to_string())), 41 | } 42 | let anchor = randoms.to_vec(); 43 | 44 | Ok(BundlerTx { 45 | signature_type: SignerMap::None, 46 | signature: vec![], 47 | owner: vec![], 48 | target, 49 | anchor, 50 | tags, 51 | data: Data::Bytes(data), 52 | }) 53 | } 54 | 55 | fn from_info_bytes(buffer: &[u8]) -> Result<(Self, usize), BundlerError> { 56 | let sig_type_b = &buffer[0..2]; 57 | let signature_type = u16::from_le_bytes( 58 | <[u8; 2]>::try_from(sig_type_b) 59 | .map_err(|err| BundlerError::BytesError(err.to_string()))?, 60 | ); 61 | let signer = SignerMap::from(signature_type); 62 | 63 | let Config { 64 | pub_length, 65 | sig_length, 66 | .. 67 | } = signer.get_config(); 68 | 69 | let signature = &buffer[2..2 + sig_length]; 70 | let owner = &buffer[2 + sig_length..2 + sig_length + pub_length]; 71 | 72 | let target_start = 2 + sig_length + pub_length; 73 | let target_present = u8::from_le_bytes( 74 | <[u8; 1]>::try_from(&buffer[target_start..target_start + 1]) 75 | .map_err(|err| BundlerError::BytesError(err.to_string()))?, 76 | ); 77 | let target = match target_present { 78 | 0 => &[], 79 | 1 => &buffer[target_start + 1..target_start + 33], 80 | b => return Err(BundlerError::InvalidPresenceByte(b.to_string())), 81 | }; 82 | let anchor_start = target_start + 1 + target.len(); 83 | let anchor_present = u8::from_le_bytes( 84 | <[u8; 1]>::try_from(&buffer[anchor_start..anchor_start + 1]) 85 | .map_err(|err| BundlerError::BytesError(err.to_string()))?, 86 | ); 87 | let anchor = match anchor_present { 88 | 0 => &[], 89 | 1 => &buffer[anchor_start + 1..anchor_start + 33], 90 | b => return Err(BundlerError::InvalidPresenceByte(b.to_string())), 91 | }; 92 | 93 | let tags_start = anchor_start + 1 + anchor.len(); 94 | let number_of_tags = u64::from_le_bytes( 95 | <[u8; 8]>::try_from(&buffer[tags_start..tags_start + 8]) 96 | .map_err(|err| BundlerError::BytesError(err.to_string()))?, 97 | ); 98 | 99 | let number_of_tags_bytes = u64::from_le_bytes( 100 | <[u8; 8]>::try_from(&buffer[tags_start + 8..tags_start + 16]) 101 | .map_err(|err| BundlerError::BytesError(err.to_string()))?, 102 | ); 103 | 104 | let mut b = buffer.to_vec(); 105 | let mut tags_bytes = 106 | &mut b[tags_start + 16..tags_start + 16 + number_of_tags_bytes as usize]; 107 | 108 | let tags = if number_of_tags_bytes > 0 { 109 | tags_bytes.decode()? 110 | } else { 111 | vec![] 112 | }; 113 | 114 | if number_of_tags != tags.len() as u64 { 115 | return Err(BundlerError::InvalidTagEncoding); 116 | } 117 | 118 | let bundler_tx = BundlerTx { 119 | signature_type: signer, 120 | signature: signature.to_vec(), 121 | owner: owner.to_vec(), 122 | target: target.to_vec(), 123 | anchor: anchor.to_vec(), 124 | tags, 125 | data: Data::None, 126 | }; 127 | 128 | Ok((bundler_tx, tags_start + 16 + number_of_tags_bytes as usize)) 129 | } 130 | 131 | pub fn from_bytes(buffer: Vec) -> Result { 132 | let (bundler_tx, data_start) = BundlerTx::from_info_bytes(&buffer)?; 133 | let data = &buffer[data_start..buffer.len()]; 134 | 135 | Ok(BundlerTx { 136 | data: Data::Bytes(data.to_vec()), 137 | ..bundler_tx 138 | }) 139 | } 140 | 141 | pub fn from_file_position( 142 | file: &mut File, 143 | size: u64, 144 | offset: u64, 145 | length: usize, 146 | ) -> Result { 147 | let buffer = read_offset(file, offset, length).map_err(BundlerError::IoError)?; 148 | let (bundler_tx, data_start) = BundlerTx::from_info_bytes(&buffer)?; 149 | 150 | let data_start = data_start as u64; 151 | let data_size = size - data_start; 152 | let mut file_clone = file.try_clone()?; 153 | let file_stream = try_stream! { 154 | let chunk_size = CHUNK_SIZE; 155 | let mut read = 0; 156 | while read < data_size { 157 | let b = read_offset(&mut file_clone, offset + data_start + read, cmp::min(data_size - read, chunk_size) as usize)?; 158 | read += b.len() as u64; 159 | yield b; 160 | }; 161 | }; 162 | 163 | Ok(BundlerTx { 164 | data: Data::Stream(Box::pin(file_stream)), 165 | ..bundler_tx 166 | }) 167 | } 168 | 169 | pub fn is_signed(&self) -> bool { 170 | !self.signature.is_empty() && self.signature_type != SignerMap::None 171 | } 172 | 173 | pub fn as_bytes(self) -> Result, BundlerError> { 174 | if !self.is_signed() { 175 | return Err(BundlerError::NoSignature); 176 | } 177 | let data = match &self.data { 178 | Data::Stream(_) => return Err(BundlerError::InvalidDataType), 179 | Data::None => return Err(BundlerError::InvalidDataType), 180 | Data::Bytes(data) => data, 181 | }; 182 | 183 | let encoded_tags = if !self.tags.is_empty() { 184 | self.tags.encode()? 185 | } else { 186 | Bytes::default() 187 | }; 188 | let config = self.signature_type.get_config(); 189 | let length = 2u64 190 | + config.sig_length as u64 191 | + config.pub_length as u64 192 | + 34 193 | + 16 194 | + encoded_tags.len() as u64 195 | + data.len() as u64; 196 | 197 | let mut b = Vec::with_capacity( 198 | TryInto::::try_into(length) 199 | .map_err(|err| BundlerError::TypeParseError(err.to_string()))?, 200 | ); 201 | 202 | let sig_type: [u8; 2] = (self.signature_type as u16).to_le_bytes(); 203 | let target_presence_byte = if self.target.is_empty() { 204 | &[0u8] 205 | } else { 206 | &[1u8] 207 | }; 208 | let anchor_presence_byte = if self.anchor.is_empty() { 209 | &[0u8] 210 | } else { 211 | &[1u8] 212 | }; 213 | b.put(&sig_type[..]); 214 | b.put(&self.signature[..]); 215 | b.put(&self.owner[..]); 216 | b.put(&target_presence_byte[..]); 217 | b.put(&self.target[..]); 218 | b.put(&anchor_presence_byte[..]); 219 | b.put(&self.anchor[..]); 220 | let number_of_tags = (self.tags.len() as u64).to_le_bytes(); 221 | let number_of_tags_bytes = (encoded_tags.len() as u64).to_le_bytes(); 222 | b.put(number_of_tags.as_slice()); 223 | b.put(number_of_tags_bytes.as_slice()); 224 | if !number_of_tags_bytes.is_empty() { 225 | b.put(encoded_tags); 226 | } 227 | 228 | b.put(&data[..]); 229 | Ok(b) 230 | } 231 | 232 | pub fn as_byte_stream( 233 | self, 234 | ) -> Result>>>, BundlerError> { 235 | todo!(); 236 | } 237 | 238 | async fn get_message(&mut self) -> Result { 239 | let encoded_tags = if !self.tags.is_empty() { 240 | self.tags.encode()? 241 | } else { 242 | Bytes::default() 243 | }; 244 | 245 | match &mut self.data { 246 | Data::None => Ok(Bytes::new()), 247 | Data::Bytes(data) => { 248 | let data_chunk = DeepHashChunk::Chunk(data.clone().into()); 249 | let sig_type = &self.signature_type; 250 | let sig_type_bytes = sig_type.as_u16().to_string().as_bytes().to_vec(); 251 | deep_hash_sync(DeepHashChunk::Chunks(vec![ 252 | DeepHashChunk::Chunk(DATAITEM_AS_BUFFER.into()), 253 | DeepHashChunk::Chunk(ONE_AS_BUFFER.into()), 254 | DeepHashChunk::Chunk(sig_type_bytes.to_vec().into()), 255 | DeepHashChunk::Chunk(self.owner.to_vec().into()), 256 | DeepHashChunk::Chunk(self.target.to_vec().into()), 257 | DeepHashChunk::Chunk(self.anchor.to_vec().into()), 258 | DeepHashChunk::Chunk(encoded_tags.clone()), 259 | data_chunk, 260 | ])) 261 | } 262 | Data::Stream(file_stream) => { 263 | let data_chunk = DeepHashChunk::Stream(file_stream); 264 | let sig_type = &self.signature_type; 265 | let sig_type_bytes = sig_type.as_u16().to_string().as_bytes().to_vec(); 266 | deep_hash(DeepHashChunk::Chunks(vec![ 267 | DeepHashChunk::Chunk(DATAITEM_AS_BUFFER.into()), 268 | DeepHashChunk::Chunk(ONE_AS_BUFFER.into()), 269 | DeepHashChunk::Chunk(sig_type_bytes.to_vec().into()), 270 | DeepHashChunk::Chunk(self.owner.to_vec().into()), 271 | DeepHashChunk::Chunk(self.target.to_vec().into()), 272 | DeepHashChunk::Chunk(self.anchor.to_vec().into()), 273 | DeepHashChunk::Chunk(encoded_tags.clone()), 274 | data_chunk, 275 | ])) 276 | .await 277 | } 278 | } 279 | } 280 | 281 | pub async fn sign(&mut self, signer: &dyn Signer) -> Result<(), BundlerError> { 282 | self.signature_type = signer.sig_type(); 283 | self.owner = signer.pub_key().to_vec(); 284 | 285 | let message = self.get_message().await?; 286 | 287 | let sig = signer.sign(message)?; 288 | self.signature = sig.to_vec(); 289 | 290 | Ok(()) 291 | } 292 | 293 | pub async fn verify(&mut self) -> Result<(), BundlerError> { 294 | let message = self.get_message().await?; 295 | let pub_key = &self.owner; 296 | let signature = &self.signature; 297 | 298 | let verifier = &self.signature_type; 299 | verifier.verify(pub_key, &message, signature) 300 | } 301 | 302 | pub fn get_signarure(&self) -> Vec { 303 | self.signature.clone() 304 | } 305 | } 306 | 307 | #[cfg(test)] 308 | mod tests { 309 | use crate::tags::Tag; 310 | #[cfg(feature = "solana")] 311 | use crate::transaction::irys::BundlerTx; 312 | use crate::{ArweaveSigner, Ed25519Signer, Secp256k1Signer}; 313 | use secp256k1::SecretKey; 314 | use std::path::PathBuf; 315 | use std::str::FromStr; 316 | use std::{fs, fs::File, io::Write}; 317 | 318 | #[allow(unused)] 319 | macro_rules! aw { 320 | ($e:expr) => { 321 | tokio_test::block_on($e) 322 | }; 323 | } 324 | #[tokio::test] 325 | async fn test_create_sign_verify_load_ed25519() { 326 | let path = "./res/test_bundles/test_data_item_ed25519"; 327 | let secret_key = "kNykCXNxgePDjFbDWjPNvXQRa8U12Ywc19dFVaQ7tebUj3m7H4sF4KKdJwM7yxxb3rqxchdjezX9Szh8bLcQAjb"; 328 | let signer = Ed25519Signer::from_base58(secret_key).unwrap(); 329 | let mut data_item_1 = BundlerTx::new( 330 | Vec::from(""), 331 | Vec::from("hello"), 332 | vec![Tag::new("name", "value")], 333 | ) 334 | .unwrap(); 335 | let res = data_item_1.sign(&signer).await; 336 | assert!(res.is_ok()); 337 | 338 | let mut f = File::create(path).unwrap(); 339 | let data_item_1_bytes = data_item_1.as_bytes().unwrap(); 340 | f.write_all(&data_item_1_bytes).unwrap(); 341 | 342 | let buffer = fs::read(path).expect("Could not read file"); 343 | let data_item_2 = BundlerTx::from_bytes(buffer).expect("Invalid bytes"); 344 | assert!(&data_item_2.is_signed()); 345 | 346 | assert_eq!(data_item_1_bytes, data_item_2.as_bytes().unwrap()); 347 | } 348 | 349 | #[tokio::test] 350 | async fn test_create_sign_verify_load_rsa4096() { 351 | let path = "./res/test_bundles/test_data_item_rsa4096"; 352 | let key_path = PathBuf::from_str("res/test_wallet.json").unwrap(); 353 | let signer = ArweaveSigner::from_keypair_path(key_path).unwrap(); 354 | let mut data_item_1 = BundlerTx::new( 355 | Vec::from(""), 356 | Vec::from("hello"), 357 | vec![Tag::new("name", "value")], 358 | ) 359 | .unwrap(); 360 | let res = data_item_1.sign(&signer).await; 361 | assert!(res.is_ok()); 362 | 363 | let mut f = File::create(path).unwrap(); 364 | let data_item_1_bytes = data_item_1.as_bytes().unwrap(); 365 | f.write_all(&data_item_1_bytes).unwrap(); 366 | 367 | let buffer = fs::read(path).expect("Could not read file"); 368 | let data_item_2 = BundlerTx::from_bytes(buffer).expect("Invalid bytes"); 369 | assert!(&data_item_2.is_signed()); 370 | assert_eq!(data_item_1_bytes, data_item_2.as_bytes().unwrap()); 371 | } 372 | 373 | #[tokio::test] 374 | async fn test_create_sign_verify_load_cosmos() { 375 | //TODO: assign cosmos constant then fix this 376 | /* 377 | let path = "./res/test_bundles/test_data_item_cosmos"; 378 | let base58_secret_key = "28PmkjeZqLyfRQogb3FU4E1vJh68dXpbojvS2tcPwezZmVQp8zs8ebGmYg1hNRcjX4DkUALf3SkZtytGWPG3vYhs"; 379 | let signer = CosmosSigner::from_base58(base58_secret_key).unwrap(); 380 | let mut data_item_1 = BundlerTx::new( 381 | Vec::from(""), 382 | Vec::from("hello"), 383 | vec![Tag::new("name", "value")], 384 | ); 385 | let res = data_item_1.sign(&signer).await; 386 | assert!(res.is_ok()); 387 | 388 | let mut f = File::create(path).unwrap(); 389 | let data_item_1_bytes = data_item_1.as_bytes().unwrap(); 390 | f.write_all(&data_item_1_bytes).unwrap(); 391 | 392 | let buffer = fs::read(path).expect("Could not read file"); 393 | let data_item_2 = BundlerTx::from_bytes(buffer).expect("Invalid bytes"); 394 | assert!(&data_item_2.is_signed()); 395 | assert_eq!(data_item_1_bytes, data_item_2.as_bytes().unwrap()); 396 | */ 397 | } 398 | 399 | #[tokio::test] 400 | async fn test_create_sign_verify_load_secp256k1() { 401 | let path = "./res/test_bundles/test_data_item_secp256k1"; 402 | let secret_key = SecretKey::from_slice(b"00000000000000000000000000000000").unwrap(); 403 | let signer = Secp256k1Signer::new(secret_key); 404 | let mut data_item_1 = BundlerTx::new( 405 | Vec::from(""), 406 | Vec::from("hello"), 407 | vec![Tag::new("name", "value")], 408 | ) 409 | .unwrap(); 410 | let res = data_item_1.sign(&signer).await; 411 | assert!(res.is_ok()); 412 | let mut f = File::create(path).unwrap(); 413 | let data_item_1_bytes = data_item_1.as_bytes().unwrap(); 414 | f.write_all(&data_item_1_bytes).unwrap(); 415 | 416 | let buffer = fs::read(path).expect("Could not read file"); 417 | let data_item_2 = BundlerTx::from_bytes(buffer).expect("Invalid bytes"); 418 | assert!(&data_item_2.is_signed()); 419 | assert_eq!(data_item_1_bytes, data_item_2.as_bytes().unwrap()); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/transaction/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod irys; 2 | pub mod poll; 3 | 4 | #[derive(Debug)] 5 | pub struct TxStatus { 6 | pub confirmations: u64, 7 | pub height: u128, 8 | pub block_hash: String, 9 | } 10 | 11 | pub struct Tx { 12 | pub id: String, 13 | pub from: String, 14 | pub to: String, 15 | pub amount: u64, 16 | pub fee: u64, 17 | pub block_height: u128, 18 | pub pending: bool, 19 | pub confirmed: bool, 20 | } 21 | -------------------------------------------------------------------------------- /src/transaction/poll.rs: -------------------------------------------------------------------------------- 1 | use std::{thread::sleep, time::Duration}; 2 | 3 | use crate::{ 4 | consts::{CONFIRMATIONS_NEEDED, RETRY_SLEEP}, 5 | token::Token, 6 | }; 7 | 8 | pub struct ConfirmationPoll(); 9 | 10 | #[allow(unused)] 11 | impl ConfirmationPoll { 12 | pub async fn await_confirmation(tx_id: &String, token: &dyn Token) { 13 | let mut confirmations = 0; 14 | while confirmations < CONFIRMATIONS_NEEDED { 15 | let (status, tx_status) = match token.get_tx_status(tx_id.to_string()).await { 16 | Ok(ok) => ok, 17 | Err(err) => continue, 18 | }; 19 | 20 | if let Some(tx_status) = tx_status { 21 | confirmations = tx_status.confirmations 22 | } 23 | 24 | sleep(Duration::from_secs(RETRY_SLEEP)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/upload.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, thread::sleep, time::Duration}; 2 | 3 | use reqwest::{header::ACCEPT, Url}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{ 7 | consts::{CHUNKS_RETRIES, CHUNKS_RETRY_SLEEP, CHUNK_SIZE, DEFAULT_BUNDLER_URL}, 8 | error::BundlerError, 9 | token::TokenType, 10 | }; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | struct IdRes { 14 | id: String, 15 | max: u64, 16 | min: u64, 17 | } 18 | 19 | pub struct Uploader { 20 | url: Url, 21 | client: reqwest::Client, 22 | pub upload_id: Option, 23 | token: TokenType, 24 | chunk_size: u64, 25 | } 26 | 27 | impl Default for Uploader { 28 | fn default() -> Self { 29 | let url = Url::from_str(DEFAULT_BUNDLER_URL).unwrap(); //Unwrap ok, never fails 30 | let client = reqwest::Client::new(); 31 | Self { 32 | url, 33 | client, 34 | upload_id: None, 35 | token: TokenType::Arweave, 36 | chunk_size: CHUNK_SIZE, 37 | } 38 | } 39 | } 40 | 41 | impl Uploader { 42 | pub fn new(url: Url, client: reqwest::Client, token: TokenType) -> Self { 43 | Uploader { 44 | url, 45 | client, 46 | upload_id: None, 47 | token, 48 | chunk_size: CHUNK_SIZE, 49 | } 50 | } 51 | 52 | pub async fn upload(&mut self, _data: Vec) -> Result<(), BundlerError> { 53 | let (max, min) = if let Some(upload_id) = self.upload_id.clone() { 54 | let url = self 55 | .url 56 | .join(&format!("/chunks/{}/{}/-1", self.token, upload_id)) 57 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 58 | let res = self 59 | .client 60 | .get(url) 61 | .header("x-chunking-version", "2") 62 | .send() 63 | .await 64 | .map_err(|err| BundlerError::UploadError(err.to_string()))? 65 | .json::() 66 | .await 67 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 68 | 69 | (res.max, res.min) 70 | } else { 71 | let url = self 72 | .url 73 | .join(&format!("/chunks/{}/-1/-1", self.token)) 74 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 75 | let res = self 76 | .client 77 | .get(url) 78 | .header("x-chunking-version", "2") 79 | .send() 80 | .await 81 | .map_err(|err| BundlerError::UploadError(err.to_string()))? 82 | .json::() 83 | .await 84 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 85 | 86 | self.upload_id = Some(res.id); 87 | 88 | (res.max, res.min) 89 | }; 90 | 91 | if self.chunk_size < min || self.chunk_size > max { 92 | return Err(BundlerError::ChunkSizeOutOfRange(min, max)); 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | /* 99 | fn upload_transaction_chunks_stream<'a>( 100 | uploader: &'a Uploader, 101 | chunks: Vec>, 102 | buffer: usize, 103 | ) -> impl Stream> + 'a { 104 | stream::iter(0..chunks.len()) 105 | .map(move |i| { 106 | let chunk = chunks[i].clone(); 107 | uploader.post_chunk_with_retries(chunk, 0, vec![]) 108 | }) 109 | .buffer_unordered(buffer) 110 | } 111 | */ 112 | 113 | pub async fn post_chunk_with_retries( 114 | &self, 115 | chunk: Vec, 116 | offset: usize, 117 | headers: Vec<(String, String)>, 118 | ) -> Result { 119 | let mut retries = 0; 120 | let mut resp = self.post_chunk(&chunk, offset, headers.clone()).await; 121 | 122 | while retries < CHUNKS_RETRIES { 123 | match resp { 124 | Ok(offset) => return Ok(offset), 125 | Err(e) => { 126 | dbg!("post_chunk_with_retries: {:?}", e); 127 | sleep(Duration::from_secs(CHUNKS_RETRY_SLEEP)); 128 | retries += 1; 129 | resp = self.post_chunk(&chunk, offset, headers.clone()).await; 130 | } 131 | } 132 | } 133 | resp 134 | } 135 | 136 | pub async fn post_chunk( 137 | &self, 138 | chunk: &[u8], 139 | offset: usize, 140 | headers: Vec<(String, String)>, 141 | ) -> Result { 142 | let upload_id = match &self.upload_id { 143 | Some(id) => id, 144 | None => return Err(BundlerError::UploadError("No upload id".to_string())), 145 | }; 146 | let url = self 147 | .url 148 | .join(&format!("/chunks/{}/{}/{}", self.token, upload_id, offset)) 149 | .map_err(|err| BundlerError::ParseError(err.to_string()))?; 150 | 151 | let mut req = self 152 | .client 153 | .post(url) 154 | .json(&chunk) 155 | .header(&ACCEPT, "application/json"); 156 | for (header, value) in headers { 157 | req = req.header(header, value); 158 | } 159 | 160 | let res = req 161 | .send() 162 | .await 163 | .map_err(|e| BundlerError::PostChunkError(e.to_string()))?; 164 | 165 | match res.status() { 166 | reqwest::StatusCode::OK => Ok(offset), 167 | err => Err(BundlerError::RequestError(err.to_string())), 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/utils/eip712/encode.rs: -------------------------------------------------------------------------------- 1 | use super::error::serde_error; 2 | use super::error::Eip712Error; 3 | use super::parser::parse_type; 4 | use super::parser::Type; 5 | use super::MessageTypes; 6 | use super::EIP712; 7 | use indexmap::IndexSet; 8 | use rustc_hex::FromHex; 9 | use serde_json::to_value; 10 | use serde_json::Value; 11 | use std::collections::HashSet; 12 | use std::str::FromStr; 13 | use validator::Validate; 14 | use web3::ethabi::ethereum_types::{Address as EthAddress, H256, U256}; 15 | use web3::ethabi::{encode, Token as EthAbiToken}; 16 | use web3::signing::keccak256; 17 | 18 | fn check_hex(string: &str) -> Result<(), Eip712Error> { 19 | if string.len() >= 2 && &string[..2] == "0x" { 20 | return Ok(()); 21 | } 22 | 23 | Err(Eip712Error::HexParseError(format!( 24 | "Expected a 0x-prefixed string of even length, found {} length string", 25 | string.len() 26 | ))) 27 | } 28 | /// given a type and HashMap> 29 | /// returns a HashSet of dependent types of the given type 30 | fn build_dependencies<'a>( 31 | message_type: &'a str, 32 | message_types: &'a MessageTypes, 33 | ) -> Option> { 34 | message_types.get(message_type)?; 35 | 36 | let mut types = IndexSet::new(); 37 | types.insert(message_type); 38 | let mut deps = HashSet::new(); 39 | 40 | while let Some(item) = types.pop() { 41 | if let Some(fields) = message_types.get(item) { 42 | deps.insert(item); 43 | 44 | for field in fields { 45 | // check if this field is an array type 46 | let field_type = if let Some(index) = field.type_.find('[') { 47 | &field.type_[..index] 48 | } else { 49 | &field.type_ 50 | }; 51 | // seen this type before? or not a custom type skip 52 | if !deps.contains(field_type) || message_types.contains_key(field_type) { 53 | types.insert(field_type); 54 | } 55 | } 56 | } 57 | } 58 | 59 | Some(deps) 60 | } 61 | 62 | fn encode_type(message_type: &str, message_types: &MessageTypes) -> Result { 63 | let deps = { 64 | let mut temp = 65 | build_dependencies(message_type, message_types).ok_or(Eip712Error::NonExistentType)?; 66 | temp.remove(message_type); 67 | let mut temp = temp.into_iter().collect::>(); 68 | (temp[..]).sort_unstable(); 69 | temp.insert(0, message_type); 70 | temp 71 | }; 72 | 73 | let encoded = deps 74 | .into_iter() 75 | .filter_map(|dep| { 76 | message_types.get(dep).map(|field_types| { 77 | let types = field_types 78 | .iter() 79 | .map(|value| format!("{} {}", value.type_, value.name)) 80 | .collect::>() 81 | .join(","); 82 | format!("{}({})", dep, types) 83 | }) 84 | }) 85 | .collect::>() 86 | .concat(); 87 | Ok(encoded) 88 | } 89 | 90 | fn type_hash(message_type: &str, typed_data: &MessageTypes) -> Result { 91 | let binding = encode_type(message_type, typed_data)?; 92 | let encoded = binding.as_bytes(); 93 | Ok(web3::types::H256(keccak256(encoded))) 94 | } 95 | 96 | fn encode_data( 97 | message_type: &Type, 98 | message_types: &MessageTypes, 99 | value: &Value, 100 | field_name: Option<&str>, 101 | ) -> Result, Eip712Error> { 102 | let encoded = match message_type { 103 | Type::Array { inner, length } => { 104 | let mut items = vec![]; 105 | let values = value.as_array().ok_or(serde_error("array", field_name))?; 106 | 107 | // check if the type definition actually matches 108 | // the length of items to be encoded 109 | if length.is_some() && Some(values.len() as u64) != *length { 110 | let array_type = format!("{}[{}]", inner.to_string(), length.unwrap()); 111 | return Err(Eip712Error::UnequalArrayItems( 112 | length.unwrap(), 113 | array_type, 114 | values.len() as u64, 115 | ))?; 116 | } 117 | 118 | for item in values { 119 | let mut encoded = encode_data(inner, message_types, item, field_name)?; 120 | items.append(&mut encoded); 121 | } 122 | 123 | keccak256(&items).as_ref().to_vec() 124 | } 125 | 126 | Type::Custom(ref ident) if message_types.get(ident).is_some() => { 127 | let type_hash = (type_hash(ident, message_types)?).0.to_vec(); 128 | let mut tokens = encode(&[EthAbiToken::FixedBytes(type_hash)]); 129 | 130 | for field in message_types 131 | .get(ident) 132 | .expect("Already checked in match guard; qed") 133 | { 134 | let value = &value[&field.name]; 135 | let type_ = parse_type(&field.type_)?; 136 | let mut encoded = encode_data(&type_, message_types, value, Some(&*field.name))?; 137 | tokens.append(&mut encoded); 138 | } 139 | 140 | keccak256(&tokens).as_ref().to_vec() 141 | } 142 | 143 | Type::Bytes => { 144 | let string = value.as_str().ok_or(serde_error("string", field_name))?; 145 | 146 | check_hex(string)?; 147 | 148 | let bytes = (string[2..]) 149 | .from_hex::>() 150 | .map_err(|err| Eip712Error::HexParseError(format!("{}", err)))?; 151 | let bytes = keccak256(&bytes).as_ref().to_vec(); 152 | 153 | encode(&[EthAbiToken::FixedBytes(bytes)]) 154 | } 155 | 156 | Type::Byte(_) => { 157 | let string = value.as_str().ok_or(serde_error("string", field_name))?; 158 | 159 | check_hex(string)?; 160 | 161 | let bytes = (string[2..]) 162 | .from_hex::>() 163 | .map_err(|err| Eip712Error::HexParseError(format!("{}", err)))?; 164 | 165 | encode(&[EthAbiToken::FixedBytes(bytes)]) 166 | } 167 | 168 | Type::String => { 169 | let value = value.as_str().ok_or(serde_error("string", field_name))?; 170 | let hash = keccak256(value.as_bytes()).as_ref().to_vec(); 171 | encode(&[EthAbiToken::FixedBytes(hash)]) 172 | } 173 | 174 | Type::Bool => encode(&[EthAbiToken::Bool( 175 | value.as_bool().ok_or(serde_error("bool", field_name))?, 176 | )]), 177 | 178 | Type::Address => { 179 | let addr = value.as_str().ok_or(serde_error("string", field_name))?; 180 | if addr.len() != 42 { 181 | return Err(Eip712Error::InvalidAddressLength(addr.len()))?; 182 | } 183 | let address = EthAddress::from_str(&addr[2..]) 184 | .map_err(|err| Eip712Error::HexParseError(format!("{}", err)))?; 185 | encode(&[EthAbiToken::Address(address)]) 186 | } 187 | 188 | Type::Uint | Type::Int => { 189 | let string = value.as_str().ok_or(serde_error("int/uint", field_name))?; 190 | 191 | check_hex(string)?; 192 | 193 | let uint = U256::from_str(&string[2..]) 194 | .map_err(|err| Eip712Error::HexParseError(format!("{}", err)))?; 195 | 196 | let token = if *message_type == Type::Uint { 197 | EthAbiToken::Uint(uint) 198 | } else { 199 | EthAbiToken::Int(uint) 200 | }; 201 | encode(&[token]) 202 | } 203 | 204 | _ => { 205 | return Err(Eip712Error::UnknownType( 206 | field_name.unwrap_or("").to_string(), 207 | message_type.to_string(), 208 | )); 209 | } 210 | }; 211 | 212 | Ok(encoded) 213 | } 214 | 215 | /// encodes and hashes the given EIP712 struct 216 | pub fn hash_structured_data(typed_data: EIP712) -> Result<[u8; 32], Eip712Error> { 217 | // validate input 218 | typed_data 219 | .validate() 220 | .map_err(Eip712Error::ValidationErrors)?; 221 | // EIP-191 compliant 222 | let prefix = (b"\x19\x01").to_vec(); 223 | let domain = to_value(&typed_data.domain).unwrap(); 224 | let (domain_hash, data_hash) = ( 225 | encode_data( 226 | &Type::Custom("EIP712Domain".into()), 227 | &typed_data.types, 228 | &domain, 229 | None, 230 | )?, 231 | encode_data( 232 | &Type::Custom(typed_data.primary_type), 233 | &typed_data.types, 234 | &typed_data.message, 235 | None, 236 | )?, 237 | ); 238 | let concat = [&prefix[..], &domain_hash[..], &data_hash[..]].concat(); 239 | Ok(keccak256(&concat)) 240 | } 241 | 242 | #[cfg(test)] 243 | mod tests { 244 | use super::*; 245 | use rustc_hex::ToHex; 246 | use serde_json::from_str; 247 | 248 | const JSON: &'static str = r#"{ 249 | "primaryType": "Mail", 250 | "domain": { 251 | "name": "Ether Mail", 252 | "version": "1", 253 | "chainId": "0x1", 254 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 255 | }, 256 | "message": { 257 | "from": { 258 | "name": "Cow", 259 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 260 | }, 261 | "to": { 262 | "name": "Bob", 263 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 264 | }, 265 | "contents": "Hello, Bob!" 266 | }, 267 | "types": { 268 | "EIP712Domain": [ 269 | { "name": "name", "type": "string" }, 270 | { "name": "version", "type": "string" }, 271 | { "name": "chainId", "type": "uint256" }, 272 | { "name": "verifyingContract", "type": "address" } 273 | ], 274 | "Person": [ 275 | { "name": "name", "type": "string" }, 276 | { "name": "wallet", "type": "address" } 277 | ], 278 | "Mail": [ 279 | { "name": "from", "type": "Person" }, 280 | { "name": "to", "type": "Person" }, 281 | { "name": "contents", "type": "string" } 282 | ] 283 | } 284 | }"#; 285 | 286 | #[test] 287 | fn test_build_dependencies() { 288 | let string = r#"{ 289 | "EIP712Domain": [ 290 | { "name": "name", "type": "string" }, 291 | { "name": "version", "type": "string" }, 292 | { "name": "chainId", "type": "uint256" }, 293 | { "name": "verifyingContract", "type": "address" } 294 | ], 295 | "Person": [ 296 | { "name": "name", "type": "string" }, 297 | { "name": "wallet", "type": "address" } 298 | ], 299 | "Mail": [ 300 | { "name": "from", "type": "Person" }, 301 | { "name": "to", "type": "Person" }, 302 | { "name": "contents", "type": "string" } 303 | ] 304 | }"#; 305 | 306 | let value = from_str::(string).expect("alas error!"); 307 | let mail = "Mail"; 308 | let person = "Person"; 309 | 310 | let hashset = { 311 | let mut temp = HashSet::new(); 312 | temp.insert(mail); 313 | temp.insert(person); 314 | temp 315 | }; 316 | assert_eq!(build_dependencies(mail, &value), Some(hashset)); 317 | } 318 | 319 | #[test] 320 | fn test_encode_type() { 321 | let string = r#"{ 322 | "EIP712Domain": [ 323 | { "name": "name", "type": "string" }, 324 | { "name": "version", "type": "string" }, 325 | { "name": "chainId", "type": "uint256" }, 326 | { "name": "verifyingContract", "type": "address" } 327 | ], 328 | "Person": [ 329 | { "name": "name", "type": "string" }, 330 | { "name": "wallet", "type": "address" } 331 | ], 332 | "Mail": [ 333 | { "name": "from", "type": "Person" }, 334 | { "name": "to", "type": "Person" }, 335 | { "name": "contents", "type": "string" } 336 | ] 337 | }"#; 338 | 339 | let value = from_str::(string).expect("alas error!"); 340 | let mail = &String::from("Mail"); 341 | assert_eq!( 342 | "Mail(Person from,Person to,string contents)Person(string name,address wallet)", 343 | encode_type(&mail, &value).expect("alas error!") 344 | ) 345 | } 346 | 347 | #[test] 348 | fn test_encode_type_hash() { 349 | let string = r#"{ 350 | "EIP712Domain": [ 351 | { "name": "name", "type": "string" }, 352 | { "name": "version", "type": "string" }, 353 | { "name": "chainId", "type": "uint256" }, 354 | { "name": "verifyingContract", "type": "address" } 355 | ], 356 | "Person": [ 357 | { "name": "name", "type": "string" }, 358 | { "name": "wallet", "type": "address" } 359 | ], 360 | "Mail": [ 361 | { "name": "from", "type": "Person" }, 362 | { "name": "to", "type": "Person" }, 363 | { "name": "contents", "type": "string" } 364 | ] 365 | }"#; 366 | 367 | let value = from_str::(string).expect("alas error!"); 368 | let mail = &String::from("Mail"); 369 | let hash = (type_hash(&mail, &value).expect("alas error!").0).to_hex::(); 370 | assert_eq!( 371 | hash, 372 | "a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2" 373 | ); 374 | } 375 | 376 | #[test] 377 | fn test_hash_data() { 378 | let typed_data = from_str::(JSON).expect("alas error!"); 379 | let hash = hash_structured_data(typed_data).expect("alas error!"); 380 | assert_eq!( 381 | &format!("{:x}", web3::types::H256(hash))[..], 382 | "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2", 383 | ) 384 | } 385 | 386 | #[test] 387 | fn test_unequal_array_lengths() { 388 | const TEST: &'static str = r#"{ 389 | "primaryType": "Mail", 390 | "domain": { 391 | "name": "Ether Mail", 392 | "version": "1", 393 | "chainId": "0x1", 394 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 395 | }, 396 | "message": { 397 | "from": { 398 | "name": "Cow", 399 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 400 | }, 401 | "to": [{ 402 | "name": "Bob", 403 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 404 | }], 405 | "contents": "Hello, Bob!" 406 | }, 407 | "types": { 408 | "EIP712Domain": [ 409 | { "name": "name", "type": "string" }, 410 | { "name": "version", "type": "string" }, 411 | { "name": "chainId", "type": "uint256" }, 412 | { "name": "verifyingContract", "type": "address" } 413 | ], 414 | "Person": [ 415 | { "name": "name", "type": "string" }, 416 | { "name": "wallet", "type": "address" } 417 | ], 418 | "Mail": [ 419 | { "name": "from", "type": "Person" }, 420 | { "name": "to", "type": "Person[2]" }, 421 | { "name": "contents", "type": "string" } 422 | ] 423 | } 424 | }"#; 425 | 426 | let typed_data = from_str::(TEST).expect("alas error!"); 427 | assert_eq!( 428 | hash_structured_data(typed_data).unwrap_err(), 429 | Eip712Error::UnequalArrayItems(2, "Person[2]".into(), 1) 430 | ) 431 | } 432 | 433 | #[test] 434 | fn test_typed_data_v4() { 435 | let string = r#"{ 436 | "types": { 437 | "EIP712Domain": [ 438 | { 439 | "name": "name", 440 | "type": "string" 441 | }, 442 | { 443 | "name": "version", 444 | "type": "string" 445 | }, 446 | { 447 | "name": "chainId", 448 | "type": "uint256" 449 | }, 450 | { 451 | "name": "verifyingContract", 452 | "type": "address" 453 | } 454 | ], 455 | "Person": [ 456 | { 457 | "name": "name", 458 | "type": "string" 459 | }, 460 | { 461 | "name": "wallets", 462 | "type": "address[]" 463 | } 464 | ], 465 | "Mail": [ 466 | { 467 | "name": "from", 468 | "type": "Person" 469 | }, 470 | { 471 | "name": "to", 472 | "type": "Person[]" 473 | }, 474 | { 475 | "name": "contents", 476 | "type": "string" 477 | } 478 | ], 479 | "Group": [ 480 | { 481 | "name": "name", 482 | "type": "string" 483 | }, 484 | { 485 | "name": "members", 486 | "type": "Person[]" 487 | } 488 | ] 489 | }, 490 | "domain": { 491 | "name": "Ether Mail", 492 | "version": "1", 493 | "chainId": "0x1", 494 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 495 | }, 496 | "primaryType": "Mail", 497 | "message": { 498 | "from": { 499 | "name": "Cow", 500 | "wallets": [ 501 | "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", 502 | "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" 503 | ] 504 | }, 505 | "to": [ 506 | { 507 | "name": "Bob", 508 | "wallets": [ 509 | "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", 510 | "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", 511 | "0xB0B0b0b0b0b0B000000000000000000000000000" 512 | ] 513 | } 514 | ], 515 | "contents": "Hello, Bob!" 516 | } 517 | }"#; 518 | 519 | let typed_data = from_str::(string).expect("alas error!"); 520 | let hash = hash_structured_data(typed_data.clone()).expect("alas error!"); 521 | assert_eq!( 522 | &format!("{:x}", web3::types::H256(hash))[..], 523 | "a85c2e2b118698e88db68a8105b794a8cc7cec074e89ef991cb4f5f533819cc2", 524 | ); 525 | } 526 | 527 | #[test] 528 | fn test_typed_data_v4_custom_array() { 529 | let string = r#"{ 530 | "types": { 531 | "EIP712Domain": [ 532 | { 533 | "name": "name", 534 | "type": "string" 535 | }, 536 | { 537 | "name": "version", 538 | "type": "string" 539 | }, 540 | { 541 | "name": "chainId", 542 | "type": "uint256" 543 | }, 544 | { 545 | "name": "verifyingContract", 546 | "type": "address" 547 | } 548 | ], 549 | "Person": [ 550 | { 551 | "name": "name", 552 | "type": "string" 553 | }, 554 | { 555 | "name": "wallets", 556 | "type": "address[]" 557 | } 558 | ], 559 | "Mail": [ 560 | { 561 | "name": "from", 562 | "type": "Person" 563 | }, 564 | { 565 | "name": "to", 566 | "type": "Group" 567 | }, 568 | { 569 | "name": "contents", 570 | "type": "string" 571 | } 572 | ], 573 | "Group": [ 574 | { 575 | "name": "name", 576 | "type": "string" 577 | }, 578 | { 579 | "name": "members", 580 | "type": "Person[]" 581 | } 582 | ] 583 | }, 584 | "domain": { 585 | "name": "Ether Mail", 586 | "version": "1", 587 | "chainId": "0x1", 588 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 589 | }, 590 | "primaryType": "Mail", 591 | "message": { 592 | "from": { 593 | "name": "Cow", 594 | "wallets": [ 595 | "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", 596 | "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" 597 | ] 598 | }, 599 | "to": { 600 | "name": "Farmers", 601 | "members": [ 602 | { 603 | "name": "Bob", 604 | "wallets": [ 605 | "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", 606 | "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", 607 | "0xB0B0b0b0b0b0B000000000000000000000000000" 608 | ] 609 | } 610 | ] 611 | }, 612 | "contents": "Hello, Bob!" 613 | } 614 | }"#; 615 | let typed_data = from_str::(string).expect("alas error!"); 616 | let hash = hash_structured_data(typed_data.clone()).expect("alas error!"); 617 | 618 | assert_eq!( 619 | &format!("{:x}", web3::types::H256(hash))[..], 620 | "cd8b34cd09c541cfc0a2fcd147e47809b98b335649c2aa700db0b0c4501a02a0", 621 | ); 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /src/utils/eip712/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use validator::{ValidationError, ValidationErrors}; 3 | 4 | /// Possible errors encountered while hashing/encoding an EIP-712 compliant data structure 5 | #[derive(Clone, Debug, PartialEq, Error)] 6 | pub enum Eip712Error { 7 | /// if we fail to deserialize from a serde::Value as a type specified in message types 8 | /// fail with this error. 9 | #[error("Expected type '{0}' for field '{1}'")] 10 | UnexpectedType(String, String), 11 | /// the primary type supplied doesn't exist in the MessageTypes 12 | #[error("The given primaryType wasn't found in the types field")] 13 | NonExistentType, 14 | /// an invalid address was encountered during encoding 15 | #[error("Address string should be a 0x-prefixed 40 character string, got '{0}'")] 16 | InvalidAddressLength(usize), 17 | /// a hex parse error occured 18 | #[error("Failed to parse hex '{0}'")] 19 | HexParseError(String), 20 | /// the field was declared with a unknown type 21 | #[error("The field '{0}' has an unknown type '{1}'")] 22 | UnknownType(String, String), 23 | /// Unexpected token 24 | #[error("Unexpected token '{0}' while parsing typename '{1}'")] 25 | UnexpectedToken(String, String), 26 | /// the user has attempted to define a typed array with a depth > 10 27 | #[error("Maximum depth for nested arrays is 10")] 28 | UnsupportedArrayDepth, 29 | /// FieldType validation error 30 | #[error("{0}")] 31 | ValidationError(ValidationError), 32 | #[error("{0}")] 33 | ValidationErrors(ValidationErrors), 34 | /// the typed array defined in message types was declared with a fixed length 35 | /// that is of unequal length with the items to be encoded 36 | #[error("Expected {0} items for array type {1}, got {2} items")] 37 | UnequalArrayItems(u64, String, u64), 38 | /// Typed array length doesn't fit into a u64 39 | #[error("Attempted to declare fixed size with length {0}")] 40 | InvalidArraySize(String), 41 | } 42 | 43 | pub(crate) fn serde_error(expected: &str, field: Option<&str>) -> Eip712Error { 44 | Eip712Error::UnexpectedType(expected.to_owned(), field.unwrap_or("").to_owned()) 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/eip712/lexer.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Lookup table layout 3 | //! =================== 4 | //! 5 | //! ```text 6 | //! EOF ; : , . ( ) { } [ ] => 7 | //! IDENT BLTIN CONTR LIB IFACE ENUM STRUCT MODIF EVENT FUNCT VAR ANON 8 | //! AS ASM BREAK CONST CONTIN DO DELETE ELSE EXTERN FOR HEX IF 9 | //! INDEX INTERN IMPORT IS MAP MEM NEW PAY PULIC PRAGMA PRIV PURE 10 | //! RET RETNS STORAG SUPER THIS THROW USING VIEW WHILE RESERV T_BOOL T_ADDR 11 | //! T_STR T_BYT T_BYTS T_INT T_UINT T_FIX T_UFIX L_TRUE L_FALS L_HEX L_INT L_RAT 12 | //! L_STR E_ETH E_FINN E_SZAB E_WEI T_YEAR T_WEEK T_DAYS T_HOUR T_MIN T_SEC := 13 | //! =: ++ -- ! ~ * / % ** + - << 14 | //! >> < <= > >= == != & ^ | && || 15 | //! ? = += -= *= /= %= <<= >>= &= ^= |= 16 | //! ERRTOK ERREOF 17 | //! ``` 18 | //! 19 | 20 | use logos::{Lexer, Logos}; 21 | #[derive(Default, Clone, Copy)] 22 | pub struct TypeSize(pub u8, pub u8); 23 | 24 | #[derive(Debug, PartialEq, Clone, Copy, Logos)] 25 | #[logos(extras = TypeSize)] 26 | pub enum Token { 27 | #[regex("[a-zA-Z_$][a-zA-Z0-9_$]*")] 28 | Identifier, 29 | 30 | #[regex("bytes1|bytes[1-2][0-9]?|bytes3[0-2]?|bytes[4-9]", validate_bytes)] 31 | TypeByte, 32 | 33 | #[token("bytes")] 34 | TypeBytes, 35 | 36 | #[token("bool")] 37 | TypeBool, 38 | 39 | #[regex("uint(8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256)", default_size)] 40 | TypeUint, 41 | 42 | #[regex("int(8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256)", default_size)] 43 | TypeInt, 44 | 45 | #[token("string")] 46 | TypeString, 47 | 48 | // 49 | #[token("address")] 50 | TypeAddress, 51 | 52 | // 53 | #[regex("[0-9]+")] 54 | LiteralInteger, 55 | 56 | #[token("[")] 57 | BracketOpen, 58 | 59 | // 60 | #[token("]")] 61 | BracketClose, 62 | } 63 | 64 | fn validate_bytes(lex: &mut Lexer) { 65 | let slice = lex.slice().as_bytes(); 66 | 67 | if slice.len() > 5 { 68 | lex.extras.0 = slice[5] - b'0'; 69 | 70 | if let Some(byte) = slice.get(6) { 71 | lex.extras.0 = lex.extras.0 * 10 + (byte - b'0'); 72 | } 73 | } else { 74 | lex.extras.0 = 1; 75 | } 76 | } 77 | 78 | fn default_size(lex: &mut Lexer) { 79 | lex.extras.0 = 32; 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/eip712/mod.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use regex::Regex; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::collections::HashMap; 6 | use validator::{Validate, ValidationError, ValidationErrors}; 7 | use web3::ethabi::ethereum_types::{Address, H256, U256}; 8 | 9 | pub(crate) type MessageTypes = HashMap>; 10 | 11 | lazy_static! { 12 | // match solidity identifier with the addition of '[(\d)*]*' 13 | static ref TYPE_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9]*(\[([1-9]\d*)*\])*$").unwrap(); 14 | static ref IDENT_REGEX: Regex = Regex::new(r"^[a-zA-Z_$][a-zA-Z_$0-9 ]*$").unwrap(); 15 | } 16 | 17 | #[derive(Deserialize, Serialize, Debug, Clone)] 18 | #[serde(rename_all = "camelCase")] 19 | pub(crate) struct EIP712Domain { 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub(crate) name: Option, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub(crate) version: Option, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub(crate) chain_id: Option, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub(crate) verifying_contract: Option
, 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub(crate) salt: Option, 30 | } 31 | 32 | fn validate_domain(domain: &EIP712Domain) -> Result<(), ValidationError> { 33 | match ( 34 | domain.name.as_ref(), 35 | domain.version.as_ref(), 36 | domain.chain_id, 37 | domain.verifying_contract, 38 | domain.salt, 39 | ) { 40 | (None, None, None, None, None) => Err(ValidationError::new( 41 | "EIP712Domain must include at least one field", 42 | )), 43 | _ => Ok(()), 44 | } 45 | } 46 | 47 | /// EIP-712 struct 48 | #[derive(Deserialize, Debug, Clone)] 49 | #[serde(rename_all = "camelCase")] 50 | #[serde(deny_unknown_fields)] 51 | pub struct EIP712 { 52 | pub(crate) types: MessageTypes, 53 | pub(crate) primary_type: String, 54 | pub(crate) message: Value, 55 | pub(crate) domain: EIP712Domain, 56 | } 57 | 58 | impl Validate for EIP712 { 59 | fn validate(&self) -> Result<(), ValidationErrors> { 60 | if let Err(err) = validate_domain(&self.domain) { 61 | let mut errors = ValidationErrors::new(); 62 | errors.add("domain", err); 63 | return Err(errors); 64 | } 65 | for field_types in self.types.values() { 66 | for field_type in field_types { 67 | field_type.validate().map_err(|err| { 68 | dbg!(err.to_string()); 69 | err 70 | })?; 71 | } 72 | } 73 | Ok(()) 74 | } 75 | } 76 | 77 | #[derive(Validate, Serialize, Deserialize, Debug, Clone)] 78 | pub(crate) struct FieldType { 79 | #[validate(regex = "IDENT_REGEX")] 80 | pub name: String, 81 | #[serde(rename = "type")] 82 | #[validate(regex = "TYPE_REGEX")] 83 | pub type_: String, 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use super::*; 89 | use serde_json::from_str; 90 | 91 | #[test] 92 | fn test_regex() { 93 | let test_cases = vec![ 94 | "unint bytes32", 95 | "Seun\\[]", 96 | "byte[]uint", 97 | "byte[7[]uint][]", 98 | "Person[0]", 99 | ]; 100 | for case in test_cases { 101 | assert_eq!(TYPE_REGEX.is_match(case), false) 102 | } 103 | 104 | let test_cases = vec![ 105 | "bytes32", 106 | "Foo[]", 107 | "bytes1", 108 | "bytes32[][]", 109 | "byte[9]", 110 | "contents", 111 | ]; 112 | for case in test_cases { 113 | assert_eq!(TYPE_REGEX.is_match(case), true) 114 | } 115 | } 116 | 117 | #[test] 118 | fn test_deserialization() { 119 | let string = r#"{ 120 | "primaryType": "Mail", 121 | "domain": { 122 | "name": "Ether Mail", 123 | "version": "1", 124 | "chainId": "0x1", 125 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 126 | }, 127 | "message": { 128 | "from": { 129 | "name": "Cow", 130 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 131 | }, 132 | "to": { 133 | "name": "Bob", 134 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 135 | }, 136 | "contents": "Hello, Bob!" 137 | }, 138 | "types": { 139 | "EIP712Domain": [ 140 | { "name": "name", "type": "string" }, 141 | { "name": "version", "type": "string" }, 142 | { "name": "chainId", "type": "uint256" }, 143 | { "name": "verifyingContract", "type": "address" } 144 | ], 145 | "Person": [ 146 | { "name": "name", "type": "string" }, 147 | { "name": "wallet", "type": "address" } 148 | ], 149 | "Mail": [ 150 | { "name": "from", "type": "Person" }, 151 | { "name": "to", "type": "Person" }, 152 | { "name": "contents", "type": "string" } 153 | ] 154 | } 155 | }"#; 156 | let _ = from_str::(string).unwrap(); 157 | } 158 | 159 | #[test] 160 | fn test_failing_deserialization() { 161 | let string = r#"{ 162 | "primaryType": "Mail", 163 | "domain": { 164 | "name": "Ether Mail", 165 | "version": "1", 166 | "chainId": "0x1", 167 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" 168 | }, 169 | "message": { 170 | "from": { 171 | "name": "Cow", 172 | "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" 173 | }, 174 | "to": { 175 | "name": "Bob", 176 | "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" 177 | }, 178 | "contents": "Hello, Bob!" 179 | }, 180 | "types": { 181 | "EIP712Domain": [ 182 | { "name": "name", "type": "string" }, 183 | { "name": "version", "type": "string" }, 184 | { "name": "chainId", "type": "7uint256[x] Seun" }, 185 | { "name": "verifyingContract", "type": "address" }, 186 | { "name": "salt", "type": "bytes32" } 187 | ], 188 | "Person": [ 189 | { "name": "name", "type": "string" }, 190 | { "name": "wallet amen", "type": "address" } 191 | ], 192 | "Mail": [ 193 | { "name": "from", "type": "Person" }, 194 | { "name": "to", "type": "Person" }, 195 | { "name": "contents", "type": "string" } 196 | ] 197 | } 198 | }"#; 199 | let data = from_str::(string).unwrap(); 200 | assert_eq!(data.validate().is_err(), true); 201 | } 202 | 203 | #[test] 204 | fn test_valid_domain() { 205 | let string = r#"{ 206 | "primaryType": "Test", 207 | "domain": { 208 | "name": "Ether Mail", 209 | "version": "1", 210 | "chainId": "0x1", 211 | "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", 212 | "salt": "0x0000000000000000000000000000000000000000000000000000000000000001" 213 | }, 214 | "message": { 215 | "test": "It works!" 216 | }, 217 | "types": { 218 | "EIP712Domain": [ 219 | { "name": "name", "type": "string" }, 220 | { "name": "version", "type": "string" }, 221 | { "name": "chainId", "type": "uint256" }, 222 | { "name": "verifyingContract", "type": "address" }, 223 | { "name": "salt", "type": "bytes32" } 224 | ], 225 | "Test": [ 226 | { "name": "test", "type": "string" } 227 | ] 228 | } 229 | }"#; 230 | let data = from_str::(string).unwrap(); 231 | assert_eq!(data.validate().is_err(), false); 232 | } 233 | 234 | #[test] 235 | fn domain_needs_at_least_one_field() { 236 | let string = r#"{ 237 | "primaryType": "Test", 238 | "domain": {}, 239 | "message": { 240 | "test": "It works!" 241 | }, 242 | "types": { 243 | "EIP712Domain": [ 244 | { "name": "name", "type": "string" }, 245 | { "name": "version", "type": "string" }, 246 | { "name": "chainId", "type": "uint256" }, 247 | { "name": "verifyingContract", "type": "address" } 248 | ], 249 | "Test": [ 250 | { "name": "test", "type": "string" } 251 | ] 252 | } 253 | }"#; 254 | let data = from_str::(string).unwrap(); 255 | assert_eq!(data.validate().is_err(), true); 256 | } 257 | } 258 | 259 | mod encode; 260 | mod error; 261 | mod lexer; 262 | mod parser; 263 | 264 | pub use encode::hash_structured_data; 265 | pub use error::Eip712Error; 266 | -------------------------------------------------------------------------------- /src/utils/eip712/parser.rs: -------------------------------------------------------------------------------- 1 | use logos::Logos; 2 | 3 | use crate::utils::eip712::{error::Eip712Error, lexer::Token}; 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub enum Type { 7 | Address, 8 | Uint, 9 | Int, 10 | String, 11 | Bool, 12 | Bytes, 13 | Byte(u8), 14 | Custom(String), 15 | Array { 16 | length: Option, 17 | inner: Box, 18 | }, 19 | } 20 | 21 | impl ToString for Type { 22 | fn to_string(&self) -> String { 23 | match self { 24 | Type::Address => "address".to_owned(), 25 | Type::Uint => "uint".to_owned(), 26 | Type::Int => "int".to_owned(), 27 | Type::String => "string".to_owned(), 28 | Type::Bool => "bool".to_owned(), 29 | Type::Bytes => "bytes".to_owned(), 30 | Type::Byte(len) => format!("bytes{}", len), 31 | Type::Custom(custom) => custom.to_string(), 32 | Type::Array { inner, length } => { 33 | let inner: String = (*inner).to_string(); 34 | match length { 35 | None => format!("{}[]", inner), 36 | Some(length) => format!("{}[{}]", inner, length), 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | /// the type string is being validated before it's parsed. 44 | pub fn parse_type(field_type: &str) -> Result { 45 | #[derive(PartialEq)] 46 | enum State { 47 | Open, 48 | Close, 49 | } 50 | 51 | let mut lexer = Token::lexer(field_type); 52 | let mut token = None; 53 | let mut state = State::Close; 54 | let mut array_depth = 0; 55 | let mut current_array_length: Option = None; 56 | 57 | while let Some(result) = lexer.next() { 58 | if let Ok(lex_token) = result { 59 | let type_: Type = match lex_token { 60 | Token::Identifier => Type::Custom(lexer.slice().to_owned()), 61 | Token::TypeByte => Type::Byte(lexer.extras.0), 62 | Token::TypeBytes => Type::Bytes, 63 | Token::TypeBool => Type::Bool, 64 | Token::TypeUint => Type::Uint, 65 | Token::TypeInt => Type::Int, 66 | Token::TypeString => Type::String, 67 | Token::TypeAddress => Type::Address, 68 | Token::LiteralInteger => { 69 | let length = lexer.slice(); 70 | current_array_length = Some( 71 | length 72 | .parse() 73 | .map_err(|_| Eip712Error::InvalidArraySize(length.into()))?, 74 | ); 75 | continue; 76 | } 77 | Token::BracketOpen if state == State::Close => { 78 | state = State::Open; 79 | continue; 80 | } 81 | Token::BracketClose if array_depth < 10 => { 82 | if state == State::Open { 83 | let length = current_array_length.take(); 84 | state = State::Close; 85 | token = Some(Type::Array { 86 | inner: Box::new(token.expect("if statement checks for some; qed")), 87 | length, 88 | }); 89 | array_depth += 1; 90 | continue; 91 | } else { 92 | return Err(Eip712Error::UnexpectedToken( 93 | lexer.slice().to_owned(), 94 | field_type.to_owned(), 95 | ))?; 96 | } 97 | } 98 | Token::BracketClose if array_depth == 10 => { 99 | return Err(Eip712Error::UnsupportedArrayDepth)?; 100 | } 101 | _ => { 102 | return Err(Eip712Error::UnexpectedToken( 103 | lexer.slice().to_owned(), 104 | field_type.to_owned(), 105 | ))? 106 | } 107 | }; 108 | token = Some(type_); 109 | } 110 | } 111 | 112 | token.ok_or(Eip712Error::NonExistentType) 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | 119 | #[test] 120 | fn test_parser() { 121 | let source = "bytes1[][][7][][][][][][][]"; 122 | let ok = parse_type(source).unwrap(); 123 | println!("{:?}", ok); 124 | assert!( 125 | ok == Type::Array { 126 | length: None, 127 | inner: Box::new(Type::Array { 128 | length: None, 129 | inner: Box::new(Type::Array { 130 | length: None, 131 | inner: Box::new(Type::Array { 132 | length: None, 133 | inner: Box::new(Type::Array { 134 | length: None, 135 | inner: Box::new(Type::Array { 136 | length: None, 137 | inner: Box::new(Type::Array { 138 | length: None, 139 | inner: Box::new(Type::Array { 140 | length: Some(7), 141 | inner: Box::new(Type::Array { 142 | length: None, 143 | inner: Box::new(Type::Array { 144 | length: None, 145 | inner: Box::new(Type::Byte(1)) 146 | }) 147 | }) 148 | }) 149 | }) 150 | }) 151 | }) 152 | }) 153 | }) 154 | }) 155 | } 156 | ); 157 | } 158 | 159 | #[test] 160 | fn test_nested_array() { 161 | let source = "bytes1[][][7][][][][][][][][]"; 162 | assert!(parse_type(source).is_err()); 163 | let source = "byte1[][][7][][][][][][][][]"; 164 | assert!(parse_type(source).is_err()); 165 | let source = "byte[][][7][][][][][][][][]"; 166 | assert!(parse_type(source).is_err()); 167 | } 168 | 169 | #[test] 170 | fn test_malformed_array_type() { 171 | let source = "bytes1[7[]uint][]"; 172 | assert!(parse_type(source).is_err()); 173 | let source = "byte[7[]uint][]"; 174 | assert!(parse_type(source).is_err()); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "ethereum", feature = "erc20"))] 2 | mod eip712; 3 | 4 | pub(crate) use eip712::hash_structured_data; 5 | pub(crate) use eip712::Eip712Error; 6 | pub(crate) use eip712::EIP712; 7 | 8 | use std::{ 9 | fs::File, 10 | io::{Read, Seek, SeekFrom}, 11 | }; 12 | 13 | use bytes::Bytes; 14 | use reqwest::{Response, Url}; 15 | use serde::Deserialize; 16 | 17 | use crate::error::BundlerError; 18 | 19 | pub async fn check_and_return Deserialize<'de>>( 20 | res: Result, 21 | ) -> Result 22 | where 23 | T: Default, 24 | { 25 | match res { 26 | Ok(r) => { 27 | if !r.status().is_success() { 28 | let status = r.status(); 29 | let text = r 30 | .text() 31 | .await 32 | .map_err(|err| BundlerError::ParseError(err.to_string()))? 33 | .replace('\"', ""); 34 | let msg = format!("Status: {}:{:?}", status, text); 35 | return Err(BundlerError::ResponseError(msg)); 36 | }; 37 | Ok(r.json::().await.unwrap_or_default()) 38 | } 39 | Err(err) => Err(BundlerError::ResponseError(err.to_string())), 40 | } 41 | } 42 | 43 | pub async fn get_nonce( 44 | client: &reqwest::Client, 45 | url: &Url, 46 | address: String, 47 | token: String, 48 | ) -> Result { 49 | let res = client 50 | .get( 51 | url.join(&format!( 52 | "/account/withdrawals/{}?address={}", 53 | token, address 54 | )) 55 | .map_err(|err| BundlerError::ParseError(err.to_string()))?, 56 | ) 57 | .send() 58 | .await; 59 | check_and_return::(res).await 60 | } 61 | 62 | // Reads `length` bytes at `offset` within `file` 63 | #[allow(clippy::uninit_vec)] 64 | #[allow(clippy::unused_io_amount)] 65 | pub fn read_offset(file: &mut File, offset: u64, length: usize) -> Result { 66 | let mut b = Vec::with_capacity(length); 67 | unsafe { b.set_len(length) }; 68 | file.seek(SeekFrom::Start(offset))?; 69 | 70 | b.fill(0); 71 | 72 | file.read(&mut b)?; 73 | Ok(b.into()) 74 | } 75 | -------------------------------------------------------------------------------- /src/verify/file.rs: -------------------------------------------------------------------------------- 1 | use super::types::{Header, Item}; 2 | use crate::error::BundlerError; 3 | use crate::utils::read_offset; 4 | use crate::BundlerTx; 5 | use data_encoding::BASE64URL; 6 | use primitive_types::U256; 7 | use std::{cmp, fs::File}; 8 | 9 | impl From for BundlerError { 10 | fn from(e: std::io::Error) -> Self { 11 | BundlerError::FsError(e.to_string()) 12 | } 13 | } 14 | 15 | pub async fn verify_file_bundle(filename: String) -> Result, BundlerError> { 16 | let mut file = File::open(&filename)?; 17 | 18 | let bundle_length = U256::from_little_endian(&read_offset(&mut file, 0, 32)?).as_u64(); 19 | 20 | // NOTE THIS IS UNSAFE BEYOND USIZE LIMIT 21 | let header_bytes = read_offset(&mut file, 32, bundle_length as usize * 64)?; 22 | // This will use ~100 bytes per header. So 1 GB is 1e+7 headers 23 | let mut headers = Vec::with_capacity(cmp::min(bundle_length as usize, 1000)); 24 | 25 | for i in (0..(64 26 | * usize::try_from(bundle_length) 27 | .map_err(|err| BundlerError::TypeParseError(err.to_string()))?)) 28 | .step_by(64) 29 | { 30 | let h = Header( 31 | U256::from_little_endian(&header_bytes[i..i + 32]).as_u64(), 32 | BASE64URL.encode(&header_bytes[i + 32..i + 64]), 33 | ); 34 | headers.push(h); 35 | } 36 | 37 | let mut offset = 32 + (64 * bundle_length); 38 | let mut items = Vec::with_capacity(cmp::min(bundle_length as usize, 1000)); 39 | 40 | for Header(size, id) in headers { 41 | // Read 4 KiB - max data-less bundler tx 42 | // We do it all at once to improve performance - by lowering fs ops and doing ops in memory 43 | let mut tx = BundlerTx::from_file_position(&mut file, size, offset, 4096)?; 44 | 45 | match tx.verify().await { 46 | Err(err) => return Err(err), 47 | Ok(_) => { 48 | let sig = tx.get_signarure(); 49 | let item = Item { 50 | tx_id: id, 51 | signature: sig, 52 | }; 53 | items.push(item); 54 | offset += size; 55 | } 56 | } 57 | } 58 | 59 | Ok(items) 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use crate::error::BundlerError; 65 | 66 | use super::verify_file_bundle; 67 | 68 | #[tokio::test] 69 | async fn should_verify_test_bundle() -> Result<(), BundlerError> { 70 | verify_file_bundle("./res/test_bundles/test_bundle".to_string()) 71 | .await 72 | .map(|_| ()) 73 | } 74 | 75 | #[tokio::test] 76 | async fn should_verify_arweave() -> Result<(), BundlerError> { 77 | verify_file_bundle("./res/test_bundles/arweave_sig".to_string()) 78 | .await 79 | .map(|_| ()) 80 | } 81 | 82 | #[tokio::test] 83 | async fn should_verify_secp256k1() -> Result<(), BundlerError> { 84 | verify_file_bundle("./res/test_bundles/ethereum_sig".to_string()).await?; 85 | verify_file_bundle("./res/test_bundles/typedethereum_sig".to_string()).await?; 86 | verify_file_bundle("./res/test_bundles/arbitrum_sig".to_string()).await?; 87 | verify_file_bundle("./res/test_bundles/avalanche_sig".to_string()).await?; 88 | verify_file_bundle("./res/test_bundles/bnb_sig".to_string()).await?; 89 | verify_file_bundle("./res/test_bundles/boba-eth_sig".to_string()).await?; 90 | verify_file_bundle("./res/test_bundles/chainlink_sig".to_string()).await?; 91 | verify_file_bundle("./res/test_bundles/kyve_sig".to_string()).await?; 92 | verify_file_bundle("./res/test_bundles/matic_sig".to_string()).await?; 93 | Ok(()) 94 | } 95 | 96 | /* 97 | #[tokio::test] 98 | #[cfg(feature = "cosmos")] 99 | async fn should_verify_cosmos() { 100 | //TODO: update cosmos signed transaction when its constant is defined 101 | assert!( 102 | verify_file_bundle("./res/test_bundles/cosmos_sig".to_string()) 103 | .await 104 | .is_ok() 105 | ); 106 | } 107 | */ 108 | 109 | #[tokio::test] 110 | async fn should_verify_ed25519() -> Result<(), BundlerError> { 111 | verify_file_bundle("./res/test_bundles/solana_sig".to_string()).await?; 112 | verify_file_bundle("./res/test_bundles/algorand_sig".to_string()).await?; 113 | verify_file_bundle("./res/test_bundles/near_sig".to_string()).await?; 114 | verify_file_bundle("./res/test_bundles/aptos_sig".to_string()).await?; 115 | verify_file_bundle("./res/test_bundles/aptos_multisig".to_string()).await?; 116 | Ok(()) 117 | } 118 | 119 | #[tokio::test] 120 | async fn should_verify_random_bundles() -> Result<(), BundlerError> { 121 | for i in 1..100 { 122 | verify_file_bundle(format!("./res/gen_bundles/bundle_{}", i).to_string()).await?; 123 | } 124 | Ok(()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/verify/mod.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | 3 | use crate::error::BundlerError; 4 | 5 | pub mod file; 6 | pub mod types; 7 | 8 | pub trait Verifier 9 | where 10 | Self: Sized, 11 | { 12 | fn verify(pk: Bytes, message: Bytes, signature: Bytes) -> Result<(), BundlerError>; 13 | } 14 | -------------------------------------------------------------------------------- /src/verify/stream.rs: -------------------------------------------------------------------------------- 1 | use core::slice::SlicePattern; 2 | use std::{any::TypeId, cmp, ops::Sub, rc::Rc, vec}; 3 | 4 | use async_stream::stream; 5 | use bytes::{Buf, BufMut, Bytes, BytesMut}; 6 | use data_encoding::BASE64URL; 7 | use derive_more::{Display, Error}; 8 | use futures::stream::TryStreamExt; 9 | use futures::Stream; 10 | use num_traits::FromPrimitive; 11 | use primitive_types::U256; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | use crate::{error::BundleError, index::SignerMap, tags::AvroDecode}; 15 | 16 | async fn verify_and_index_stream( 17 | mut s: impl Stream> + Unpin, 18 | ) -> Result, BundleError> { 19 | // Assume average number of items to be 500 20 | let mut header_bytes = BytesMut::with_capacity(32 + (64 * 500)); 21 | 22 | // Read first 32 bytes for item count 23 | read(&mut header_bytes, 32, &mut s).await?; 24 | 25 | // TODO: Test this for max val 26 | let length = U256::from_little_endian(&header_bytes[0..32]).as_usize(); 27 | 28 | header_bytes.advance(32); 29 | 30 | // Read header bytes 31 | read(&mut header_bytes, 64 * length, &mut s).await?; 32 | 33 | let mut headers = Vec::with_capacity(cmp::min(length, 1000)); 34 | 35 | for i in 0..length { 36 | let start = 64 * i; 37 | let size = U256::from_little_endian(&header_bytes[start..(start + 32)]); 38 | let id = BASE64URL.encode(&header_bytes[(start + 32)..(start + 64)]); 39 | headers.push(Header(size, id)); 40 | } 41 | 42 | let mut item_bytes = BytesMut::from(&header_bytes[32 + (length * 64)..]); 43 | 44 | // Free header bytes 45 | drop(header_bytes); 46 | 47 | let mut items = Vec::with_capacity(cmp::min(length, 1000)); 48 | 49 | for Header(size, id) in headers { 50 | // Get sig type 51 | read(&mut item_bytes, 2, &mut s).await?; 52 | let signature_type = u16::from_le_bytes(item_bytes[0..2].try_into()?); 53 | 54 | let signer: SignerMap = SignerMap::from_u16(signature_type)?; 55 | let signer_config = signer.get_config(); 56 | item_bytes.advance(2); 57 | 58 | // Get sig 59 | read(&mut item_bytes, signer_config.sig_length.into(), &mut s).await?; 60 | let signature = &item_bytes[..signer_config.sig_length.into()]; 61 | item_bytes.advance(signer_config.sig_length.into()); 62 | 63 | // Get pub 64 | read(&mut item_bytes, signer_config.pub_length.into(), &mut s).await?; 65 | let public = &item_bytes[..signer_config.pub_length.into()]; 66 | item_bytes.advance(signer_config.pub_length.into()); 67 | 68 | // Get tags 69 | read(&mut item_bytes, 16, &mut s).await?; 70 | let number_of_tags = u8::from_le_bytes(item_bytes[0..8].try_into()?); 71 | let number_of_tags_bytes = u16::from_le_bytes(item_bytes[8..16].try_into()?); 72 | item_bytes.advance(16); 73 | 74 | let tags = (&item_bytes[..number_of_tags_bytes as usize]).decode()?; 75 | if tags.len() != number_of_tags as usize { 76 | return Err(BundleError::InvalidTagEncoding); 77 | } 78 | 79 | let non_data_size = 2 + signer_config.total_length() + 16 + number_of_tags_bytes as u32; 80 | item_bytes.advance(non_data_size.try_into()?); 81 | 82 | let data_size = size.sub(non_data_size); 83 | 84 | let data_stream = stream! { 85 | let data_count = U256::zero(); 86 | while (data_count < data_size) { 87 | match s.try_next().await.map_err(|_| BundleError::NoBytesLeft)? { 88 | Some(b) => yield Ok(b), 89 | None => { 90 | yield Err(BundleError::NoBytesLeft); 91 | return (); 92 | } 93 | }; 94 | }; 95 | 96 | if data_size > data_count { 97 | println!("{}", "Bad sizes"); 98 | }; 99 | 100 | item_bytes.advance((data_count - data_size).as_usize()); 101 | }; 102 | 103 | let item = Item { 104 | id: "id".to_string(), 105 | }; 106 | 107 | items.push(item); 108 | } 109 | 110 | Ok(vec![]) 111 | } 112 | 113 | async fn read( 114 | b: &mut BytesMut, 115 | len: usize, 116 | mut s: impl Stream> + Unpin, 117 | ) -> Result<(), BundleError> { 118 | if b.len() >= len { 119 | return Ok(()); 120 | }; 121 | 122 | while b.len() < len { 123 | let next = &s.try_next().await; 124 | let new_bytes = match next.as_ref().map_err(|_| BundleError::NoBytesLeft)? { 125 | Some(bytess) => bytess, 126 | None => return Err(BundleError::NoBytesLeft), 127 | }; 128 | 129 | b.extend(new_bytes); 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | async fn produce_data_stream( 136 | mut s: impl Stream> + Unpin, 137 | ) -> Result<(), BundleError> { 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use crate::stream::verify_and_index_stream; 143 | 144 | #[actix_web::test] 145 | async fn test() { 146 | // let client = awc::Client::default(); 147 | // let stream = client 148 | // .get("https://google.com") 149 | // .send() 150 | // .await.unwrap(); 151 | 152 | // assert!(verify_and_index_stream(stream).await.is_err()); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/verify/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | pub struct Item { 5 | pub tx_id: String, 6 | pub signature: Vec, 7 | } 8 | 9 | pub struct Header(pub u64, pub String); 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "ESNext", 5 | "moduleResolution": "nodenext", 6 | "skipLibCheck": true, 7 | "allowSyntheticDefaultImports": true, 8 | "allowJs": true, 9 | "importHelpers": true, 10 | "declaration": false, 11 | "outDir": "dist", 12 | "lib": [ 13 | "esnext", 14 | "es2019", 15 | "DOM" 16 | ], 17 | "alwaysStrict": true, 18 | "sourceMap": true, 19 | "strictNullChecks": false, 20 | "experimentalDecorators": true, 21 | "strict": true, 22 | "resolveJsonModule": true, 23 | "esModuleInterop": true, 24 | }, 25 | } 26 | --------------------------------------------------------------------------------