From 6a9589b2a240f0d43be62b811bc7a9aafa63f0ce Mon Sep 17 00:00:00 2001 From: LimpidCrypto <97235361+LimpidCrypto@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:00:10 +0200 Subject: [PATCH] Transaction signing (#71) * move BinarySerializer and BinaryParser * add STObject * current state of txn signing * current state of sign * current state of sign * use String instead of Cow<'a, str> for structs using serde_with_tag macro due to lifetime issues when deserializing * utilize Cow for models * utilize Cow for models * cargo fmt * add simple test * add devcontainer * fix async client tests * fix github workflow * fix tests with --all-features * fix --no-default-features tests with embedded-ws * try fix colliding embebedded-websocket feature with dep: syntax * fix ambiguous name error * Revise Models (#74) * current state * initial * initial * add to changelog * impl LedgerObject trait * remove .DS_Store * add documentation * cargo fix * add Default trait to FlagCollection * refactoring of models * fix serde tests * fix serde tests * Delete src/models/.DS_Store * remove dafault_none * resolve comments * revise dependencies * remove old clients * add new clients * add codec for std embedded websocket * move websocket exceptions * add WebsocketBase * add Client and AsyncClient traits * add XRPLWebsocketIO as standardized trait * adjust clients mod * add XRPLResponse * revise the Request trait to have methods to get a resut models common fields * cargo fmt * add get_random_id utility function * add xrpl testnet uri * revise the tests to work with the changes * error handling * improve testing * implement websocket client request * implement websocket client request for embedded websocket * update rand trying to get rid of cargo check: OsRng: rand_core::RngCore is not satisfied error * fix github workflow build and tests * fix github workflow build and tests * run linters * add json rpc client * add json rpc for std and tests * cargo fmt * add pre-commit test with json-rpc-std * cargo fmt * cargo fix * improve client code * refactor features * fix tungstenite example * fix clients * fix client tests * fix tests * rename features in github workflow * add transaction auto filling as in #76 (#80) * current state of txn signing * current state of sign * current state of sign * add simple test * resolve errors caused by updating branch * resolve errors caused by updating branch * add autofill_and_sign * make tx signing work * improve sign test * handle unwraps * add autofill_and_sign * add sign and submit * fix errors caused by solving merge conflicts * rename SerializedDict and SerializedList to STObject and STArray * rename SerializedDict and SerializedList to STObject and STArray * handle unwraps * fix tests * fix tests * fix tests * add multisigning * add multisigning * fix tests * fix tests --------- Co-authored-by: LimpidCrypto --- Cargo.toml | 5 +- rustc-ice-2024-08-27T11_36_40-17775.txt | 59 ++ src/asynch/clients/client.rs | 15 +- src/asynch/clients/websocket/_no_std.rs | 10 +- src/asynch/clients/websocket/_std.rs | 5 +- src/asynch/transaction/exceptions.rs | 8 + src/asynch/transaction/mod.rs | 375 +++++++++- src/core/binarycodec/binary_wrappers.rs | 811 ++++++++++++++++++++ src/core/binarycodec/exceptions.rs | 3 + src/core/binarycodec/mod.rs | 855 ++-------------------- src/core/definitions/definitions.json | 61 +- src/core/definitions/mod.rs | 6 +- src/core/definitions/types.rs | 6 +- src/core/mod.rs | 6 +- src/core/test_data/data-driven-tests.json | 42 +- src/core/types/exceptions.rs | 28 + src/core/types/mod.rs | 461 +++++++++++- src/lib.rs | 2 + src/models/amount/xrp_amount.rs | 8 + src/models/results/mod.rs | 30 + src/models/results/submit.rs | 42 ++ src/models/transactions/exceptions.rs | 10 + src/models/transactions/mod.rs | 21 +- src/models/transactions/payment.rs | 2 +- src/transaction/exceptions.rs | 7 + src/transaction/mod.rs | 4 + src/transaction/multisign.rs | 80 ++ src/utils/mod.rs | 3 +- src/utils/transactions.rs | 83 +++ tests/common/mod.rs | 5 +- 30 files changed, 2153 insertions(+), 900 deletions(-) create mode 100644 rustc-ice-2024-08-27T11_36_40-17775.txt create mode 100644 src/core/binarycodec/binary_wrappers.rs create mode 100644 src/models/results/submit.rs create mode 100644 src/transaction/exceptions.rs create mode 100644 src/transaction/mod.rs create mode 100644 src/transaction/multisign.rs create mode 100644 src/utils/transactions.rs diff --git a/Cargo.toml b/Cargo.toml index 3f14a787..deda7588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,12 +64,13 @@ thiserror-no-std = "2.0.2" anyhow = { version = "1.0.69", default-features = false } # networking -url = { version = "2.2.1", default-features = false, optional = true } +url = { version = "2.2.2", default-features = false, optional = true } # websocket embassy-sync = { version = "0.6.0", optional = true } embedded-io-async = { version = "0.6.1", optional = true } embedded-websocket = { version = "0.9.3", default-features = false, optional = true } futures = { version = "0.3.30", optional = true } +rand_core = { version = "0.6.4", default-features = false } # websocket-codec bytes = { version = "1.7.1", default-features = false, optional = true } tokio-util = { version = "0.7.11", features = ["codec"], optional = true } @@ -90,6 +91,7 @@ reqwest = { version = "0.12.5", features = ["json"], optional = true } [dev-dependencies] criterion = "0.5.1" tokio = { version = "1.0", features = ["full"] } +tokio-util = { version = "0.7.11", features = ["codec"] } [[bench]] name = "benchmarks" @@ -114,6 +116,7 @@ helpers = ["account-helpers", "ledger-helpers", "transaction-helpers"] account-helpers = ["amounts", "currencies", "requests", "results"] ledger-helpers = ["amounts", "currencies", "requests", "results"] transaction-helpers = [ + "wallet", "amounts", "currencies", "requests", diff --git a/rustc-ice-2024-08-27T11_36_40-17775.txt b/rustc-ice-2024-08-27T11_36_40-17775.txt new file mode 100644 index 00000000..d13252de --- /dev/null +++ b/rustc-ice-2024-08-27T11_36_40-17775.txt @@ -0,0 +1,59 @@ +thread 'rustc' panicked at /rustc/80eb5a8e910e5185d47cdefe3732d839c78a5e7e/compiler/rustc_query_system/src/query/plumbing.rs:727:9: +Found unstable fingerprints for evaluate_obligation(9acaf53e42db4106-b70f9be4bd000590): Ok(EvaluatedToAmbig) +stack backtrace: + 0: 0x7f2385fa3de5 - std::backtrace::Backtrace::create::h5fc94655595d7e27 + 1: 0x7f2384710fe5 - std::backtrace::Backtrace::force_capture::h3ae8cd2c99c0732c + 2: 0x7f23838b111e - std[edc1ba278773f0d3]::panicking::update_hook::>::{closure#0} + 3: 0x7f2384728e27 - std::panicking::rust_panic_with_hook::h9e59a429b59b11d4 + 4: 0x7f2384728ae7 - std::panicking::begin_panic_handler::{{closure}}::h97d6e0922a833e87 + 5: 0x7f23847262e9 - std::sys::backtrace::__rust_end_short_backtrace::ha09625205ff6b122 + 6: 0x7f23847287b4 - rust_begin_unwind + 7: 0x7f2381922f53 - core::panicking::panic_fmt::h089121e4cbbc3220 + 8: 0x7f23841d9c41 - rustc_query_system[cb6aa6728468b543]::query::plumbing::incremental_verify_ich_failed:: + 9: 0x7f238558c6af - rustc_query_system[cb6aa6728468b543]::query::plumbing::try_execute_query::>, rustc_middle[354bbee2ce4c460]::query::erase::Erased<[u8; 2usize]>>, false, false, false>, rustc_query_impl[2dfeb81381ece16c]::plumbing::QueryCtxt, true> + 10: 0x7f238558ab62 - rustc_query_impl[2dfeb81381ece16c]::query_impl::evaluate_obligation::get_query_incr::__rust_end_short_backtrace + 11: 0x7f238246b63e - ::evaluate_obligation_no_overflow + 12: 0x7f2381749e92 - , ::consider_candidates::{closure#0}>, ::consider_candidates::{closure#1}> as core[8c2a069408211e69]::iter::traits::iterator::Iterator>::next + 13: 0x7f238552a7b4 - ::pick_method + 14: 0x7f2385529ed4 - ::pick_core + 15: 0x7f2382485c0e - ::lookup_probe + 16: 0x7f2385931fec - ::check_expr_with_expectation_and_args + 17: 0x7f23859345ae - ::check_expr_with_expectation_and_args + 18: 0x7f2385935734 - ::check_expr_with_expectation_and_args + 19: 0x7f2385935734 - ::check_expr_with_expectation_and_args + 20: 0x7f2385933b44 - ::check_expr_with_expectation_and_args + 21: 0x7f238593362e - ::check_expr_with_expectation_and_args + 22: 0x7f238592aa16 - ::check_block_with_expected + 23: 0x7f2385931ed3 - ::check_expr_with_expectation_and_args + 24: 0x7f238593376d - ::check_expr_with_expectation_and_args + 25: 0x7f238592aa16 - ::check_block_with_expected + 26: 0x7f2385931ed3 - ::check_expr_with_expectation_and_args + 27: 0x7f23851bac37 - rustc_hir_typeck[d8d14d8a3ea8603d]::check::check_fn + 28: 0x7f23852c6edf - rustc_hir_typeck[d8d14d8a3ea8603d]::typeck + 29: 0x7f23852c6933 - rustc_query_impl[2dfeb81381ece16c]::plumbing::__rust_begin_short_backtrace::> + 30: 0x7f2385251b4a - rustc_query_system[cb6aa6728468b543]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2dfeb81381ece16c]::plumbing::QueryCtxt, true> + 31: 0x7f2385334054 - rustc_query_impl[2dfeb81381ece16c]::query_impl::typeck::get_query_incr::__rust_end_short_backtrace + 32: 0x7f238524da5b - ::par_body_owners::::{closure#0} + 33: 0x7f238524b7a4 - rustc_hir_analysis[618f1997a162b265]::check_crate + 34: 0x7f238572bcbf - rustc_interface[fc21679c26087701]::passes::run_required_analyses + 35: 0x7f2385a97e5e - rustc_interface[fc21679c26087701]::passes::analysis + 36: 0x7f2385a97e31 - rustc_query_impl[2dfeb81381ece16c]::plumbing::__rust_begin_short_backtrace::> + 37: 0x7f2385f83c0d - rustc_query_system[cb6aa6728468b543]::query::plumbing::try_execute_query::>, false, false, false>, rustc_query_impl[2dfeb81381ece16c]::plumbing::QueryCtxt, true> + 38: 0x7f2385f838ba - rustc_query_impl[2dfeb81381ece16c]::query_impl::analysis::get_query_incr::__rust_end_short_backtrace + 39: 0x7f2385d12229 - rustc_interface[fc21679c26087701]::interface::run_compiler::, rustc_driver_impl[734e2c9770122f8]::run_compiler::{closure#0}>::{closure#1} + 40: 0x7f2385de6f44 - std[edc1ba278773f0d3]::sys::backtrace::__rust_begin_short_backtrace::, rustc_driver_impl[734e2c9770122f8]::run_compiler::{closure#0}>::{closure#1}, core[8c2a069408211e69]::result::Result<(), rustc_span[e1b931742f0b02c1]::ErrorGuaranteed>>::{closure#0}, core[8c2a069408211e69]::result::Result<(), rustc_span[e1b931742f0b02c1]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[8c2a069408211e69]::result::Result<(), rustc_span[e1b931742f0b02c1]::ErrorGuaranteed>> + 41: 0x7f2385de75b0 - <::spawn_unchecked_, rustc_driver_impl[734e2c9770122f8]::run_compiler::{closure#0}>::{closure#1}, core[8c2a069408211e69]::result::Result<(), rustc_span[e1b931742f0b02c1]::ErrorGuaranteed>>::{closure#0}, core[8c2a069408211e69]::result::Result<(), rustc_span[e1b931742f0b02c1]::ErrorGuaranteed>>::{closure#0}::{closure#0}, core[8c2a069408211e69]::result::Result<(), rustc_span[e1b931742f0b02c1]::ErrorGuaranteed>>::{closure#1} as core[8c2a069408211e69]::ops::function::FnOnce<()>>::call_once::{shim:vtable#0} + 42: 0x7f2385de792b - std::sys::pal::unix::thread::Thread::new::thread_start::he6ae6a1223d421a8 + 43: 0x7f2380330ea7 - start_thread + 44: 0x7f2380250a6f - clone + 45: 0x0 - + + +rustc version: 1.82.0-nightly (80eb5a8e9 2024-08-13) +platform: x86_64-unknown-linux-gnu + +query stack during panic: +#0 [evaluate_obligation] evaluating trait selection obligation `models::transactions::payment::Payment<'a>: models::transactions::Transaction<'b, ^1_2>` +#1 [typeck] type-checking `models::transactions::payment::::_get_partial_payment_error` +#2 [analysis] running analysis passes on this crate +end of query stack diff --git a/src/asynch/clients/client.rs b/src/asynch/clients/client.rs index ebbb609b..0fec4d50 100644 --- a/src/asynch/clients/client.rs +++ b/src/asynch/clients/client.rs @@ -4,27 +4,26 @@ use crate::models::{ }; #[cfg(feature = "std")] use crate::utils::get_random_id; -use alloc::borrow::Cow; +use alloc::borrow::{Cow, ToOwned}; use anyhow::Result; #[allow(async_fn_in_trait)] pub trait Client { async fn request_impl<'a: 'b, 'b>(&self, request: XRPLRequest<'a>) -> Result>; - fn set_request_id<'a: 'b, 'b>(&self, request: &mut XRPLRequest<'a>) -> Cow<'b, str> { - let common_fields = request.get_common_fields(); - let request_id: Cow<'_, str> = match common_fields.id.clone() { - Some(id) => id, + + fn set_request_id(&self, request: &mut XRPLRequest<'_>) -> () { + let common_fields = request.get_common_fields_mut(); + common_fields.id = match &common_fields.id { + Some(id) => Some(id.to_owned()), None => { #[cfg(feature = "std")] { let mut rng = rand::thread_rng(); - Cow::Owned(get_random_id(&mut rng)) + Some(Cow::Owned(get_random_id(&mut rng))) } #[cfg(not(feature = "std"))] unimplemented!("get_random_id is not yet implemented for no_std. Please provide an `id` in the request."); } }; - request.get_common_fields_mut().id = Some(request_id.clone()); - request_id } } diff --git a/src/asynch/clients/websocket/_no_std.rs b/src/asynch/clients/websocket/_no_std.rs index 7552b3a2..99bbc4d5 100644 --- a/src/asynch/clients/websocket/_no_std.rs +++ b/src/asynch/clients/websocket/_no_std.rs @@ -18,11 +18,14 @@ use embedded_websocket::{ }; use futures::Sink; use futures::Stream; -use rand::RngCore; +use rand_core::RngCore; use url::Url; use super::{WebsocketClosed, WebsocketOpen}; -use crate::{asynch::clients::SingleExecutorMutex, models::requests::XRPLRequest}; +use crate::{ + asynch::clients::SingleExecutorMutex, + models::requests::{Request, XRPLRequest}, +}; use crate::{ asynch::clients::{ client::Client as ClientTrait, @@ -247,7 +250,8 @@ where mut request: XRPLRequest<'a>, ) -> Result> { // setup request future - let request_id = self.set_request_id(&mut request); + self.set_request_id(&mut request); + let request_id = request.get_common_fields().id.as_ref().unwrap(); let mut websocket_base = self.websocket_base.lock().await; websocket_base .setup_request_future(request_id.to_string()) diff --git a/src/asynch/clients/websocket/_std.rs b/src/asynch/clients/websocket/_std.rs index dcb906ac..a2935fce 100644 --- a/src/asynch/clients/websocket/_std.rs +++ b/src/asynch/clients/websocket/_std.rs @@ -3,7 +3,7 @@ use super::{WebsocketClosed, WebsocketOpen}; use crate::asynch::clients::client::Client; use crate::asynch::clients::websocket::websocket_base::{MessageHandler, WebsocketBase}; use crate::asynch::clients::SingleExecutorMutex; -use crate::models::requests::XRPLRequest; +use crate::models::requests::{Request, XRPLRequest}; use crate::models::results::XRPLResponse; use crate::Err; @@ -204,7 +204,8 @@ where mut request: XRPLRequest<'a>, ) -> Result> { // setup request future - let request_id = self.set_request_id(&mut request); + self.set_request_id(&mut request); + let request_id = request.get_common_fields().id.as_ref().unwrap(); let mut websocket_base = self.websocket_base.lock().await; websocket_base .setup_request_future(request_id.to_string()) diff --git a/src/asynch/transaction/exceptions.rs b/src/asynch/transaction/exceptions.rs index 92e8d8f9..0b4456c3 100644 --- a/src/asynch/transaction/exceptions.rs +++ b/src/asynch/transaction/exceptions.rs @@ -14,3 +14,11 @@ pub enum XRPLTransactionException<'a> { #[error("Invalid rippled version: {0}")] InvalidRippledVersion(Cow<'a, str>), } + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLSignTransactionException<'a> { + #[error("{0:?} value does not match X-Address tag")] + TagFieldMismatch(&'a str), + #[error("Fee value of {0:?} is likely entered incorrectly, since it is much larger than the typical XRP transaction cost. If this is intentional, use `check_fee=Some(false)`.")] + FeeTooHigh(Cow<'a, str>), +} diff --git a/src/asynch/transaction/mod.rs b/src/asynch/transaction/mod.rs index 315e81aa..607c07dd 100644 --- a/src/asynch/transaction/mod.rs +++ b/src/asynch/transaction/mod.rs @@ -1,44 +1,59 @@ -use super::account::get_next_valid_seq_number; -use super::clients::AsyncClient; -use super::clients::CommonFields; -use super::ledger::get_fee; -use super::ledger::get_latest_validated_ledger_sequence; -use crate::models::amount::XRPAmount; -use crate::models::exceptions::XRPLModelException; -use crate::models::requests::ServerState; -use crate::models::results::ServerState as ServerStateResult; -use crate::models::transactions::Transaction; -use crate::models::transactions::TransactionType; -use crate::models::Model; -use crate::Err; -use alloc::borrow::Cow; +pub mod exceptions; + +use crate::{ + asynch::{ + account::get_next_valid_seq_number, + clients::{AsyncClient, CommonFields}, + ledger::{get_fee, get_latest_validated_ledger_sequence}, + transaction::exceptions::XRPLSignTransactionException, + }, + core::{ + addresscodec::{is_valid_xaddress, xaddress_to_classic_address}, + binarycodec::{encode, encode_for_multisigning, encode_for_signing}, + keypairs::sign as keypairs_sign, + }, + models::{ + amount::XRPAmount, + exceptions::XRPLModelException, + requests::{ServerState, Submit}, + results::{ServerState as ServerStateResult, Submit as SubmitResult}, + transactions::{Signer, Transaction, TransactionType, XRPLTransactionFieldException}, + Model, + }, + utils::transactions::{ + get_transaction_field_value, set_transaction_field_value, validate_transaction_has_field, + }, + wallet::Wallet, + Err, +}; + use alloc::string::String; use alloc::string::ToString; use alloc::vec::Vec; +use alloc::{borrow::Cow, vec}; use anyhow::Result; use core::convert::TryInto; use core::fmt::Debug; use exceptions::XRPLTransactionException; use rust_decimal::Decimal; use serde::Serialize; +use serde::{de::DeserializeOwned, Deserialize}; use strum::IntoEnumIterator; -pub mod exceptions; - const OWNER_RESERVE: &str = "2000000"; // 2 XRP const RESTRICTED_NETWORKS: u16 = 1024; const REQUIRED_NETWORKID_VERSION: &str = "1.11.0"; const LEDGER_OFFSET: u8 = 20; -pub async fn autofill<'a, 'b, F, T>( +pub async fn autofill<'a, 'b, F, T, C>( transaction: &mut T, - client: &'a impl AsyncClient, + client: &'b C, signers_count: Option, ) -> Result<()> where - 'a: 'b, - T: Transaction<'b, F> + Model + Clone, + T: Transaction<'a, F> + Model + Clone, F: IntoEnumIterator + Serialize + Debug + PartialEq, + C: AsyncClient, { let txn = transaction.clone(); let txn_common_fields = transaction.get_mut_common_fields(); @@ -52,7 +67,7 @@ where } if txn_common_fields.fee.is_none() { txn_common_fields.fee = - Some(calculate_fee_per_transaction_type(txn, Some(client), signers_count).await?); + Some(calculate_fee_per_transaction_type(&txn, Some(client), signers_count).await?); } if txn_common_fields.last_ledger_sequence.is_none() { let ledger_sequence = get_latest_validated_ledger_sequence(client).await?; @@ -62,15 +77,15 @@ where Ok(()) } -pub async fn calculate_fee_per_transaction_type<'a, 'b, T, F>( - transaction: T, - client: Option<&'a impl AsyncClient>, +pub async fn calculate_fee_per_transaction_type<'a, 'b, 'c, T, F, C>( + transaction: &T, + client: Option<&'b C>, signers_count: Option, -) -> Result> +) -> Result> where - 'a: 'b, - T: Transaction<'b, F>, + T: Transaction<'a, F>, F: IntoEnumIterator + Serialize + Debug + PartialEq, + C: AsyncClient, { let mut net_fee = XRPAmount::from("10"); let base_fee; @@ -122,10 +137,10 @@ async fn get_owner_reserve_from_response(client: &impl AsyncClient) -> Result( +fn calculate_base_fee_for_escrow_finish<'a: 'b, 'b>( net_fee: XRPAmount<'a>, fulfillment: Option>, -) -> Result> { +) -> Result> { if let Some(fulfillment) = fulfillment { calculate_based_on_fulfillment(fulfillment, net_fee) } else { @@ -239,6 +254,224 @@ fn is_not_later_rippled_version<'a>( } } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +enum AccountFieldType { + Account, + Destination, +} + +pub async fn sign_and_submit<'a, 'b, T, F, C>( + transaction: &mut T, + client: &'b C, + wallet: &Wallet, + autofill: bool, + check_fee: bool, +) -> Result> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Model + Serialize + DeserializeOwned + Clone + Debug, + C: AsyncClient, +{ + if autofill { + autofill_and_sign(transaction, client, wallet, check_fee).await?; + } else { + if check_fee { + check_txn_fee(transaction, client).await?; + } + sign(transaction, wallet, false)?; + } + submit(transaction, client).await +} + +pub fn sign<'a, T, F>(transaction: &mut T, wallet: &Wallet, multisign: bool) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone + Debug, +{ + if multisign { + let serialized_for_signing = + encode_for_multisigning(transaction, wallet.classic_address.clone().into())?; + let serialized_bytes = hex::decode(serialized_for_signing).unwrap(); + let signature = keypairs_sign(&serialized_bytes, &wallet.private_key).unwrap(); + let signer = Signer::new( + wallet.classic_address.clone().into(), + signature.into(), + wallet.public_key.clone().into(), + ); + transaction.get_mut_common_fields().signers = Some(vec![signer]); + + Ok(()) + } else { + prepare_transaction(transaction, wallet)?; + let serialized_for_signing = encode_for_signing(transaction)?; + let serialized_bytes = match hex::decode(serialized_for_signing) { + Ok(bytes) => bytes, + Err(e) => return Err!(e), + }; + let signature = match keypairs_sign(&serialized_bytes, &wallet.private_key) { + Ok(signature) => signature, + Err(e) => return Err!(e), + }; + transaction.get_mut_common_fields().txn_signature = Some(signature.into()); + + Ok(()) + } +} + +pub async fn autofill_and_sign<'a, 'b, T, F, C>( + transaction: &mut T, + client: &'b C, + wallet: &Wallet, + check_fee: bool, +) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Model + Serialize + DeserializeOwned + Clone + Debug, + C: AsyncClient, +{ + if check_fee { + check_txn_fee(transaction, client).await?; + } + autofill(transaction, client, None).await?; + sign(transaction, wallet, false)?; + + Ok(()) +} + +pub async fn submit<'a, T, F, C>(transaction: &T, client: &C) -> Result> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone + Debug, + C: AsyncClient, +{ + let txn_blob = encode(transaction)?; + let req = Submit::new(None, txn_blob.into(), None); + let res = client.request(req.into()).await?; + match res.try_into_result::>() { + Ok(value) => { + let submit_result = SubmitResult::from(value); + Ok(submit_result) + } + Err(e) => Err!(e), + } +} + +async fn check_txn_fee<'a, 'b, T, F, C>(transaction: &mut T, client: &'b C) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Model + Serialize + DeserializeOwned + Clone, + C: AsyncClient, +{ + // max of xrp_to_drops(0.1) and calculate_fee_per_transaction_type + let expected_fee = XRPAmount::from("100000") + .max(calculate_fee_per_transaction_type(transaction, Some(client), None).await?); + let transaction_fee = transaction + .get_common_fields() + .fee + .clone() + .unwrap_or(XRPAmount::from("0")); + if transaction_fee > expected_fee { + return Err!(XRPLSignTransactionException::FeeTooHigh( + transaction_fee.try_into()? + )); + } + Ok(()) +} + +fn prepare_transaction<'a, T, F>(transaction: &mut T, wallet: &Wallet) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone, +{ + let commond_fields = transaction.get_mut_common_fields(); + commond_fields.signing_pub_key = Some(wallet.public_key.clone().into()); + + validate_account_xaddress(transaction, AccountFieldType::Account)?; + if validate_transaction_has_field(transaction, "Destination").is_ok() { + validate_account_xaddress(transaction, AccountFieldType::Destination)?; + } + + let _ = convert_to_classic_address(transaction, "Unauthorize"); + let _ = convert_to_classic_address(transaction, "Authorize"); + // EscrowCancel, EscrowFinish + let _ = convert_to_classic_address(transaction, "Owner"); + // SetRegularKey + + let _ = convert_to_classic_address(transaction, "RegularKey"); + + Ok(()) +} + +fn validate_account_xaddress<'a, T, F>( + prepared_transaction: &mut T, + account_field: AccountFieldType, +) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone, +{ + let (account_field_name, tag_field_name) = match serde_json::to_string(&account_field) { + Ok(name) => { + let name_str = name.as_str().trim(); + if name_str == "\"Account\"" { + ("Account", "SourceTag") + } else if name_str == "\"Destination\"" { + ("Destination", "DestinationTag") + } else { + return Err!(XRPLTransactionFieldException::UnknownAccountField(name_str)); + } + } + Err(error) => return Err!(error), + }; + let account_address = match account_field { + AccountFieldType::Account => prepared_transaction.get_common_fields().account.clone(), + AccountFieldType::Destination => { + get_transaction_field_value(prepared_transaction, "Destination")? + } + }; + + if is_valid_xaddress(&account_address) { + let (address, tag, _) = match xaddress_to_classic_address(&account_address) { + Ok(t) => t, + Err(error) => return Err!(error), + }; + validate_transaction_has_field(prepared_transaction, &account_field_name)?; + set_transaction_field_value(prepared_transaction, &account_field_name, address)?; + + if validate_transaction_has_field(prepared_transaction, &tag_field_name).is_ok() + && get_transaction_field_value(prepared_transaction, &tag_field_name).unwrap_or(Some(0)) + != tag + { + Err!(XRPLSignTransactionException::TagFieldMismatch( + &tag_field_name + )) + } else { + set_transaction_field_value(prepared_transaction, &tag_field_name, tag)?; + + Ok(()) + } + } else { + Ok(()) + } +} + +fn convert_to_classic_address<'a, T, F>(transaction: &mut T, field_name: &str) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone, +{ + let address = get_transaction_field_value::(transaction, field_name)?; + if is_valid_xaddress(&address) { + let classic_address = match xaddress_to_classic_address(&address) { + Ok(t) => t.0, + Err(error) => return Err!(error), + }; + set_transaction_field_value(transaction, field_name, classic_address) + } else { + Ok(()) + } +} + #[cfg(all(feature = "websocket-std", feature = "std", not(feature = "websocket")))] #[cfg(test)] mod test_autofill { @@ -255,7 +488,7 @@ mod test_autofill { #[tokio::test] async fn test_autofill_txn() -> Result<()> { let mut txn = OfferCreate::new( - "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq".into(), + "r9mhdWo1NXVZr2pDnCtC1xwxE85kFtSzYR".into(), None, None, None, @@ -290,3 +523,85 @@ mod test_autofill { Ok(()) } } + +#[cfg(all(feature = "websocket-std", feature = "std", not(feature = "websocket")))] +#[cfg(test)] +mod test_sign { + use alloc::borrow::Cow; + + use crate::{ + asynch::{ + clients::{AsyncWebsocketClient, SingleExecutorMutex}, + transaction::{autofill_and_sign, sign}, + }, + models::transactions::{AccountSet, Transaction}, + wallet::Wallet, + }; + + #[test] + fn test_sign() { + let wallet = Wallet::new("sEdT7wHTCLzDG7ueaw4hroSTBvH7Mk5", 0).unwrap(); + let mut tx = AccountSet::new( + Cow::from(wallet.classic_address.clone()), + None, + Some("10".into()), + None, + None, + None, + Some(227234), + None, + None, + None, + None, + Some("6578616d706c652e636f6d".into()), // "example.com" + None, + None, + None, + None, + None, + None, + ); + sign(&mut tx, &wallet, false).unwrap(); + let expected_signature: Cow = + "B310792432B0242C2542C2B46CA234C87F4AE3FFC33226797AF72A92D9295ED20BD05A85D0\ + C13760B653AE9B8C0D74B9BBD310B09524F63B41D1776E7F2BB609" + .into(); + let actual_signature = tx.get_common_fields().txn_signature.as_ref().unwrap(); + assert_eq!(expected_signature, *actual_signature); + } + + #[tokio::test] + async fn test_autofill_and_sign() { + let wallet = Wallet::new("sEdT7wHTCLzDG7ueaw4hroSTBvH7Mk5", 0).unwrap(); + let mut tx = AccountSet::new( + Cow::from(wallet.classic_address.clone()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("6578616d706c652e636f6d".into()), // "example.com" + None, + None, + None, + None, + None, + None, + ); + let client = AsyncWebsocketClient::::open( + "wss://testnet.xrpl-labs.com/".parse().unwrap(), + ) + .await + .unwrap(); + autofill_and_sign(&mut tx, &client, &wallet, true) + .await + .unwrap(); + assert!(tx.get_common_fields().sequence.is_some()); + assert!(tx.get_common_fields().txn_signature.is_some()); + } +} diff --git a/src/core/binarycodec/binary_wrappers.rs b/src/core/binarycodec/binary_wrappers.rs new file mode 100644 index 00000000..d8085bd7 --- /dev/null +++ b/src/core/binarycodec/binary_wrappers.rs @@ -0,0 +1,811 @@ +use crate::core::binarycodec::exceptions::XRPLBinaryCodecException; +use crate::core::binarycodec::utils::*; +use crate::core::definitions::*; +use crate::core::types::TryFromParser; +use crate::utils::ToBytes; +use alloc::borrow::ToOwned; +use alloc::vec; +use alloc::vec::Vec; +use core::convert::TryFrom; +use core::convert::TryInto; + +/// Serializes JSON to XRPL binary format. +pub type BinarySerializer = Vec; + +/// Deserializes from hex-encoded XRPL binary format to +/// serde JSON fields and values. +/// +/// # Examples +/// +/// ## Basic usage +/// +/// ``` +/// use xrpl::core::binarycodec::BinaryParser; +/// use xrpl::core::Parser; +/// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; +/// +/// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; +/// let binary_parser: BinaryParser = BinaryParser::from(test_bytes); +/// +/// assert_eq!(binary_parser, test_bytes[..]); +/// ``` +#[derive(Debug, Clone)] +pub struct BinaryParser(Vec); + +/// Helper function for length-prefixed fields including +/// Blob types and some AccountID types. Calculates the +/// prefix of variable length bytes. +/// +/// The length of the prefix is 1-3 bytes depending on the +/// length of the contents: +/// Content length <= 192 bytes: prefix is 1 byte +/// 192 bytes < Content length <= 12480 bytes: prefix is 2 bytes +/// 12480 bytes < Content length <= 918744 bytes: prefix is 3 bytes +/// +/// See Length Prefixing: +/// `` +fn _encode_variable_length_prefix(length: &usize) -> Result, XRPLBinaryCodecException> { + if length <= &MAX_SINGLE_BYTE_LENGTH { + Ok([*length as u8].to_vec()) + } else if length < &MAX_DOUBLE_BYTE_LENGTH { + let mut bytes = vec![]; + let b_length = *length - (MAX_SINGLE_BYTE_LENGTH + 1); + let val_a: u8 = ((b_length >> 8) + (MAX_SINGLE_BYTE_LENGTH + 1)).try_into()?; + let val_b: u8 = (b_length & 0xFF).try_into()?; + + bytes.extend_from_slice(&[val_a]); + bytes.extend_from_slice(&[val_b]); + + Ok(bytes) + } else if length <= &MAX_LENGTH_VALUE { + let mut bytes = vec![]; + let b_length = *length - MAX_DOUBLE_BYTE_LENGTH; + let val_a: u8 = ((MAX_SECOND_BYTE_VALUE + 1) + (b_length >> 16)).try_into()?; + let val_b: u8 = ((b_length >> 8) & 0xFF).try_into()?; + let val_c: u8 = (b_length & 0xFF).try_into()?; + + bytes.extend_from_slice(&[val_a]); + bytes.extend_from_slice(&[val_b]); + bytes.extend_from_slice(&[val_c]); + + Ok(bytes) + } else { + Err(XRPLBinaryCodecException::InvalidVariableLengthTooLarge { + max: MAX_LENGTH_VALUE, + }) + } +} + +pub trait Parser { + /// Peek the first byte of the BinaryParser. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// + /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; + /// let binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// let first_byte: Option<[u8; 1]> = binary_parser.peek(); + /// + /// assert_eq!(Some([test_bytes[0]; 1]), first_byte); + /// ``` + fn peek(&self) -> Option<[u8; 1]>; + + /// Consume the first n bytes of the BinaryParser. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// + /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; + /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// + /// match binary_parser.skip_bytes(4) { + /// Ok(parser) => assert_eq!(*parser, test_bytes[4..]), + /// Err(e) => match e { + /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { + /// max: _, + /// found: _, + /// } => assert!(false), + /// _ => assert!(false) + /// } + /// } + /// ``` + fn skip_bytes(&mut self, n: usize) -> Result<&Self, XRPLBinaryCodecException>; + + /// Consume and return the first n bytes of the BinaryParser. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// + /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; + /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// + /// match binary_parser.read(5) { + /// Ok(data) => assert_eq!(test_bytes[..5], data), + /// Err(e) => match e { + /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { + /// max: _, + /// found: _, + /// } => assert!(false), + /// _ => assert!(false) + /// } + /// } + /// ``` + fn read(&mut self, n: usize) -> Result, XRPLBinaryCodecException>; + + /// Read 1 byte from parser and return as unsigned int. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// + /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; + /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// + /// match binary_parser.read_uint8() { + /// Ok(data) => assert_eq!(0, data), + /// Err(e) => match e { + /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { + /// max: _, + /// found: _, + /// } => assert!(false), + /// _ => assert!(false) + /// } + /// } + /// ``` + fn read_uint8(&mut self) -> Result; + + /// Read 2 bytes from parser and return as unsigned int. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// + /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; + /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// + /// match binary_parser.read_uint16() { + /// Ok(data) => assert_eq!(17, data), + /// Err(e) => match e { + /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { + /// max: _, + /// found: _, + /// } => assert!(false), + /// _ => assert!(false) + /// } + /// } + /// ``` + fn read_uint16(&mut self) -> Result; + + /// Read 4 bytes from parser and return as unsigned int. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// + /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; + /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// + /// match binary_parser.read_uint32() { + /// Ok(data) => assert_eq!(1122867, data), + /// Err(e) => match e { + /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { + /// max: _, + /// found: _, + /// } => assert!(false), + /// _ => assert!(false) + /// } + /// } + /// ``` + fn read_uint32(&mut self) -> Result; + + /// Returns whether the binary parser has finished + /// parsing (e.g. there is nothing left in the buffer + /// that needs to be processed). + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// extern crate alloc; + /// use alloc::vec; + /// + /// let empty: &[u8] = &[]; + /// let mut buffer: Vec = vec![]; + /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; + /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// + /// while !binary_parser.is_end(None) { + /// match binary_parser.read(1) { + /// Ok(data) => buffer.extend_from_slice(&data), + /// Err(e) => match e { + /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { + /// max: _, + /// found: _, + /// } => assert!(false), + /// _ => assert!(false) + /// } + /// } + /// } + /// + /// assert_eq!(test_bytes, &buffer[..]); + /// // The BinaryParser is emptied as it is read. + /// assert_eq!(binary_parser, empty[..]); + /// + /// ``` + fn is_end(&self, custom_end: Option) -> bool; + + /// Reads a variable length encoding prefix and returns + /// the encoded length. The formula for decoding a length + /// prefix is described in Length Prefixing. + /// + /// See Length Prefixing: + /// `` + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinaryParser; + /// use xrpl::core::Parser; + /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; + /// + /// let test_bytes: &[u8] = &[6, 17, 34, 51, 68, 85, 102]; + /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); + /// + /// match binary_parser.read_length_prefix() { + /// Ok(data) => assert_eq!(6, data), + /// Err(e) => match e { + /// XRPLBinaryCodecException::UnexpectedLengthPrefixRange { + /// min: _, max: _ + /// } => assert!(false), + /// _ => assert!(false) + /// } + /// } + fn read_length_prefix(&mut self) -> Result; + + /// Reads field ID from BinaryParser and returns as + /// a FieldHeader object. + fn read_field_header(&mut self) -> Result; + + /// Read the field ordinal at the head of the + /// BinaryParser and return a FieldInstance object + /// representing information about the field + /// containedin the following bytes. + fn read_field(&mut self) -> Result; + + /// Read next bytes from BinaryParser as the given type. + fn read_type(&mut self) -> Result; + + /// Read value of the type specified by field from + /// the BinaryParser. + fn read_field_value(&mut self, field: &FieldInstance) -> Result + where + T::Error: From; +} + +pub trait Serialization { + /// Write given bytes to this BinarySerializer. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinarySerializer; + /// use xrpl::core::binarycodec::Serialization; + /// + /// let mut test_bytes: Vec = [0, 17, 34, 51, 68, 85, 102].to_vec(); + /// let mut serializer: BinarySerializer = BinarySerializer::new(); + /// + /// serializer.append(&mut test_bytes.to_owned()); + /// assert_eq!(test_bytes, serializer); + /// ``` + fn append(&mut self, bytes: &[u8]) -> &Self; + + /// Write a variable length encoded value to + /// the BinarySerializer. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinarySerializer; + /// use xrpl::core::binarycodec::Serialization; + /// + /// let expected: Vec = [3, 0, 17, 34].to_vec(); + /// let mut test_bytes: Vec = [0, 17, 34].to_vec(); + /// let mut serializer: BinarySerializer = BinarySerializer::new(); + /// + /// serializer.write_length_encoded(&mut test_bytes, true); + /// assert_eq!(expected, serializer); + /// ``` + fn write_length_encoded(&mut self, value: &[u8], encode_value: bool) -> &Self; + + /// Write field and value to the buffer. + /// + /// # Examples + /// + /// ## Basic usage + /// + /// ``` + /// use xrpl::core::binarycodec::BinarySerializer; + /// use xrpl::core::binarycodec::Serialization; + /// use xrpl::core::definitions::FieldInstance; + /// use xrpl::core::definitions::FieldInfo; + /// use xrpl::core::definitions::FieldHeader; + /// + /// let field_header: FieldHeader = FieldHeader { + /// type_code: -2, + /// field_code: 0, + /// }; + /// + /// let field_info: FieldInfo = FieldInfo { + /// nth: 0, + /// is_vl_encoded: false, + /// is_serialized: false, + /// is_signing_field: false, + /// r#type: "Unknown".to_string(), + /// }; + /// + /// let field_instance = FieldInstance::new(&field_info, "Generic", field_header); + /// let expected: Vec = [224, 0, 17, 34].to_vec(); + /// let test_bytes: Vec = [0, 17, 34].to_vec(); + /// let mut serializer: BinarySerializer = BinarySerializer::new(); + /// + /// serializer.write_field_and_value(field_instance, &test_bytes, false); + /// assert_eq!(expected, serializer); + /// ``` + fn write_field_and_value( + &mut self, + field: FieldInstance, + value: &[u8], + is_unl_modify_workaround: bool, + ) -> &Self; +} + +impl Serialization for BinarySerializer { + fn append(&mut self, bytes: &[u8]) -> &Self { + self.extend_from_slice(bytes); + self + } + + fn write_length_encoded(&mut self, value: &[u8], encode_value: bool) -> &Self { + let mut byte_object: Vec = Vec::new(); + if encode_value { + // write value to byte_object + byte_object.extend_from_slice(value); + } + // TODO Handle unwrap better + let length_prefix = _encode_variable_length_prefix(&byte_object.len()).unwrap(); + + self.extend_from_slice(&length_prefix); + self.extend_from_slice(&byte_object); + + self + } + + fn write_field_and_value( + &mut self, + field: FieldInstance, + value: &[u8], + is_unl_modify_workaround: bool, + ) -> &Self { + self.extend_from_slice(&field.header.to_bytes()); + + if field.is_vl_encoded { + self.write_length_encoded(value, !is_unl_modify_workaround); + } else { + self.extend_from_slice(value); + } + + self + } +} + +/// Peek the first byte of the BinaryParser. +impl Parser for BinaryParser { + fn peek(&self) -> Option<[u8; 1]> { + if !self.0.is_empty() { + Some(self.0[0].to_be_bytes()) + } else { + None + } + } + + fn skip_bytes(&mut self, n: usize) -> Result<&Self, XRPLBinaryCodecException> { + if n > self.0.len() { + Err(XRPLBinaryCodecException::UnexpectedParserSkipOverflow { + max: self.0.len(), + found: n, + }) + } else { + self.0 = self.0[n..].to_vec(); + Ok(self) + } + } + + fn read(&mut self, n: usize) -> Result, XRPLBinaryCodecException> { + let first_n_bytes = self.0[..n].to_owned(); + + self.skip_bytes(n)?; + Ok(first_n_bytes) + } + + fn read_uint8(&mut self) -> Result { + let result = self.read(1)?; + Ok(u8::from_be_bytes(result.try_into().or(Err( + XRPLBinaryCodecException::InvalidReadFromBytesValue, + ))?)) + } + + fn read_uint16(&mut self) -> Result { + let result = self.read(2)?; + Ok(u16::from_be_bytes(result.try_into().or(Err( + XRPLBinaryCodecException::InvalidReadFromBytesValue, + ))?)) + } + + fn read_uint32(&mut self) -> Result { + let result = self.read(4)?; + Ok(u32::from_be_bytes(result.try_into().or(Err( + XRPLBinaryCodecException::InvalidReadFromBytesValue, + ))?)) + } + + fn is_end(&self, custom_end: Option) -> bool { + if let Some(end) = custom_end { + self.0.len() <= end + } else { + self.0.is_empty() + } + } + + fn read_length_prefix(&mut self) -> Result { + let byte1: usize = self.read_uint8()? as usize; + + match byte1 { + // If the field contains 0 to 192 bytes of data, + // the first byte defines the length of the contents. + x if x <= MAX_SINGLE_BYTE_LENGTH => Ok(byte1), + // If the field contains 193 to 12480 bytes of data, + // the first two bytes indicate the length of the + // field with the following formula: + // 193 + ((byte1 - 193) * 256) + byte2 + x if x <= MAX_SECOND_BYTE_VALUE => { + let byte2: usize = self.read_uint8()? as usize; + Ok((MAX_SINGLE_BYTE_LENGTH + 1) + + ((byte1 - (MAX_SINGLE_BYTE_LENGTH + 1)) * MAX_BYTE_VALUE) + + byte2) + } + // If the field contains 12481 to 918744 bytes of data, + // the first three bytes indicate the length of the + // field with the following formula: + // 12481 + ((byte1 - 241) * 65536) + (byte2 * 256) + byte3 + x if x <= 254 => { + let byte2: usize = self.read_uint8()? as usize; + let byte3: usize = self.read_uint8()? as usize; + + Ok(MAX_DOUBLE_BYTE_LENGTH + + ((byte1 - (MAX_SECOND_BYTE_VALUE + 1)) * MAX_DOUBLE_BYTE_VALUE) + + (byte2 * MAX_BYTE_VALUE) + + byte3) + } + _ => Err(XRPLBinaryCodecException::UnexpectedLengthPrefixRange { min: 1, max: 3 }), + } + } + + fn read_field_header(&mut self) -> Result { + let mut type_code: i16 = self.read_uint8()? as i16; + let mut field_code: i16 = type_code & 15; + + type_code >>= 4; + + if type_code == 0 { + type_code = self.read_uint8()? as i16; + + if type_code == 0 || type_code < 16 { + return Err(XRPLBinaryCodecException::UnexpectedTypeCodeRange { min: 1, max: 16 }); + }; + }; + + if field_code == 0 { + field_code = self.read_uint8()? as i16; + + if field_code == 0 || field_code < 16 { + return Err(XRPLBinaryCodecException::UnexpectedFieldCodeRange { min: 1, max: 16 }); + }; + }; + + Ok(FieldHeader { + type_code, + field_code, + }) + } + + fn read_field(&mut self) -> Result { + let field_header = self.read_field_header()?; + let field_name = get_field_name_from_header(&field_header); + + if let Some(name) = field_name { + if let Some(instance) = get_field_instance(name) { + return Ok(instance); + }; + }; + + Err(XRPLBinaryCodecException::UnknownFieldName) + } + + fn read_type(&mut self) -> Result { + T::from_parser(self, None) + } + + fn read_field_value(&mut self, field: &FieldInstance) -> Result + where + T::Error: From, + { + if field.is_vl_encoded { + let length = self.read_length_prefix()?; + T::from_parser(self, Some(length)) + } else { + T::from_parser(self, None) + } + } +} + +impl From<&[u8]> for BinaryParser { + fn from(hex_bytes: &[u8]) -> Self { + BinaryParser(hex_bytes.to_vec()) + } +} + +impl From> for BinaryParser { + fn from(hex_bytes: Vec) -> Self { + BinaryParser(hex_bytes) + } +} + +impl TryFrom<&str> for BinaryParser { + type Error = XRPLBinaryCodecException; + + fn try_from(hex_bytes: &str) -> Result { + Ok(BinaryParser(hex::decode(hex_bytes)?)) + } +} + +impl PartialEq<[u8]> for BinaryParser { + fn eq(&self, bytes: &[u8]) -> bool { + self.0 == bytes + } +} + +impl PartialEq> for BinaryParser { + fn eq(&self, bytes: &Vec) -> bool { + &self.0 == bytes + } +} + +impl ExactSizeIterator for BinaryParser { + fn len(&self) -> usize { + self.0.len() + } +} + +impl Iterator for BinaryParser { + type Item = u8; + + fn next(&mut self) -> Option { + if self.is_end(None) { + None + } else { + Some(self.read_uint8().expect("BinaryParser::next")) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::alloc::string::ToString; + use alloc::string::String; + + const TEST_HEX: &str = "00112233445566"; + + #[test] + fn test_binaryparser_from() { + let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); + let ref_bytes: &[u8] = test_bytes.as_ref(); + let slice_parser = BinaryParser::from(ref_bytes); + let vec_parser = BinaryParser::from(test_bytes.to_owned()); + + assert_eq!(slice_parser, test_bytes[..]); + assert_eq!(vec_parser, test_bytes[..]); + } + + #[test] + fn test_binaryparser_try_from() { + let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); + let string_parser = BinaryParser::try_from(TEST_HEX).unwrap(); + + assert_eq!(string_parser, test_bytes[..]); + } + + #[test] + fn test_peek() { + let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); + let binary_parser = BinaryParser::from(test_bytes.as_ref()); + + assert_eq!(binary_parser.peek(), Some([test_bytes[0]; 1])); + } + + #[test] + fn test_skip_bytes() { + let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); + let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); + + assert!(binary_parser.skip_bytes(4).is_ok()); + assert_eq!(binary_parser, test_bytes[4..]); + } + + #[test] + fn test_read() { + let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); + let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); + let result = binary_parser.read(5); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), test_bytes[..5]); + } + + #[test] + fn test_read_uint8() { + let test_hex: &str = "01000200000003"; + let test_bytes: Vec = hex::decode(test_hex).expect(""); + let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); + let result = binary_parser.read_uint8(); + + assert!(result.is_ok()); + assert_eq!(result, Ok(1)); + } + + #[test] + fn test_read_uint16() { + let test_hex: &str = "000200000003"; + let test_bytes: Vec = hex::decode(test_hex).expect(""); + let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); + let result = binary_parser.read_uint16(); + + assert!(result.is_ok()); + assert_eq!(result, Ok(2)); + } + + #[test] + fn test_read_uint32() { + let test_hex: &str = "00000003"; + let test_bytes: Vec = hex::decode(test_hex).expect(""); + let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); + let result = binary_parser.read_uint32(); + + assert!(result.is_ok()); + assert_eq!(result, Ok(3)); + } + + #[test] + fn test_read_length_prefix() { + let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); + let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); + let result = binary_parser.read_length_prefix(); + + assert!(result.is_ok()); + assert_eq!(result, Ok(0)); + } + + // TODO Finish tests + #[test] + fn test_read_field_header() {} + + #[test] + fn test_read_field_value() {} + + #[test] + fn test_read_field_and_value() {} + + #[test] + fn test_read_type() {} + + #[test] + fn accept_peek_skip_read() { + let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); + let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); + + assert_eq!(binary_parser.peek(), Some([test_bytes[0]; 1])); + assert!(binary_parser.skip_bytes(3).is_ok()); + assert_eq!(binary_parser, test_bytes[3..]); + + let result = binary_parser.read(2); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), test_bytes[3..5]); + } + + #[test] + fn test_binaryserializer_write_field_and_value() { + let field_header = FieldHeader { + type_code: -2, + field_code: 0, + }; + + let field_info = FieldInfo { + nth: 0, + is_vl_encoded: false, + is_serialized: false, + is_signing_field: false, + r#type: "Unknown".to_string(), + }; + + let field_instance = FieldInstance::new(&field_info, "Generic", field_header); + let expected: Vec = [224, 0, 17, 34].to_vec(); + let test_bytes: Vec = [0, 17, 34].to_vec(); + let mut serializer: BinarySerializer = BinarySerializer::new(); + + serializer.write_field_and_value(field_instance, &test_bytes, false); + assert_eq!(expected, serializer); + } + + /// This is currently a sanity check for private + /// [`_encode_variable_length_prefix`], which is called by + /// BinarySerializer.write_length_encoded. + #[test] + fn test_encode_variable_length_prefix() { + for case in [100_usize, 1000, 20_000] { + let blob = (0..case).map(|_| "A2").collect::(); + let mut binary_serializer: BinarySerializer = BinarySerializer::new(); + + binary_serializer.write_length_encoded(&hex::decode(blob).expect(""), true); + + let mut binary_parser: BinaryParser = BinaryParser::from(binary_serializer.as_ref()); + let decoded_length = binary_parser.read_length_prefix(); + + assert!(decoded_length.is_ok()); + assert_eq!(decoded_length, Ok(case)); + } + } +} diff --git a/src/core/binarycodec/exceptions.rs b/src/core/binarycodec/exceptions.rs index fd1a6931..d88eccd8 100644 --- a/src/core/binarycodec/exceptions.rs +++ b/src/core/binarycodec/exceptions.rs @@ -26,6 +26,9 @@ pub enum XRPLBinaryCodecException { SerdeJsonError(serde_json::error::Category), DecimalError(rust_decimal::Error), ISOCodeError(ISOCodeException), + FieldHasNoAssiciatedTag, + XAddressTagMismatch, + FieldIsNotAccountOrDestination, } impl From for XRPLBinaryCodecException { diff --git a/src/core/binarycodec/mod.rs b/src/core/binarycodec/mod.rs index ff4c02d3..d28b44fb 100644 --- a/src/core/binarycodec/mod.rs +++ b/src/core/binarycodec/mod.rs @@ -1,802 +1,93 @@ //! Functions for encoding objects into the XRP Ledger's //! canonical binary format and decoding them. -pub mod exceptions; -pub(crate) mod test_cases; -pub mod utils; - -use crate::core::binarycodec::exceptions::XRPLBinaryCodecException; -use crate::core::binarycodec::utils::*; -use crate::core::definitions::*; -use crate::core::types::TryFromParser; -use crate::utils::ToBytes; -use alloc::borrow::ToOwned; -use alloc::vec; -use alloc::vec::Vec; -use core::convert::TryFrom; -use core::convert::TryInto; - -/// Serializes JSON to XRPL binary format. -pub type BinarySerializer = Vec; - -/// Deserializes from hex-encoded XRPL binary format to -/// serde JSON fields and values. -/// -/// # Examples -/// -/// ## Basic usage -/// -/// ``` -/// use xrpl::core::binarycodec::BinaryParser; -/// use xrpl::core::Parser; -/// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; -/// -/// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; -/// let binary_parser: BinaryParser = BinaryParser::from(test_bytes); -/// -/// assert_eq!(binary_parser, test_bytes[..]); -/// ``` -#[derive(Debug, Clone)] -pub struct BinaryParser(Vec); - -/// Helper function for length-prefixed fields including -/// Blob types and some AccountID types. Calculates the -/// prefix of variable length bytes. -/// -/// The length of the prefix is 1-3 bytes depending on the -/// length of the contents: -/// Content length <= 192 bytes: prefix is 1 byte -/// 192 bytes < Content length <= 12480 bytes: prefix is 2 bytes -/// 12480 bytes < Content length <= 918744 bytes: prefix is 3 bytes -/// -/// See Length Prefixing: -/// `` -fn _encode_variable_length_prefix(length: &usize) -> Result, XRPLBinaryCodecException> { - if length <= &MAX_SINGLE_BYTE_LENGTH { - Ok([*length as u8].to_vec()) - } else if length < &MAX_DOUBLE_BYTE_LENGTH { - let mut bytes = vec![]; - let b_length = *length - (MAX_SINGLE_BYTE_LENGTH + 1); - let val_a: u8 = ((b_length >> 8) + (MAX_SINGLE_BYTE_LENGTH + 1)).try_into()?; - let val_b: u8 = (b_length & 0xFF).try_into()?; - - bytes.extend_from_slice(&[val_a]); - bytes.extend_from_slice(&[val_b]); - - Ok(bytes) - } else if length <= &MAX_LENGTH_VALUE { - let mut bytes = vec![]; - let b_length = *length - MAX_DOUBLE_BYTE_LENGTH; - let val_a: u8 = ((MAX_SECOND_BYTE_VALUE + 1) + (b_length >> 16)).try_into()?; - let val_b: u8 = ((b_length >> 8) & 0xFF).try_into()?; - let val_c: u8 = (b_length & 0xFF).try_into()?; - - bytes.extend_from_slice(&[val_a]); - bytes.extend_from_slice(&[val_b]); - bytes.extend_from_slice(&[val_c]); - - Ok(bytes) - } else { - Err(XRPLBinaryCodecException::InvalidVariableLengthTooLarge { - max: MAX_LENGTH_VALUE, - }) - } -} - -pub trait Parser { - /// Peek the first byte of the BinaryParser. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// - /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; - /// let binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// let first_byte: Option<[u8; 1]> = binary_parser.peek(); - /// - /// assert_eq!(Some([test_bytes[0]; 1]), first_byte); - /// ``` - fn peek(&self) -> Option<[u8; 1]>; - - /// Consume the first n bytes of the BinaryParser. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// - /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; - /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// - /// match binary_parser.skip_bytes(4) { - /// Ok(parser) => assert_eq!(*parser, test_bytes[4..]), - /// Err(e) => match e { - /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { - /// max: _, - /// found: _, - /// } => assert!(false), - /// _ => assert!(false) - /// } - /// } - /// ``` - fn skip_bytes(&mut self, n: usize) -> Result<&Self, XRPLBinaryCodecException>; - - /// Consume and return the first n bytes of the BinaryParser. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// - /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; - /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// - /// match binary_parser.read(5) { - /// Ok(data) => assert_eq!(test_bytes[..5], data), - /// Err(e) => match e { - /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { - /// max: _, - /// found: _, - /// } => assert!(false), - /// _ => assert!(false) - /// } - /// } - /// ``` - fn read(&mut self, n: usize) -> Result, XRPLBinaryCodecException>; - - /// Read 1 byte from parser and return as unsigned int. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// - /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; - /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// - /// match binary_parser.read_uint8() { - /// Ok(data) => assert_eq!(0, data), - /// Err(e) => match e { - /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { - /// max: _, - /// found: _, - /// } => assert!(false), - /// _ => assert!(false) - /// } - /// } - /// ``` - fn read_uint8(&mut self) -> Result; - - /// Read 2 bytes from parser and return as unsigned int. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// - /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; - /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// - /// match binary_parser.read_uint16() { - /// Ok(data) => assert_eq!(17, data), - /// Err(e) => match e { - /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { - /// max: _, - /// found: _, - /// } => assert!(false), - /// _ => assert!(false) - /// } - /// } - /// ``` - fn read_uint16(&mut self) -> Result; - - /// Read 4 bytes from parser and return as unsigned int. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// - /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; - /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// - /// match binary_parser.read_uint32() { - /// Ok(data) => assert_eq!(1122867, data), - /// Err(e) => match e { - /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { - /// max: _, - /// found: _, - /// } => assert!(false), - /// _ => assert!(false) - /// } - /// } - /// ``` - fn read_uint32(&mut self) -> Result; - - /// Returns whether the binary parser has finished - /// parsing (e.g. there is nothing left in the buffer - /// that needs to be processed). - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// extern crate alloc; - /// use alloc::vec; - /// - /// let empty: &[u8] = &[]; - /// let mut buffer: Vec = vec![]; - /// let test_bytes: &[u8] = &[0, 17, 34, 51, 68, 85, 102]; - /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// - /// while !binary_parser.is_end(None) { - /// match binary_parser.read(1) { - /// Ok(data) => buffer.extend_from_slice(&data), - /// Err(e) => match e { - /// XRPLBinaryCodecException::UnexpectedParserSkipOverflow { - /// max: _, - /// found: _, - /// } => assert!(false), - /// _ => assert!(false) - /// } - /// } - /// } - /// - /// assert_eq!(test_bytes, &buffer[..]); - /// // The BinaryParser is emptied as it is read. - /// assert_eq!(binary_parser, empty[..]); - /// - /// ``` - fn is_end(&self, custom_end: Option) -> bool; - - /// Reads a variable length encoding prefix and returns - /// the encoded length. The formula for decoding a length - /// prefix is described in Length Prefixing. - /// - /// See Length Prefixing: - /// `` - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinaryParser; - /// use xrpl::core::Parser; - /// use xrpl::core::binarycodec::exceptions::XRPLBinaryCodecException; - /// - /// let test_bytes: &[u8] = &[6, 17, 34, 51, 68, 85, 102]; - /// let mut binary_parser: BinaryParser = BinaryParser::from(test_bytes); - /// - /// match binary_parser.read_length_prefix() { - /// Ok(data) => assert_eq!(6, data), - /// Err(e) => match e { - /// XRPLBinaryCodecException::UnexpectedLengthPrefixRange { - /// min: _, max: _ - /// } => assert!(false), - /// _ => assert!(false) - /// } - /// } - fn read_length_prefix(&mut self) -> Result; - - /// Reads field ID from BinaryParser and returns as - /// a FieldHeader object. - fn read_field_header(&mut self) -> Result; - - /// Read the field ordinal at the head of the - /// BinaryParser and return a FieldInstance object - /// representing information about the field - /// containedin the following bytes. - fn read_field(&mut self) -> Result; - - /// Read next bytes from BinaryParser as the given type. - fn read_type(&mut self) -> Result; - - /// Read value of the type specified by field from - /// the BinaryParser. - fn read_field_value(&mut self, field: &FieldInstance) -> Result - where - T::Error: From; -} - -pub trait Serialization { - /// Write given bytes to this BinarySerializer. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinarySerializer; - /// use xrpl::core::binarycodec::Serialization; - /// - /// let mut test_bytes: Vec = [0, 17, 34, 51, 68, 85, 102].to_vec(); - /// let mut serializer: BinarySerializer = BinarySerializer::new(); - /// - /// serializer.append(&mut test_bytes.to_owned()); - /// assert_eq!(test_bytes, serializer); - /// ``` - fn append(&mut self, bytes: &[u8]) -> &Self; - - /// Write a variable length encoded value to - /// the BinarySerializer. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinarySerializer; - /// use xrpl::core::binarycodec::Serialization; - /// - /// let expected: Vec = [3, 0, 17, 34].to_vec(); - /// let mut test_bytes: Vec = [0, 17, 34].to_vec(); - /// let mut serializer: BinarySerializer = BinarySerializer::new(); - /// - /// serializer.write_length_encoded(&mut test_bytes); - /// assert_eq!(expected, serializer); - /// ``` - fn write_length_encoded(&mut self, value: &[u8]) -> &Self; - - /// Write field and value to the buffer. - /// - /// # Examples - /// - /// ## Basic usage - /// - /// ``` - /// use xrpl::core::binarycodec::BinarySerializer; - /// use xrpl::core::binarycodec::Serialization; - /// use xrpl::core::definitions::FieldInstance; - /// use xrpl::core::definitions::FieldInfo; - /// use xrpl::core::definitions::FieldHeader; - /// - /// let field_header: FieldHeader = FieldHeader { - /// type_code: -2, - /// field_code: 0, - /// }; - /// - /// let field_info: FieldInfo = FieldInfo { - /// nth: 0, - /// is_vl_encoded: false, - /// is_serialized: false, - /// is_signing_field: false, - /// r#type: "Unknown".to_string(), - /// }; - /// - /// let field_instance = FieldInstance::new(&field_info, "Generic", field_header); - /// let expected: Vec = [255, 224, 0, 17, 34].to_vec(); - /// let test_bytes: Vec = [0, 17, 34].to_vec(); - /// let mut serializer: BinarySerializer = BinarySerializer::new(); - /// - /// serializer.write_field_and_value(field_instance, &test_bytes); - /// assert_eq!(expected, serializer); - /// ``` - fn write_field_and_value(&mut self, field: FieldInstance, value: &[u8]) -> &Self; -} - -impl Serialization for BinarySerializer { - fn append(&mut self, bytes: &[u8]) -> &Self { - self.extend_from_slice(bytes); - self - } - - fn write_length_encoded(&mut self, value: &[u8]) -> &Self { - let length_prefix = _encode_variable_length_prefix(&value.len()); - - // TODO Handle unwrap better - self.extend_from_slice(&length_prefix.unwrap()); - self.extend_from_slice(value); - - self - } - - fn write_field_and_value(&mut self, field: FieldInstance, value: &[u8]) -> &Self { - self.extend_from_slice(&field.header.to_bytes()); - - if field.is_vl_encoded { - self.write_length_encoded(value); - } else { - self.extend_from_slice(value); - } - - self - } -} - -/// Peek the first byte of the BinaryParser. -impl Parser for BinaryParser { - fn peek(&self) -> Option<[u8; 1]> { - if !self.0.is_empty() { - Some(self.0[0].to_be_bytes()) - } else { - None - } - } - - fn skip_bytes(&mut self, n: usize) -> Result<&Self, XRPLBinaryCodecException> { - if n > self.0.len() { - Err(XRPLBinaryCodecException::UnexpectedParserSkipOverflow { - max: self.0.len(), - found: n, - }) - } else { - self.0 = self.0[n..].to_vec(); - Ok(self) - } - } - - fn read(&mut self, n: usize) -> Result, XRPLBinaryCodecException> { - let first_n_bytes = self.0[..n].to_owned(); - - self.skip_bytes(n)?; - Ok(first_n_bytes) - } - - fn read_uint8(&mut self) -> Result { - let result = self.read(1)?; - Ok(u8::from_be_bytes(result.try_into().or(Err( - XRPLBinaryCodecException::InvalidReadFromBytesValue, - ))?)) - } - - fn read_uint16(&mut self) -> Result { - let result = self.read(2)?; - Ok(u16::from_be_bytes(result.try_into().or(Err( - XRPLBinaryCodecException::InvalidReadFromBytesValue, - ))?)) - } - - fn read_uint32(&mut self) -> Result { - let result = self.read(4)?; - Ok(u32::from_be_bytes(result.try_into().or(Err( - XRPLBinaryCodecException::InvalidReadFromBytesValue, - ))?)) - } - - fn is_end(&self, custom_end: Option) -> bool { - if let Some(end) = custom_end { - self.0.len() <= end - } else { - self.0.is_empty() - } - } - - fn read_length_prefix(&mut self) -> Result { - let byte1: usize = self.read_uint8()? as usize; - - match byte1 { - // If the field contains 0 to 192 bytes of data, - // the first byte defines the length of the contents. - x if x <= MAX_SINGLE_BYTE_LENGTH => Ok(byte1), - // If the field contains 193 to 12480 bytes of data, - // the first two bytes indicate the length of the - // field with the following formula: - // 193 + ((byte1 - 193) * 256) + byte2 - x if x <= MAX_SECOND_BYTE_VALUE => { - let byte2: usize = self.read_uint8()? as usize; - Ok((MAX_SINGLE_BYTE_LENGTH + 1) - + ((byte1 - (MAX_SINGLE_BYTE_LENGTH + 1)) * MAX_BYTE_VALUE) - + byte2) - } - // If the field contains 12481 to 918744 bytes of data, - // the first three bytes indicate the length of the - // field with the following formula: - // 12481 + ((byte1 - 241) * 65536) + (byte2 * 256) + byte3 - x if x <= 254 => { - let byte2: usize = self.read_uint8()? as usize; - let byte3: usize = self.read_uint8()? as usize; - - Ok(MAX_DOUBLE_BYTE_LENGTH - + ((byte1 - (MAX_SECOND_BYTE_VALUE + 1)) * MAX_DOUBLE_BYTE_VALUE) - + (byte2 * MAX_BYTE_VALUE) - + byte3) - } - _ => Err(XRPLBinaryCodecException::UnexpectedLengthPrefixRange { min: 1, max: 3 }), - } - } - - fn read_field_header(&mut self) -> Result { - let mut type_code: i16 = self.read_uint8()? as i16; - let mut field_code: i16 = type_code & 15; - - type_code >>= 4; - - if type_code == 0 { - type_code = self.read_uint8()? as i16; - - if type_code == 0 || type_code < 16 { - return Err(XRPLBinaryCodecException::UnexpectedTypeCodeRange { min: 1, max: 16 }); - }; - }; - - if field_code == 0 { - field_code = self.read_uint8()? as i16; - - if field_code == 0 || field_code < 16 { - return Err(XRPLBinaryCodecException::UnexpectedFieldCodeRange { min: 1, max: 16 }); - }; - }; - Ok(FieldHeader { - type_code, - field_code, - }) - } - - fn read_field(&mut self) -> Result { - let field_header = self.read_field_header()?; - let field_name = get_field_name_from_header(&field_header); - - if let Some(name) = field_name { - if let Some(instance) = get_field_instance(name) { - return Ok(instance); - }; - }; - - Err(XRPLBinaryCodecException::UnknownFieldName) - } - - fn read_type(&mut self) -> Result { - T::from_parser(self, None) - } - - fn read_field_value(&mut self, field: &FieldInstance) -> Result - where - T::Error: From, - { - if field.is_vl_encoded { - let length = self.read_length_prefix()?; - T::from_parser(self, Some(length)) - } else { - T::from_parser(self, None) - } - } -} +use super::types::{AccountId, STObject}; +use crate::{models::transactions::Transaction, Err}; -impl From<&[u8]> for BinaryParser { - fn from(hex_bytes: &[u8]) -> Self { - BinaryParser(hex_bytes.to_vec()) - } -} +use alloc::{borrow::Cow, string::String, vec::Vec}; +use anyhow::Result; +use core::{convert::TryFrom, fmt::Debug}; +use hex::ToHex; +use serde::{de::DeserializeOwned, Serialize}; +use strum::IntoEnumIterator; -impl From> for BinaryParser { - fn from(hex_bytes: Vec) -> Self { - BinaryParser(hex_bytes) - } -} +pub mod binary_wrappers; +pub mod exceptions; +pub(crate) mod test_cases; +pub mod utils; -impl TryFrom<&str> for BinaryParser { - type Error = XRPLBinaryCodecException; +pub use binary_wrappers::*; - fn try_from(hex_bytes: &str) -> Result { - Ok(BinaryParser(hex::decode(hex_bytes)?)) - } -} +const TRANSACTION_SIGNATURE_PREFIX: i32 = 0x53545800; +const TRANSACTION_MULTISIG_PREFIX: i32 = 0x534D5400; -impl PartialEq<[u8]> for BinaryParser { - fn eq(&self, bytes: &[u8]) -> bool { - self.0 == bytes - } +pub fn encode<'a, T, F>(signed_transaction: &T) -> Result +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone + Debug, +{ + serialize_json(signed_transaction, None, None, false) } -impl PartialEq> for BinaryParser { - fn eq(&self, bytes: &Vec) -> bool { - &self.0 == bytes - } +pub fn encode_for_signing<'a, T, F>(prepared_transaction: &T) -> Result +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone + Debug, +{ + serialize_json( + prepared_transaction, + Some(TRANSACTION_SIGNATURE_PREFIX.to_be_bytes().as_ref()), + None, + true, + ) } -impl ExactSizeIterator for BinaryParser { - fn len(&self) -> usize { - self.0.len() - } +pub fn encode_for_multisigning<'a, T, F>( + prepared_transaction: &T, + signing_account: Cow<'a, str>, +) -> Result +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone + Debug, +{ + let signing_account_id = AccountId::try_from(signing_account.as_ref()).unwrap(); + + serialize_json( + prepared_transaction, + Some(TRANSACTION_MULTISIG_PREFIX.to_be_bytes().as_ref()), + Some(signing_account_id.as_ref()), + true, + ) } -impl Iterator for BinaryParser { - type Item = u8; - - fn next(&mut self) -> Option { - if self.is_end(None) { - None - } else { - Some(self.read_uint8().expect("BinaryParser::next")) +fn serialize_json<'a, T, F>( + prepared_transaction: &T, + prefix: Option<&[u8]>, + suffix: Option<&[u8]>, + signing_only: bool, +) -> Result +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned + Clone + Debug, +{ + let mut buffer = Vec::new(); + if let Some(p) = prefix { + buffer.extend(p); + } + let json_value = match serde_json::to_value(prepared_transaction) { + Ok(v) => v, + Err(e) => { + return Err!(e); } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::alloc::string::ToString; - use alloc::string::String; + }; + let st_object = STObject::try_from_value(json_value, signing_only)?; - const TEST_HEX: &str = "00112233445566"; - - #[test] - fn test_binaryparser_from() { - let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); - let ref_bytes: &[u8] = test_bytes.as_ref(); - let slice_parser = BinaryParser::from(ref_bytes); - let vec_parser = BinaryParser::from(test_bytes.to_owned()); - - assert_eq!(slice_parser, test_bytes[..]); - assert_eq!(vec_parser, test_bytes[..]); - } - - #[test] - fn test_binaryparser_try_from() { - let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); - let string_parser = BinaryParser::try_from(TEST_HEX).unwrap(); - - assert_eq!(string_parser, test_bytes[..]); + buffer.extend(st_object.as_ref()); + if let Some(s) = suffix { + buffer.extend(s); } - #[test] - fn test_peek() { - let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); - let binary_parser = BinaryParser::from(test_bytes.as_ref()); + let hex_string = buffer.encode_hex_upper::(); - assert_eq!(binary_parser.peek(), Some([test_bytes[0]; 1])); - } - - #[test] - fn test_skip_bytes() { - let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); - let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); - - assert!(binary_parser.skip_bytes(4).is_ok()); - assert_eq!(binary_parser, test_bytes[4..]); - } - - #[test] - fn test_read() { - let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); - let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); - let result = binary_parser.read(5); - - assert!(result.is_ok()); - assert_eq!(result.unwrap(), test_bytes[..5]); - } - - #[test] - fn test_read_uint8() { - let test_hex: &str = "01000200000003"; - let test_bytes: Vec = hex::decode(test_hex).expect(""); - let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); - let result = binary_parser.read_uint8(); - - assert!(result.is_ok()); - assert_eq!(result, Ok(1)); - } - - #[test] - fn test_read_uint16() { - let test_hex: &str = "000200000003"; - let test_bytes: Vec = hex::decode(test_hex).expect(""); - let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); - let result = binary_parser.read_uint16(); - - assert!(result.is_ok()); - assert_eq!(result, Ok(2)); - } - - #[test] - fn test_read_uint32() { - let test_hex: &str = "00000003"; - let test_bytes: Vec = hex::decode(test_hex).expect(""); - let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); - let result = binary_parser.read_uint32(); - - assert!(result.is_ok()); - assert_eq!(result, Ok(3)); - } - - #[test] - fn test_read_length_prefix() { - let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); - let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); - let result = binary_parser.read_length_prefix(); - - assert!(result.is_ok()); - assert_eq!(result, Ok(0)); - } - - // TODO Finish tests - #[test] - fn test_read_field_header() {} - - #[test] - fn test_read_field_value() {} - - #[test] - fn test_read_field_and_value() {} - - #[test] - fn test_read_type() {} - - #[test] - fn accept_peek_skip_read() { - let test_bytes: Vec = hex::decode(TEST_HEX).expect(""); - let mut binary_parser = BinaryParser::from(test_bytes.as_ref()); - - assert_eq!(binary_parser.peek(), Some([test_bytes[0]; 1])); - assert!(binary_parser.skip_bytes(3).is_ok()); - assert_eq!(binary_parser, test_bytes[3..]); - - let result = binary_parser.read(2); - - assert!(result.is_ok()); - assert_eq!(result.unwrap(), test_bytes[3..5]); - } - - #[test] - fn test_binaryserializer_write_field_and_value() { - let field_header = FieldHeader { - type_code: -2, - field_code: 0, - }; - - let field_info = FieldInfo { - nth: 0, - is_vl_encoded: false, - is_serialized: false, - is_signing_field: false, - r#type: "Unknown".to_string(), - }; - - let field_instance = FieldInstance::new(&field_info, "Generic", field_header); - let expected: Vec = [255, 224, 0, 17, 34].to_vec(); - let test_bytes: Vec = [0, 17, 34].to_vec(); - let mut serializer: BinarySerializer = BinarySerializer::new(); - - serializer.write_field_and_value(field_instance, &test_bytes); - assert_eq!(expected, serializer); - } - - /// This is currently a sanity check for private - /// [`_encode_variable_length_prefix`], which is called by - /// BinarySerializer.write_length_encoded. - #[test] - fn test_encode_variable_length_prefix() { - for case in [100_usize, 1000, 20_000] { - let blob = (0..case).map(|_| "A2").collect::(); - let mut binary_serializer: BinarySerializer = BinarySerializer::new(); - - binary_serializer.write_length_encoded(&hex::decode(blob).expect("")); - - let mut binary_parser: BinaryParser = BinaryParser::from(binary_serializer.as_ref()); - let decoded_length = binary_parser.read_length_prefix(); - - assert!(decoded_length.is_ok()); - assert_eq!(decoded_length, Ok(case)); - } - } + Ok(hex_string) } diff --git a/src/core/definitions/definitions.json b/src/core/definitions/definitions.json index 9a4f4453..1ffda30c 100644 --- a/src/core/definitions/definitions.json +++ b/src/core/definitions/definitions.json @@ -9,7 +9,7 @@ "Hash256": 5, "UInt8": 16, "Vector256": 19, - "SerializedDict": 14, + "STObject": 14, "Unknown": -2, "Transaction": 10001, "Hash160": 17, @@ -19,7 +19,7 @@ "NotPresent": 0, "UInt64": 3, "UInt32": 2, - "SerializedList": 15 + "STArray": 15 }, "LEDGER_ENTRY_TYPES": { "Any": -3, @@ -1141,7 +1141,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1151,7 +1151,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1161,7 +1161,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1171,7 +1171,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1181,7 +1181,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1191,7 +1191,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1201,7 +1201,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1211,7 +1211,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1221,7 +1221,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1231,7 +1231,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1241,7 +1241,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1251,7 +1251,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1261,7 +1261,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1271,7 +1271,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedDict" + "type": "STObject" } ], [ @@ -1281,7 +1281,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1291,7 +1291,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": false, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1301,7 +1301,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1311,7 +1311,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1321,7 +1321,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1331,7 +1331,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1341,7 +1341,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1351,7 +1351,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1361,7 +1361,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1371,7 +1371,7 @@ "isVLEncoded": false, "isSerialized": true, "isSigningField": true, - "type": "SerializedList" + "type": "STArray" } ], [ @@ -1619,7 +1619,6 @@ "telCAN_NOT_QUEUE_BLOCKED": -389, "telCAN_NOT_QUEUE_FEE": -388, "telCAN_NOT_QUEUE_FULL": -387, - "temMALFORMED": -299, "temBAD_AMOUNT": -298, "temBAD_CURRENCY": -297, @@ -1655,7 +1654,6 @@ "temCANNOT_PREAUTH_SELF": -267, "temUNCERTAIN": -266, "temUNKNOWN": -265, - "tefFAILURE": -199, "tefALREADY": -198, "tefBAD_ADD_AUTH": -197, @@ -1675,7 +1673,6 @@ "tefBAD_AUTH_MASTER": -183, "tefINVARIANT_FAILED": -182, "tefTOO_BIG": -181, - "terRETRY": -99, "terFUNDS_SPENT": -98, "terINSUF_FEE_B": -97, @@ -1687,9 +1684,7 @@ "terLAST": -91, "terNO_RIPPLE": -90, "terQUEUED": -89, - "tesSUCCESS": 0, - "tecCLAIM": 100, "tecPATH_PARTIAL": 101, "tecUNFUNDED_ADD": 102, @@ -1731,7 +1726,6 @@ }, "TRANSACTION_TYPES": { "Invalid": -1, - "Payment": 0, "EscrowCreate": 1, "EscrowFinish": 2, @@ -1754,9 +1748,8 @@ "DepositPreauth": 19, "TrustSet": 20, "AccountDelete": 21, - "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 } -} +} \ No newline at end of file diff --git a/src/core/definitions/mod.rs b/src/core/definitions/mod.rs index 12dfe556..554c0269 100644 --- a/src/core/definitions/mod.rs +++ b/src/core/definitions/mod.rs @@ -65,7 +65,7 @@ pub struct FieldHeader { /// let field_instance: FieldInstance = /// FieldInstance::new(&field_info, "Generic", field_header); /// ``` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FieldInstance { pub nth: i16, pub is_vl_encoded: bool, @@ -146,10 +146,10 @@ impl ToBytes for FieldHeader { if self.type_code < 16 { if self.field_code < 16 { - let shift = self.type_code << 4 | self.field_code; + let shift = (self.type_code << 4 | self.field_code) as u8; header_bytes.extend_from_slice(&shift.to_be_bytes()); } else { - let shift = self.type_code << 4; + let shift = (self.type_code << 4) as u8; header_bytes.extend_from_slice(&shift.to_be_bytes()); header_bytes.extend_from_slice(&self.field_code.to_be_bytes()); diff --git a/src/core/definitions/types.rs b/src/core/definitions/types.rs index e03c9844..8a374c89 100644 --- a/src/core/definitions/types.rs +++ b/src/core/definitions/types.rs @@ -36,7 +36,8 @@ pub struct Types { pub hash_256: i16, pub u_int_8: i16, pub vector_256: i16, - pub serialized_dict: i16, + #[serde(rename = "STObject")] + pub st_object: i16, pub unknown: i16, pub transaction: i16, pub hash_160: i16, @@ -46,7 +47,8 @@ pub struct Types { pub not_present: i16, pub u_int_64: i16, pub u_int_32: i16, - pub serialized_list: i16, + #[serde(rename = "STArray")] + pub st_array: i16, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/core/mod.rs b/src/core/mod.rs index 0a16f755..a5d6073b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -6,7 +6,7 @@ pub mod definitions; pub mod keypairs; pub mod types; -pub use self::binarycodec::BinaryParser; -pub use self::binarycodec::BinarySerializer; -pub use self::binarycodec::Parser; +pub use self::binarycodec::binary_wrappers::BinaryParser; +pub use self::binarycodec::binary_wrappers::BinarySerializer; +pub use self::binarycodec::binary_wrappers::Parser; pub use self::definitions::load_definition_map; diff --git a/src/core/test_data/data-driven-tests.json b/src/core/test_data/data-driven-tests.json index 5f27bf2f..017624dd 100644 --- a/src/core/test_data/data-driven-tests.json +++ b/src/core/test_data/data-driven-tests.json @@ -33,11 +33,11 @@ "ordinal": 8 }, { - "name": "SerializedDict", + "name": "STObject", "ordinal": 14 }, { - "name": "SerializedList", + "name": "STArray", "ordinal": 15 }, { @@ -736,126 +736,126 @@ "expected_hex": "88" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "ObjectEndMarker", "nth_of_type": 1, "type": 14, "expected_hex": "E1" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "TransactionMetaData", "nth_of_type": 2, "type": 14, "expected_hex": "E2" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "CreatedNode", "nth_of_type": 3, "type": 14, "expected_hex": "E3" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "DeletedNode", "nth_of_type": 4, "type": 14, "expected_hex": "E4" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "ModifiedNode", "nth_of_type": 5, "type": 14, "expected_hex": "E5" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "PreviousFields", "nth_of_type": 6, "type": 14, "expected_hex": "E6" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "FinalFields", "nth_of_type": 7, "type": 14, "expected_hex": "E7" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "NewFields", "nth_of_type": 8, "type": 14, "expected_hex": "E8" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "TemplateEntry", "nth_of_type": 9, "type": 14, "expected_hex": "E9" }, { - "type_name": "SerializedDict", + "type_name": "STObject", "name": "Memo", "nth_of_type": 10, "type": 14, "expected_hex": "EA" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "ArrayEndMarker", "nth_of_type": 1, "type": 15, "expected_hex": "F1" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "Signers", "nth_of_type": 3, "type": 15, "expected_hex": "F3" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "SignerEntries", "nth_of_type": 4, "type": 15, "expected_hex": "F4" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "Template", "nth_of_type": 5, "type": 15, "expected_hex": "F5" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "Necessary", "nth_of_type": 6, "type": 15, "expected_hex": "F6" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "Sufficient", "nth_of_type": 7, "type": 15, "expected_hex": "F7" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "AffectedNodes", "nth_of_type": 8, "type": 15, "expected_hex": "F8" }, { - "type_name": "SerializedList", + "type_name": "STArray", "name": "Memos", "nth_of_type": 9, "type": 15, @@ -3948,4 +3948,4 @@ "expected_hex": "8D" } ] -} +} \ No newline at end of file diff --git a/src/core/types/exceptions.rs b/src/core/types/exceptions.rs index 06f6aec0..94e1edb3 100644 --- a/src/core/types/exceptions.rs +++ b/src/core/types/exceptions.rs @@ -6,6 +6,7 @@ use crate::utils::exceptions::ISOCodeException; use crate::utils::exceptions::JSONParseException; use crate::utils::exceptions::XRPRangeException; use strum_macros::Display; +use thiserror_no_std::Error; #[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] @@ -17,6 +18,33 @@ pub enum XRPLTypeException { XRPLRangeError(XRPRangeException), DecimalError(rust_decimal::Error), JSONParseError(JSONParseException), + UnknownXRPLType, +} + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum XRPLSerializeArrayException { + #[error("Expected `Value` to be an array.")] + ExpectedArray, + #[error("Expected `Value` to be an array of objects.")] + ExpectedObjectArray, +} + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum XRPLSerializeMapException<'a> { + #[error("Expected `Value` to be an object.")] + ExpectedObject, + #[error("Field `{field}` is not allowed to have an associated tag.")] + DisallowedTag { field: &'a str }, + #[error("Cannot have mismatched Account X-Address and SourceTag")] + AccountMismatchingTags, + #[error("Cannot have mismatched Destination X-Address and DestinationTag")] + DestinationMismatchingTags, + #[error("Unknown transaction type: {0}")] + UnknownTransactionType(&'a str), + #[error("Unknown transaction result: {0}")] + UnknownTransactionResult(&'a str), + #[error("Unknown ledger entry type: {0}")] + UnknownLedgerEntryType(&'a str), } #[derive(Debug, Clone, PartialEq, Display)] diff --git a/src/core/types/mod.rs b/src/core/types/mod.rs index 91d2ec80..dac7bbb8 100644 --- a/src/core/types/mod.rs +++ b/src/core/types/mod.rs @@ -11,6 +11,12 @@ pub(crate) mod test_cases; pub mod utils; pub mod vector256; +use core::convert::TryFrom; +use core::convert::TryInto; +use core::fmt::Debug; +use core::fmt::Display; +use core::iter::FromIterator; + pub use self::account_id::AccountId; pub use self::amount::Amount; pub use self::blob::Blob; @@ -24,11 +30,154 @@ pub use self::paths::PathSet; pub use self::paths::PathStep; pub use self::vector256::Vector256; +use crate::core::binarycodec::binary_wrappers::Serialization; +use crate::core::definitions::get_field_instance; +use crate::core::definitions::get_transaction_result_code; +use crate::core::definitions::get_transaction_type_code; +use crate::core::definitions::FieldInstance; use crate::core::BinaryParser; +use crate::Err; +use alloc::borrow::Cow; +use alloc::borrow::ToOwned; +use alloc::string::String; +use alloc::string::ToString; +use alloc::vec; use alloc::vec::Vec; +use amount::IssuedCurrency; +use anyhow::Result; +use serde_json::Map; +use serde_json::Value; + +use super::addresscodec::is_valid_xaddress; +use super::addresscodec::xaddress_to_classic_address; +use super::BinarySerializer; + +const ACCOUNT: &str = "Account"; +const SOURCE_TAG: &str = "SourceTag"; +const DESTINATION: &str = "Destination"; +const DESTINATION_TAG: &str = "DestinationTag"; +const UNL_MODIFY_TX_TYPE: &str = "0066"; +const ST_OBJECT: &str = "STObject"; +const OBJECT_END_MARKER_BYTES: [u8; 1] = [0xE1]; +const ARRAY_END_MARKER: [u8; 1] = [0xF1]; -/// Contains a serialized buffer of a Serializer type. #[derive(Debug)] +pub enum XRPLTypes { + AccountID(AccountId), + Amount(Amount), + Blob(Blob), + Currency(Currency), + Hash128(Hash128), + Hash160(Hash160), + Hash256(Hash256), + Path(Path), + PathSet(PathSet), + PathStep(PathStep), + Vector256(Vector256), + STArray(STArray), + STObject(STObject), + UInt8(u8), + UInt16(u16), + UInt32(u32), + UInt64(u64), + Unknown, +} + +impl XRPLTypes { + pub fn from_value(name: &str, value: Value) -> Result { + if let Some(value) = value.as_str() { + match name { + "AccountID" => Ok(XRPLTypes::AccountID(Self::type_from_str(value)?)), + "Amount" => Ok(XRPLTypes::Amount(Self::type_from_str(value)?)), + "Blob" => Ok(XRPLTypes::Blob(Self::type_from_str(value)?)), + "Currency" => Ok(XRPLTypes::Currency(Self::type_from_str(value)?)), + "Hash128" => Ok(XRPLTypes::Hash128(Self::type_from_str(value)?)), + "Hash160" => Ok(XRPLTypes::Hash160(Self::type_from_str(value)?)), + "Hash256" => Ok(XRPLTypes::Hash256(Self::type_from_str(value)?)), + _ => Err!(exceptions::XRPLTypeException::UnknownXRPLType), + } + } else if let Some(value) = value.as_u64() { + match name { + "UInt8" => Ok(XRPLTypes::UInt8(value as u8)), + "UInt16" => Ok(XRPLTypes::UInt16(value as u16)), + "UInt32" => Ok(XRPLTypes::UInt32(value as u32)), + "UInt64" => Ok(XRPLTypes::UInt64(value as u64)), + _ => Err!(exceptions::XRPLTypeException::UnknownXRPLType), + } + } else if let Some(value) = value.as_object() { + match name { + "Amount" => Ok(XRPLTypes::Amount(Self::amount_from_map(value.to_owned())?)), + "STObject" => Ok(XRPLTypes::STObject(STObject::try_from_value( + Value::Object(value.to_owned()), + false, + )?)), + _ => Err!(exceptions::XRPLTypeException::UnknownXRPLType), + } + } else if let Some(value) = value.as_array() { + match name { + "STArray" => Ok(XRPLTypes::STArray(STArray::try_from_value(Value::Array( + value.to_owned(), + ))?)), + _ => Err!(exceptions::XRPLTypeException::UnknownXRPLType), + } + } else { + Err!(exceptions::XRPLTypeException::UnknownXRPLType) + } + } + + fn type_from_str<'a, T>(value: &'a str) -> Result + where + T: TryFrom<&'a str>, + >::Error: Display, + { + match value.try_into() { + Ok(value) => Ok(value), + Err(error) => Err!(error), + } + } + + fn amount_from_map(value: Map) -> Result + where + T: TryFrom, + >::Error: Display, + { + match IssuedCurrency::try_from(Value::Object(value)) { + Ok(value) => match value.try_into() { + Ok(value) => Ok(value), + Err(error) => Err!(error), + }, + Err(error) => Err!(error), + } + } +} + +impl Into for XRPLTypes { + fn into(self) -> SerializedType { + match self { + XRPLTypes::AccountID(account_id) => SerializedType::from(account_id), + XRPLTypes::Amount(amount) => SerializedType::from(amount), + XRPLTypes::Blob(blob) => SerializedType::from(blob), + XRPLTypes::Currency(currency) => SerializedType::from(currency), + XRPLTypes::Hash128(hash128) => SerializedType::from(hash128), + XRPLTypes::Hash160(hash160) => SerializedType::from(hash160), + XRPLTypes::Hash256(hash256) => SerializedType::from(hash256), + XRPLTypes::Path(path) => SerializedType::from(path), + XRPLTypes::PathSet(path_set) => SerializedType::from(path_set), + XRPLTypes::PathStep(path_step) => SerializedType::from(path_step), + XRPLTypes::Vector256(vector256) => SerializedType::from(vector256), + XRPLTypes::STArray(st_array) => st_array.0, + XRPLTypes::STObject(st_object) => st_object.0, + XRPLTypes::UInt8(value) => SerializedType(value.to_be_bytes().to_vec()), + XRPLTypes::UInt16(value) => SerializedType(value.to_be_bytes().to_vec()), + XRPLTypes::UInt32(value) => SerializedType(value.to_be_bytes().to_vec()), + XRPLTypes::UInt64(value) => SerializedType(value.to_be_bytes().to_vec()), + XRPLTypes::Unknown => SerializedType(vec![]), + } + } +} + +/// Contains a serialized buffer of a Serializer type. +#[derive(Debug, Clone)] pub struct SerializedType(Vec); /// Class for serializing and deserializing Lists of objects. @@ -36,14 +185,299 @@ pub struct SerializedType(Vec); /// See Array Fields: /// `` #[derive(Debug)] -pub struct SerializedList(SerializedType); +pub struct STArray(SerializedType); + +impl STArray { + /// Create a SerializedArray from a serde_json::Value. + /// + /// ``` + /// use xrpl::core::types::STArray; + /// use serde_json::Value; + /// use hex::ToHex; + /// + /// let array_end_marker = [0xF1]; + /// let memo = r#"{ + /// "Memo": { + /// "MemoType": "687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E65726963", + /// "MemoData": "72656E74" + /// } + /// }"#; + /// let memo_hex = "EA7C1F687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E657269637D0472656E74E1"; + /// let expected_json = Value::Array(vec![serde_json::from_str(memo).unwrap(), serde_json::from_str(memo).unwrap()]); + /// let expected_hex = memo_hex.to_owned() + memo_hex + &array_end_marker.to_vec().encode_hex_upper::(); + /// let st_array = STArray::try_from_value(expected_json).unwrap(); + /// let actual_hex = hex::encode_upper(st_array.as_ref()); + /// + /// assert_eq!(actual_hex, expected_hex); + /// ``` + pub fn try_from_value(value: Value) -> Result { + if let Some(array) = value.as_array() { + if !array.is_empty() && array.iter().filter(|v| v.is_object()).count() != array.len() { + Err!(exceptions::XRPLSerializeArrayException::ExpectedObjectArray) + } else { + let mut serializer = BinarySerializer::new(); + for object in array { + let obj = match object { + Value::Object(map) => map, + _ => { + return Err!( + exceptions::XRPLSerializeArrayException::ExpectedObjectArray + ) + } + }; + let transaction = STObject::try_from_value(Value::Object(obj.clone()), false)?; + serializer.append(transaction.as_ref().to_vec().as_mut()); + } + serializer.append(ARRAY_END_MARKER.to_vec().as_mut()); + Ok(STArray(serializer.into())) + } + } else { + Err!(exceptions::XRPLSerializeArrayException::ExpectedArray) + } + } +} + +impl XRPLType for STArray { + type Error = anyhow::Error; + + fn new(buffer: Option<&[u8]>) -> Result { + if let Some(data) = buffer { + Ok(STArray(SerializedType(data.to_vec()))) + } else { + Ok(STArray(SerializedType(vec![]))) + } + } +} + +impl AsRef<[u8]> for STArray { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} /// Class for serializing/deserializing Indexmaps of objects. /// /// See Object Fields: /// `` #[derive(Debug)] -pub struct SerializedMap(SerializedType); +pub struct STObject(SerializedType); + +impl STObject { + /// Create a SerializedMap from a serde_json::Value. + /// + /// ``` + /// use xrpl::core::types::STObject; + /// + /// let expected_json = r#"{ + /// "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3", + /// "Fee": "10", + /// "Flags": 0, + /// "Sequence": 103929, + /// "SigningPubKey": "028472865AF4CB32AA285834B57576B7290AA8C31B459047DB27E16F418D6A7166", + /// "TakerGets": { + /// "value": "1694.768", + /// "currency": "ILS", + /// "issuer": "rNPRNzBB92BVpAhhZr4iXDTveCgV5Pofm9" + /// }, + /// "TakerPays": "98957503520", + /// "TransactionType": "OfferCreate", + /// "TxnSignature": "304502202ABE08D5E78D1E74A4C18F2714F64E87B8BD57444AFA5733109EB3C077077520022100DB335EE97386E4C0591CAC024D50E9230D8F171EEB901B5E5E4BD6D1E0AEF98C" + /// }"#; + /// + /// let buffer = "120007220000000024000195F964400000170A53AC2065D5460561E\ + /// C9DE000000000000000000000000000494C53000000000092D70596\ + /// 8936C419CE614BF264B5EEB1CEA47FF468400000000000000A73210\ + /// 28472865AF4CB32AA285834B57576B7290AA8C31B459047DB27E16F\ + /// 418D6A71667447304502202ABE08D5E78D1E74A4C18F2714F64E87B\ + /// 8BD57444AFA5733109EB3C077077520022100DB335EE97386E4C059\ + /// 1CAC024D50E9230D8F171EEB901B5E5E4BD6D1E0AEF98C811439408\ + /// A69F0895E62149CFCC006FB89FA7D1E6E5D"; + /// let value = serde_json::from_str(expected_json).unwrap(); + /// let serialized_map = STObject::try_from_value(value, false).unwrap(); + /// let hex = hex::encode_upper(serialized_map.as_ref()); + /// assert_eq!(hex, buffer); + /// ``` + pub fn try_from_value(value: Value, signing_only: bool) -> Result { + let object = match value { + Value::Object(map) => map, + _ => return Err!(exceptions::XRPLSerializeMapException::ExpectedObject), + }; + let mut serializer = BinarySerializer::new(); + let mut value_xaddress_handled = Map::new(); + for (field, value) in &object { + if let Some(value) = value.as_str() { + if is_valid_xaddress(value) { + let handled_xaddress = handle_xaddress(field.into(), value.into())?; + if let Some(handled_tag) = handled_xaddress.get(SOURCE_TAG) { + if let Some(object_tag) = object.get(SOURCE_TAG) { + if handled_tag != object_tag { + return Err!( + exceptions::XRPLSerializeMapException::AccountMismatchingTags + ); + } + } + } + if let Some(handled_tag) = handled_xaddress.get(DESTINATION_TAG) { + if let Some(object_tag) = object.get(DESTINATION_TAG) { + if handled_tag != object_tag { + return Err!( + exceptions::XRPLSerializeMapException::DestinationMismatchingTags + ); + } + } + } + value_xaddress_handled.extend(handled_xaddress); + } else if field == "TransactionType" { + let transaction_type_code = match get_transaction_type_code(value) { + Some(code) => code, + None => { + return Err!( + exceptions::XRPLSerializeMapException::UnknownTransactionType( + value + ) + ) + } + }; + value_xaddress_handled.insert( + field.to_owned(), + Value::Number(transaction_type_code.to_owned().into()), + ); + } else if field == "TransactionResult" { + let transaction_result_code = match get_transaction_result_code(value) { + Some(code) => code, + None => { + return Err!( + exceptions::XRPLSerializeMapException::UnknownTransactionResult( + value + ) + ) + } + }; + value_xaddress_handled.insert( + field.to_owned(), + Value::Number(transaction_result_code.to_owned().into()), + ); + } else if field == "LedgerEntryType" { + let ledger_entry_type_code = match get_transaction_type_code(value) { + Some(code) => code, + None => { + return Err!( + exceptions::XRPLSerializeMapException::UnknownLedgerEntryType( + value + ) + ) + } + }; + value_xaddress_handled.insert( + field.to_owned(), + Value::Number(ledger_entry_type_code.to_owned().into()), + ); + } else { + value_xaddress_handled + .insert(field.to_owned(), Value::String(value.to_owned())); + } + } else { + value_xaddress_handled.insert(field.to_owned(), value.clone()); + } + } + + let mut sorted_keys: Vec = Vec::new(); + for (field, _) in &value_xaddress_handled { + let field_instance = get_field_instance(&field); + if let Some(field_instance) = field_instance { + if value_xaddress_handled.contains_key(&field_instance.name) + && field_instance.is_serialized + { + sorted_keys.push(field_instance); + } + } + } + sorted_keys.sort_by_key(|k| k.ordinal); + if signing_only { + sorted_keys.retain(|k| k.is_signing); + } + let mut is_unl_modify = false; + + for field_instance in &sorted_keys { + let associated_value = match value_xaddress_handled.get(&field_instance.name) { + Some(value) => value, + None => Err(anyhow::anyhow!( + "Error prossessing field: {}", + field_instance.name + ))?, + }; + let associated_value = XRPLTypes::from_value( + &field_instance.associated_type, + associated_value.to_owned(), + )?; + let associated_value: SerializedType = associated_value.into(); + if field_instance.name == "TransactionType" + && associated_value.to_string() == UNL_MODIFY_TX_TYPE + { + is_unl_modify = true; + } + let is_unl_modify_workaround = field_instance.name == "Account" && is_unl_modify; + + serializer.write_field_and_value( + field_instance.to_owned(), + associated_value.as_ref(), + is_unl_modify_workaround, + ); + if field_instance.associated_type == ST_OBJECT { + serializer.append(OBJECT_END_MARKER_BYTES.to_vec().as_mut()); + } + } + + Ok(STObject(serializer.into())) + } +} + +impl XRPLType for STObject { + type Error = anyhow::Error; + + fn new(buffer: Option<&[u8]>) -> Result { + if let Some(data) = buffer { + Ok(STObject(SerializedType(data.to_vec()))) + } else { + Ok(STObject(SerializedType(vec![]))) + } + } +} + +impl AsRef<[u8]> for STObject { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +fn handle_xaddress(field: Cow, xaddress: Cow) -> Result> { + let (classic_address, tag, _is_test_net) = match xaddress_to_classic_address(&xaddress) { + Ok((classic_address, tag, is_test_net)) => (classic_address, tag, is_test_net), + Err(e) => return Err!(e), + }; + if let Some(tag) = tag { + if &field == DESTINATION { + let tag_name = DESTINATION_TAG; + Ok(Map::from_iter(vec![ + (field.to_string(), Value::String(classic_address)), + (tag_name.to_string(), Value::Number(tag.into())), + ])) + } else if &field == ACCOUNT { + let tag_name = SOURCE_TAG; + Ok(Map::from_iter(vec![ + (field.to_string(), Value::String(classic_address)), + (tag_name.to_string(), Value::Number(tag.into())), + ])) + } else { + Err!(exceptions::XRPLSerializeMapException::DisallowedTag { field: &field }) + } + } else { + Ok(Map::from_iter(vec![( + field.to_string(), + Value::String(classic_address), + )])) + } +} /// An XRPL Type will implement this trait. /// @@ -114,6 +548,27 @@ pub trait TryFromParser { Self: Sized; } +impl ToString for SerializedType { + /// Get the hex representation of the SerializedType bytes. + fn to_string(&self) -> String { + hex::encode_upper(self.0.as_slice()) + } +} + +impl From> for SerializedType { + /// Create a SerializedType from a Vec. + fn from(buffer: Vec) -> Self { + SerializedType(buffer) + } +} + +impl AsRef<[u8]> for SerializedType { + /// Get a reference of the byte representation. + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + impl From for SerializedType where T: XRPLType + AsRef<[u8]>, diff --git a/src/lib.rs b/src/lib.rs index 90792129..8e527e20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,8 @@ pub mod macros; feature = "transactions" ))] pub mod models; +#[cfg(feature = "transaction-helpers")] +pub mod transaction; #[cfg(feature = "utils")] pub mod utils; pub mod wallet; diff --git a/src/models/amount/xrp_amount.rs b/src/models/amount/xrp_amount.rs index b4e6a6a5..91c3d6a1 100644 --- a/src/models/amount/xrp_amount.rs +++ b/src/models/amount/xrp_amount.rs @@ -111,6 +111,14 @@ impl<'a> TryInto for XRPAmount<'a> { } } +impl<'a> TryInto> for XRPAmount<'a> { + type Error = anyhow::Error; + + fn try_into(self) -> Result, Self::Error> { + Ok(self.0) + } +} + impl<'a> PartialOrd for XRPAmount<'a> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.0.cmp(&other.0)) diff --git a/src/models/results/mod.rs b/src/models/results/mod.rs index 200b8b2b..a52d7406 100644 --- a/src/models/results/mod.rs +++ b/src/models/results/mod.rs @@ -16,11 +16,13 @@ pub mod exceptions; pub mod fee; pub mod ledger; pub mod server_state; +pub mod submit; pub use account_info::*; pub use fee::*; pub use ledger::*; pub use server_state::*; +pub use submit::*; use crate::Err; @@ -32,6 +34,7 @@ pub enum XRPLResult<'a> { AccountInfo(AccountInfo<'a>), Ledger(Ledger<'a>), ServerState(ServerState<'a>), + Submit(Submit<'a>), Other(Value), } @@ -59,6 +62,32 @@ impl<'a> From> for XRPLResult<'a> { } } +impl<'a> From> for XRPLResult<'a> { + fn from(submit: Submit<'a>) -> Self { + XRPLResult::Submit(submit) + } +} + +impl<'a> From for XRPLResult<'a> { + fn from(value: Value) -> Self { + XRPLResult::Other(value) + } +} + +impl<'a> TryInto for XRPLResult<'a> { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + match self { + XRPLResult::Other(value) => Ok(value), + res => match serde_json::to_value(res) { + Ok(value) => Ok(value), + Err(e) => Err!(e), + }, + } + } +} + impl XRPLResult<'_> { pub(crate) fn get_name(&self) -> String { match self { @@ -66,6 +95,7 @@ impl XRPLResult<'_> { XRPLResult::AccountInfo(_) => "AccountInfo".to_string(), XRPLResult::Ledger(_) => "Ledger".to_string(), XRPLResult::ServerState(_) => "ServerState".to_string(), + XRPLResult::Submit(_) => "Submit".to_string(), XRPLResult::Other(_) => "Other".to_string(), } } diff --git a/src/models/results/submit.rs b/src/models/results/submit.rs new file mode 100644 index 00000000..3afd25c8 --- /dev/null +++ b/src/models/results/submit.rs @@ -0,0 +1,42 @@ +use core::convert::TryFrom; + +use alloc::borrow::Cow; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{models::results::exceptions::XRPLResultException, Err}; + +use super::XRPLResult; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Submit<'a> { + engine_result: Cow<'a, str>, + engine_result_code: i32, + engine_result_message: Cow<'a, str>, + tx_blob: Cow<'a, str>, + tx_json: Value, + accepted: Option, + account_sequence_available: Option, + account_sequence_next: Option, + applied: Option, + broadcast: Option, + kept: Option, + queued: Option, + open_ledger_cost: Option>, + validated_ledger_index: Option, +} + +impl<'a> TryFrom> for Submit<'a> { + type Error = anyhow::Error; + + fn try_from(result: XRPLResult<'a>) -> Result { + match result { + XRPLResult::Submit(server_state) => Ok(server_state), + res => Err!(XRPLResultException::UnexpectedResultType( + "Submit".to_string(), + res.get_name() + )), + } + } +} diff --git a/src/models/transactions/exceptions.rs b/src/models/transactions/exceptions.rs index a16503c0..bbee7124 100644 --- a/src/models/transactions/exceptions.rs +++ b/src/models/transactions/exceptions.rs @@ -22,6 +22,16 @@ pub enum XRPLTransactionException<'a> { #[cfg(feature = "std")] impl<'a> alloc::error::Error for XRPLTransactionException<'a> {} +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLTransactionFieldException<'a> { + #[error("Transaction is missing common field `{0:?}`")] + FieldMissing(&'a str), + #[error("There is no transaction common field `{0:?}`")] + InvalidCommonField(&'a str), + #[error("There is no account field named `{0:?}`")] + UnknownAccountField(&'a str), +} + #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum XRPLAccountSetException<'a> { /// A fields value exceeds its maximum value. diff --git a/src/models/transactions/mod.rs b/src/models/transactions/mod.rs index 4c2254b1..ff72c422 100644 --- a/src/models/transactions/mod.rs +++ b/src/models/transactions/mod.rs @@ -236,6 +236,21 @@ where } } +impl CommonFields<'_, T> +where + T: IntoEnumIterator + Serialize + core::fmt::Debug, +{ + pub fn is_signed(&self) -> bool { + if let Some(signers) = &self.signers { + signers + .iter() + .all(|signer| signer.txn_signature.len() > 0 && signer.signing_pub_key.len() > 0) + } else { + self.txn_signature.is_some() && self.signing_pub_key.is_some() + } + } +} + impl<'a, T> Transaction<'a, T> for CommonFields<'a, T> where T: IntoEnumIterator + Serialize + PartialEq + core::fmt::Debug, @@ -295,9 +310,9 @@ pub struct Memo { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone, new)] #[serde(rename_all = "PascalCase")] pub struct Signer<'a> { - account: Cow<'a, str>, - txn_signature: Cow<'a, str>, - signing_pub_key: Cow<'a, str>, + pub account: Cow<'a, str>, + pub txn_signature: Cow<'a, str>, + pub signing_pub_key: Cow<'a, str>, } /// Standard functions for transactions. diff --git a/src/models/transactions/payment.rs b/src/models/transactions/payment.rs index 6be16a46..e4dab26c 100644 --- a/src/models/transactions/payment.rs +++ b/src/models/transactions/payment.rs @@ -119,7 +119,7 @@ impl<'a> Transaction<'a, PaymentFlag> for Payment<'a> { } fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, PaymentFlag> { - self.common_fields.get_mut_common_fields() + &mut self.common_fields } } diff --git a/src/transaction/exceptions.rs b/src/transaction/exceptions.rs new file mode 100644 index 00000000..d0c0298a --- /dev/null +++ b/src/transaction/exceptions.rs @@ -0,0 +1,7 @@ +use thiserror_no_std::Error; + +#[derive(Debug, PartialEq, Error)] +pub enum XRPLMultisignException { + #[error("No signers set in the transaction. Use `sign` function with `multisign = true`.")] + NoSigners, +} diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs new file mode 100644 index 00000000..71b662ed --- /dev/null +++ b/src/transaction/mod.rs @@ -0,0 +1,4 @@ +pub mod exceptions; +mod multisign; + +pub use multisign::*; diff --git a/src/transaction/multisign.rs b/src/transaction/multisign.rs new file mode 100644 index 00000000..8159bdb1 --- /dev/null +++ b/src/transaction/multisign.rs @@ -0,0 +1,80 @@ +use core::fmt::Debug; + +use alloc::vec::Vec; +use anyhow::Result; +use serde::Serialize; +use strum::IntoEnumIterator; + +use crate::{ + core::addresscodec::decode_classic_address, models::transactions::Transaction, + transaction::exceptions::XRPLMultisignException, Err, +}; + +pub fn multisign<'a, T, F>(transaction: &mut T, tx_list: &'a Vec) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq + 'a, + T: Transaction<'a, F>, +{ + let mut decoded_tx_signers = Vec::new(); + for tx in tx_list { + let tx_signers = match tx.get_common_fields().signers.as_ref() { + Some(signers) => signers, + None => return Err!(XRPLMultisignException::NoSigners), + }; + let tx_signer = match tx_signers.get(0) { + Some(signer) => signer, + None => return Err!(XRPLMultisignException::NoSigners), + }; + decoded_tx_signers.push(tx_signer.clone()); + } + decoded_tx_signers + .sort_by_key(|signer| decode_classic_address(signer.account.as_ref()).unwrap()); + transaction.get_mut_common_fields().signers = Some(decoded_tx_signers); + + Ok(()) +} + +#[cfg(test)] +mod test { + use alloc::borrow::Cow; + + use super::*; + use crate::asynch::transaction::sign; + use crate::models::transactions::AccountSet; + use crate::wallet::Wallet; + + #[tokio::test] + async fn test_multisign() { + let wallet = Wallet::new("sEdT7wHTCLzDG7ueaw4hroSTBvH7Mk5", 0).unwrap(); + let wallet1 = Wallet::create(None).unwrap(); + let wallet2 = Wallet::create(None).unwrap(); + let mut multi_signed_tx = AccountSet::new( + Cow::from(wallet.classic_address.clone()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("6578616d706c652e636f6d".into()), // "example.com" + None, + None, + None, + None, + None, + None, + ); + let mut tx_1 = multi_signed_tx.clone(); + sign(&mut tx_1, &wallet1, true).unwrap(); + let mut tx_2 = multi_signed_tx.clone(); + sign(&mut tx_2, &wallet2, true).unwrap(); + let tx_list = [tx_1.clone(), tx_2.clone()].to_vec(); + + multisign(&mut multi_signed_tx, &tx_list).unwrap(); + assert!(multi_signed_tx.get_common_fields().is_signed()); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3a076c45..7f06707e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,6 +2,7 @@ pub mod exceptions; pub mod time_conversion; +pub(crate) mod transactions; pub mod xrpl_conversion; pub use self::time_conversion::*; @@ -69,7 +70,7 @@ pub fn is_iso_hex(value: &str) -> bool { } /// Generate a random id. -pub fn get_random_id(rng: &mut T) -> String { +pub fn get_random_id(rng: &mut T) -> String { let id: u32 = rng.gen(); id.to_string() } diff --git a/src/utils/transactions.rs b/src/utils/transactions.rs new file mode 100644 index 00000000..739954f5 --- /dev/null +++ b/src/utils/transactions.rs @@ -0,0 +1,83 @@ +use core::fmt::Debug; + +use anyhow::Result; +use serde::{de::DeserializeOwned, Serialize}; +use strum::IntoEnumIterator; + +use crate::{ + models::transactions::{Transaction, XRPLTransactionFieldException}, + Err, +}; + +pub fn get_transaction_field_value<'a, F, T, R>(transaction: &T, field_name: &str) -> Result +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize, + R: DeserializeOwned, +{ + match serde_json::to_value(transaction) { + Ok(transaction_json) => match transaction_json.get(field_name) { + Some(common_field_value) => { + match serde_json::from_value::(common_field_value.clone()) { + Ok(val) => Ok(val), + Err(error) => Err!(error), + } + } + None => Err!(XRPLTransactionFieldException::FieldMissing(field_name)), + }, + Err(error) => Err!(error), + } +} + +pub fn set_transaction_field_value<'a, F, T, V>( + transaction: &mut T, + field_name: &str, + field_value: V, +) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize + DeserializeOwned, + V: Serialize, +{ + match serde_json::to_value(&mut *transaction) { + Ok(mut transaction_json) => { + transaction_json[field_name] = match serde_json::to_value(field_value) { + Ok(json_value) => json_value, + Err(error) => return Err!(error), + }; + match serde_json::from_value::(transaction_json) { + Ok(val) => { + *transaction = val; + Ok(()) + } + Err(error) => Err!(error), + } + } + Err(error) => Err!(error), + } +} + +pub fn validate_transaction_has_field<'a, T, F>(transaction: &T, field_name: &str) -> Result<()> +where + F: IntoEnumIterator + Serialize + Debug + PartialEq, + T: Transaction<'a, F> + Serialize, +{ + match serde_json::to_value(transaction) { + Ok(transaction_json) => match transaction_json.get(field_name) { + Some(_) => Ok(()), + None => Err!(XRPLTransactionFieldException::FieldMissing(field_name)), + }, + Err(error) => Err!(error), + } +} + +pub fn validate_common_fied(common_field_name: &str) -> Result<()> { + match common_field_name { + "Account" | "TransactionType" | "Fee" | "Sequence" | "AccountTxnID" | "Flags" + | "LastLedgerSequence" | "Memos" | "NetworkID" | "Signers" | "SourceTag" + | "SigningPubKey" | "TicketSequence" | "TxnSignature" => Ok(()), + _ => Err!(XRPLTransactionFieldException::InvalidCommonField( + common_field_name + )), + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e6ad0d4d..d3fe32d6 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -25,7 +25,6 @@ mod embedded_ws_clients { use super::constants::*; use anyhow::anyhow; use anyhow::Result; - use rand::rngs::OsRng; use std::io; use tokio::net::TcpStream; use tokio_util::codec::Framed; @@ -41,12 +40,12 @@ mod embedded_ws_clients { Framed, Vec, io::Error, - OsRng, + rand_core::OsRng, SingleExecutorMutex, WebsocketOpen, >, > { - let rng = OsRng {}; + let rng = rand_core::OsRng; let url = ECHO_WS_SERVER.parse().unwrap(); match AsyncWebsocketClient::open(rng, stream, url).await { Ok(websocket) => {