From eae53490709157b23d2dd86e0c0e879a7a7b4594 Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Thu, 28 Oct 2021 14:18:23 -0700 Subject: [PATCH 1/3] Update documentation url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c9624ff..df3212dd 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ version = "0.1.1" # 🕮 Documentation [![docs_status]][docs] -Documentation is available [here](https://docs.rs/xrpl). +Documentation is available [here](https://docs.rs/xrpl-rust). ## ⛮ Quickstart TODO - Most core functionality is in place and working. From 31137029df4092bf74d88041910f63e560e69f0c Mon Sep 17 00:00:00 2001 From: Tanveer Wahid Date: Thu, 28 Oct 2021 14:38:11 -0700 Subject: [PATCH 2/3] Remove test code --- src/core/types/amount.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/types/amount.rs b/src/core/types/amount.rs index ffcd3d01..52c69ca6 100644 --- a/src/core/types/amount.rs +++ b/src/core/types/amount.rs @@ -430,14 +430,10 @@ mod test { let tests = load_data_tests(Some("Amount")); for test in tests { - extern crate std; - std::println!("{:?}", test.test_json); - //let data = test.test_json.clone(); let amount = Amount::try_from(test.test_json); if test.error.is_none() { assert_eq!(test.expected_hex, Some(amount.unwrap().to_string())); - //assert_eq!(data, serde_json::to_string(&amount).unwrap()); } else { assert!(amount.is_err()); } From a364ee2150bc22ee77f281849f791b35e8968643 Mon Sep 17 00:00:00 2001 From: LimpidCrypto Date: Sun, 9 Apr 2023 13:13:11 +0200 Subject: [PATCH 3/3] attempt to sign all commits --- .gitignore | 1 + CHANGELOG.md | 39 +- Cargo.toml | 72 +- README.md | 53 +- assets/xrpl-rust_black.png | Bin 0 -> 229432 bytes assets/xrpl-rust_white.png | Bin 0 -> 225434 bytes src/_anyhow/mod.rs | 10 + src/_serde/mod.rs | 311 ++++++++ src/constants.rs | 31 +- src/core/addresscodec/exceptions.rs | 15 +- src/core/addresscodec/mod.rs | 5 +- src/core/addresscodec/utils.rs | 1 + src/core/binarycodec/exceptions.rs | 10 +- src/core/definitions/types.rs | 2 +- src/core/keypairs/algorithms.rs | 30 +- src/core/keypairs/exceptions.rs | 9 +- src/core/keypairs/mod.rs | 9 +- src/core/keypairs/utils.rs | 4 +- src/core/types/account_id.rs | 2 +- src/core/types/amount.rs | 6 +- src/core/types/currency.rs | 53 +- src/core/types/exceptions.rs | 41 +- src/core/types/hash.rs | 2 +- src/core/types/mod.rs | 2 +- src/core/types/paths.rs | 59 +- src/core/types/vector256.rs | 14 +- src/lib.rs | 5 + src/models/amount/exceptions.rs | 10 + src/models/amount/issued_currency_amount.rs | 37 + src/models/amount/mod.rs | 64 ++ src/models/amount/xrp_amount.rs | 35 + src/models/currency/issued_currency.rs | 61 ++ src/models/currency/mod.rs | 40 + src/models/currency/xrp.rs | 90 +++ src/models/exceptions.rs | 25 +- src/models/ledger/mod.rs | 2 + src/models/ledger/objects/account_root.rs | 243 ++++++ src/models/ledger/objects/amendments.rs | 111 +++ src/models/ledger/objects/amm.rs | 184 +++++ src/models/ledger/objects/check.rs | 152 ++++ src/models/ledger/objects/deposit_preauth.rs | 101 +++ src/models/ledger/objects/directory_node.rs | 148 ++++ src/models/ledger/objects/escrow.rs | 167 ++++ src/models/ledger/objects/fee_settings.rs | 93 +++ src/models/ledger/objects/ledger_hashes.rs | 103 +++ src/models/ledger/objects/mod.rs | 59 ++ src/models/ledger/objects/negative_unl.rs | 110 +++ src/models/ledger/objects/nftoken_offer.rs | 155 ++++ src/models/ledger/objects/nftoken_page.rs | 121 +++ src/models/ledger/objects/offer.rs | 162 ++++ src/models/ledger/objects/pay_channel.rs | 167 ++++ src/models/ledger/objects/ripple_state.rs | 191 +++++ src/models/ledger/objects/signer_list.rs | 152 ++++ src/models/ledger/objects/ticket.rs | 101 +++ src/models/mod.rs | 108 ++- src/models/model.rs | 27 + src/models/requests/account_channels.rs | 99 +++ src/models/requests/account_currencies.rs | 69 ++ src/models/requests/account_info.rs | 82 ++ src/models/requests/account_lines.rs | 79 ++ src/models/requests/account_nfts.rs | 53 ++ src/models/requests/account_objects.rs | 104 +++ src/models/requests/account_offers.rs | 78 ++ src/models/requests/account_tx.rs | 100 +++ src/models/requests/book_offers.rs | 111 +++ src/models/requests/channel_authorize.rs | 178 +++++ src/models/requests/channel_verify.rs | 62 ++ src/models/requests/deposit_authorize.rs | 62 ++ src/models/requests/exceptions.rs | 76 ++ src/models/requests/fee.rs | 41 + src/models/requests/gateway_balances.rs | 71 ++ src/models/requests/ledger.rs | 105 +++ src/models/requests/ledger_closed.rs | 40 + src/models/requests/ledger_current.rs | 40 + src/models/requests/ledger_data.rs | 71 ++ src/models/requests/ledger_entry.rs | 270 +++++++ src/models/requests/manifest.rs | 47 ++ src/models/requests/mod.rs | 249 ++++++ src/models/requests/nft_buy_offers.rs | 61 ++ src/models/requests/nft_sell_offers.rs | 35 + src/models/requests/no_ripple_check.rs | 96 +++ src/models/requests/path_find.rs | 128 +++ src/models/requests/ping.rs | 39 + src/models/requests/random.rs | 40 + src/models/requests/ripple_path_find.rs | 104 +++ src/models/requests/server_info.rs | 40 + src/models/requests/server_state.rs | 45 ++ src/models/requests/submit.rs | 71 ++ src/models/requests/submit_multisigned.rs | 50 ++ src/models/requests/subscribe.rs | 118 +++ src/models/requests/transaction_entry.rs | 60 ++ src/models/requests/tx.rs | 63 ++ src/models/requests/unsubscribe.rs | 96 +++ src/models/response.rs | 1 + src/models/transactions/account_delete.rs | 211 +++++ src/models/transactions/account_set.rs | 729 ++++++++++++++++++ src/models/transactions/check_cancel.rs | 200 +++++ src/models/transactions/check_cash.rs | 282 +++++++ src/models/transactions/check_create.rs | 225 ++++++ src/models/transactions/deposit_preauth.rs | 270 +++++++ src/models/transactions/escrow_cancel.rs | 202 +++++ src/models/transactions/escrow_create.rs | 303 ++++++++ src/models/transactions/escrow_finish.rs | 285 +++++++ src/models/transactions/exceptions.rs | 328 ++++++++ src/models/transactions/mod.rs | 235 ++++++ .../transactions/nftoken_accept_offer.rs | 350 +++++++++ src/models/transactions/nftoken_burn.rs | 204 +++++ .../transactions/nftoken_cancel_offer.rs | 266 +++++++ .../transactions/nftoken_create_offer.rs | 490 ++++++++++++ src/models/transactions/nftoken_mint.rs | 439 +++++++++++ src/models/transactions/offer_cancel.rs | 196 +++++ src/models/transactions/offer_create.rs | 352 +++++++++ src/models/transactions/payment.rs | 532 +++++++++++++ .../transactions/payment_channel_claim.rs | 279 +++++++ .../transactions/payment_channel_create.rs | 226 ++++++ .../transactions/payment_channel_fund.rs | 211 +++++ .../pseudo_transactions/enable_amendment.rs | 130 ++++ .../transactions/pseudo_transactions/mod.rs | 7 + .../pseudo_transactions/set_fee.rs | 101 +++ .../pseudo_transactions/unl_modify.rs | 106 +++ src/models/transactions/set_regular_key.rs | 200 +++++ src/models/transactions/signer_list_set.rs | 498 ++++++++++++ src/models/transactions/ticket_create.rs | 196 +++++ src/models/transactions/trust_set.rs | 268 +++++++ src/models/utils.rs | 21 + src/utils/exceptions.rs | 29 +- src/utils/mod.rs | 65 +- src/utils/time_conversion.rs | 8 +- src/utils/xrpl_conversion.rs | 30 +- src/wallet/mod.rs | 29 +- tests/common.rs | 3 + tests/test_utils.rs | 11 + 132 files changed, 15168 insertions(+), 288 deletions(-) create mode 100644 assets/xrpl-rust_black.png create mode 100644 assets/xrpl-rust_white.png create mode 100644 src/_anyhow/mod.rs create mode 100644 src/_serde/mod.rs create mode 100644 src/models/amount/exceptions.rs create mode 100644 src/models/amount/issued_currency_amount.rs create mode 100644 src/models/amount/mod.rs create mode 100644 src/models/amount/xrp_amount.rs create mode 100644 src/models/currency/issued_currency.rs create mode 100644 src/models/currency/mod.rs create mode 100644 src/models/currency/xrp.rs create mode 100644 src/models/ledger/mod.rs create mode 100644 src/models/ledger/objects/account_root.rs create mode 100644 src/models/ledger/objects/amendments.rs create mode 100644 src/models/ledger/objects/amm.rs create mode 100644 src/models/ledger/objects/check.rs create mode 100644 src/models/ledger/objects/deposit_preauth.rs create mode 100644 src/models/ledger/objects/directory_node.rs create mode 100644 src/models/ledger/objects/escrow.rs create mode 100644 src/models/ledger/objects/fee_settings.rs create mode 100644 src/models/ledger/objects/ledger_hashes.rs create mode 100644 src/models/ledger/objects/mod.rs create mode 100644 src/models/ledger/objects/negative_unl.rs create mode 100644 src/models/ledger/objects/nftoken_offer.rs create mode 100644 src/models/ledger/objects/nftoken_page.rs create mode 100644 src/models/ledger/objects/offer.rs create mode 100644 src/models/ledger/objects/pay_channel.rs create mode 100644 src/models/ledger/objects/ripple_state.rs create mode 100644 src/models/ledger/objects/signer_list.rs create mode 100644 src/models/ledger/objects/ticket.rs create mode 100644 src/models/model.rs create mode 100644 src/models/requests/account_channels.rs create mode 100644 src/models/requests/account_currencies.rs create mode 100644 src/models/requests/account_info.rs create mode 100644 src/models/requests/account_lines.rs create mode 100644 src/models/requests/account_nfts.rs create mode 100644 src/models/requests/account_objects.rs create mode 100644 src/models/requests/account_offers.rs create mode 100644 src/models/requests/account_tx.rs create mode 100644 src/models/requests/book_offers.rs create mode 100644 src/models/requests/channel_authorize.rs create mode 100644 src/models/requests/channel_verify.rs create mode 100644 src/models/requests/deposit_authorize.rs create mode 100644 src/models/requests/exceptions.rs create mode 100644 src/models/requests/fee.rs create mode 100644 src/models/requests/gateway_balances.rs create mode 100644 src/models/requests/ledger.rs create mode 100644 src/models/requests/ledger_closed.rs create mode 100644 src/models/requests/ledger_current.rs create mode 100644 src/models/requests/ledger_data.rs create mode 100644 src/models/requests/ledger_entry.rs create mode 100644 src/models/requests/manifest.rs create mode 100644 src/models/requests/mod.rs create mode 100644 src/models/requests/nft_buy_offers.rs create mode 100644 src/models/requests/nft_sell_offers.rs create mode 100644 src/models/requests/no_ripple_check.rs create mode 100644 src/models/requests/path_find.rs create mode 100644 src/models/requests/ping.rs create mode 100644 src/models/requests/random.rs create mode 100644 src/models/requests/ripple_path_find.rs create mode 100644 src/models/requests/server_info.rs create mode 100644 src/models/requests/server_state.rs create mode 100644 src/models/requests/submit.rs create mode 100644 src/models/requests/submit_multisigned.rs create mode 100644 src/models/requests/subscribe.rs create mode 100644 src/models/requests/transaction_entry.rs create mode 100644 src/models/requests/tx.rs create mode 100644 src/models/requests/unsubscribe.rs create mode 100644 src/models/response.rs create mode 100644 src/models/transactions/account_delete.rs create mode 100644 src/models/transactions/account_set.rs create mode 100644 src/models/transactions/check_cancel.rs create mode 100644 src/models/transactions/check_cash.rs create mode 100644 src/models/transactions/check_create.rs create mode 100644 src/models/transactions/deposit_preauth.rs create mode 100644 src/models/transactions/escrow_cancel.rs create mode 100644 src/models/transactions/escrow_create.rs create mode 100644 src/models/transactions/escrow_finish.rs create mode 100644 src/models/transactions/exceptions.rs create mode 100644 src/models/transactions/mod.rs create mode 100644 src/models/transactions/nftoken_accept_offer.rs create mode 100644 src/models/transactions/nftoken_burn.rs create mode 100644 src/models/transactions/nftoken_cancel_offer.rs create mode 100644 src/models/transactions/nftoken_create_offer.rs create mode 100644 src/models/transactions/nftoken_mint.rs create mode 100644 src/models/transactions/offer_cancel.rs create mode 100644 src/models/transactions/offer_create.rs create mode 100644 src/models/transactions/payment.rs create mode 100644 src/models/transactions/payment_channel_claim.rs create mode 100644 src/models/transactions/payment_channel_create.rs create mode 100644 src/models/transactions/payment_channel_fund.rs create mode 100644 src/models/transactions/pseudo_transactions/enable_amendment.rs create mode 100644 src/models/transactions/pseudo_transactions/mod.rs create mode 100644 src/models/transactions/pseudo_transactions/set_fee.rs create mode 100644 src/models/transactions/pseudo_transactions/unl_modify.rs create mode 100644 src/models/transactions/set_regular_key.rs create mode 100644 src/models/transactions/signer_list_set.rs create mode 100644 src/models/transactions/ticket_create.rs create mode 100644 src/models/transactions/trust_set.rs create mode 100644 tests/common.rs create mode 100644 tests/test_utils.rs diff --git a/.gitignore b/.gitignore index 2fbef9f4..1ec67b52 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ Cargo.lock # VSCode .vscode +.idea # Additional src/main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a484fe..5c88321c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [[Incomplete]] -- Support for no_std environments +- JSONRPC +- Websockets +- Models +- Integration Tests +- Performance Benchmarks + ## [[Unreleased]] -### Modified -- Use the core hex library + +## [[v0.2.0-beta]] ### Added -- Initial Implementation +- Request models +- Transaction models +- Ledger models +- Utilize `anyhow` and `thiserror` for models +- Utilities regarding `serde` crate +- Utilities regarding `anyhow` crate +### Changed +- Use `serde_with` to reduce repetitive serialization skip attribute tags +- Use `strum_macros::Display` instead of manual `core::fmt::Display` +- Use `strum_macros::Display` for `CryptoAlgorithm` enum +- Separated `Currency` to `Currency` (`IssuedCurrency`, `XRP`) and `Amount` (`IssuedCurrencyAmount`, `XRPAmount`) +- Make `Wallet` fields public +- Updated crates: + - secp256k1 + - crypto-bigint + - serde_with + - criterion +### Fixed +- Broken documentation link +- Flatten hex exceptions missed from previous pass + +--- + +## [v0.1.1] - 2021-10-28 +Initial core release. + ### Added +- All Core functionality working with unit tests diff --git a/Cargo.toml b/Cargo.toml index 41817880..11b261e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xrpl-rust" -version = "0.1.1" +version = "0.2.0" edition = "2018" authors = ["Tanveer Wahid "] description = "A 100% Rust library to interact with the XRPL" @@ -18,30 +18,53 @@ tag-name = "{{version}}" [lib] name = "xrpl" crate-type = ["lib"] +proc-macro = true [dependencies] lazy_static = "1.4.0" -sha2 = "0.9.8" +sha2 = "0.10.2" rand_hc = "0.3.1" -ripemd160 = "0.9.1" +ripemd = "0.1.1" ed25519-dalek = "1.0.1" +secp256k1 = { version = "0.27.0", default-features = false, features = [ + "alloc", +] } +bs58 = { version = "0.4.0", default-features = false, features = [ + "check", + "alloc", +] } indexmap = { version = "1.7.0", features = ["serde"] } -strum = { version = "0.22.0", default-features = false } -strum_macros = { version = "0.22.0", default-features = false } regex = { version = "1.5.4", default-features = false } -num-bigint = { version = "0.4.2", default-features = false } -rust_decimal = { version = "1.17.0", default-features = false, features = ["serde"] } -chrono = { version = "0.4.19", default-features = false, features = ["alloc", "clock"] } +strum = { version = "0.24.1", default-features = false } +strum_macros = { version = "0.24.2", default-features = false } +crypto-bigint = { version = "0.5.1" } +rust_decimal = { version = "1.17.0", default-features = false, features = [ + "serde", +] } +chrono = { version = "0.4.19", default-features = false, features = [ + "alloc", + "clock", +] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } -bs58 = { version = "0.4.0", default-features = false, features = ["check", "alloc"] } -serde = { version = "1.0.130", default-features = false, features = ["derive"] } -serde_json = { version = "1.0.68", default-features = false, features = ["alloc"] } rand = { version = "0.8.4", default-features = false, features = ["getrandom"] } -secp256k1 = { version = "0.20.3", default-features = false, features = ["alloc"] } +serde = { version = "1.0.130", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.68", default-features = false, features = [ + "alloc", +] } +serde_with = "2.3.1" +serde_repr = "0.1" +zeroize = "1.5.7" +hashbrown = { version = "0.13.2", default-features = false, features = ["serde"] } +fnv = { version = "1.0.7", default-features = false } +derive-new = { version = "0.5.9", default-features = false } +thiserror-no-std = "2.0.2" +anyhow = { version ="1.0.69", default-features = false } [dev-dependencies] -criterion = "0.3.5" -cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] } +criterion = "0.4.0" +cargo-husky = { version = "1.5.0", default-features = false, features = [ + "user-hooks", +] } [[bench]] name = "benchmarks" @@ -49,19 +72,12 @@ harness = false [features] default = ["std", "core", "models", "utils"] -models = ["core"] +models = ["core", "transactions", "requests", "ledger"] +transactions = ["core", "amounts", "currencies"] +requests = ["core", "amounts", "currencies"] +ledger = ["core", "amounts", "currencies"] +amounts = ["core"] +currencies = ["core"] core = ["utils"] utils = [] -std = [ - "rand/std", - "regex/std", - "chrono/std", - "num-bigint/std", - "rand/std_rng", - "hex/std", - "rust_decimal/std", - "bs58/std", - "serde/std", - "indexmap/std", - "secp256k1/std" -] +std = ["rand/std", "regex/std", "chrono/std", "rand/std_rng", "hex/std", "rust_decimal/std", "bs58/std", "serde/std", "indexmap/std", "secp256k1/std"] diff --git a/README.md b/README.md index df3212dd..8fda08db 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,41 @@ # xrpl-rust ![Downloads](https://img.shields.io/crates/d/xrpl-rust) + [![latest]][crates.io] [![deps_status]][deps] [![audit_status]][audit] [![unit_status]][unit] [latest]: https://img.shields.io/crates/v/xrpl-rust.svg [crates.io]: https://crates.io/crates/xrpl-rust - [docs_status]: https://docs.rs/xrpl-rust/badge.svg -[docs]: https://docs.rs/xrpl-rust - +[docs]: https://docs.rs/xrpl-rust/latest/xrpl/ [deps_status]: https://deps.rs/repo/github/589labs/xrpl-rust/status.svg [deps]: https://deps.rs/repo/github/589labs/xrpl-rust - [audit_status]: https://github.com/589labs/xrpl-rust/actions/workflows/audit_test.yml/badge.svg [audit]: https://github.com/589labs/xrpl-rust/actions/workflows/audit_test.yml - [rustc]: https://img.shields.io/badge/rust-1.51.0%2B-orange.svg [rust]: https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html - [unit_status]: https://github.com/589labs/xrpl-rust/actions/workflows/unit_test.yml/badge.svg [unit]: https://github.com/589labs/xrpl-rust/actions/workflows/unit_test.yml - [contributors]: https://github.com/589labs/xrpl-rust/graphs/contributors [contributors_status]: https://img.shields.io/github/contributors/589labs/xrpl-rust.svg - [license]: https://opensource.org/licenses/ISC [license_status]: https://img.shields.io/badge/License-ISC-blue.svg + + + + + A Rust library to interact with the XRPL. Based off of the [xrpl-py](https://github.com/XRPLF/xrpl-py) library. -A pure Rust implementation for interacting with the XRP Ledger. The xrpl-rust +A pure Rust implementation for interacting with the XRP Ledger. The xrpl-rust crate simplifies the hardest parts of XRP Ledger interaction including -serialization and transaction signing while providing idiomatic Rust -functionality for XRP Ledger transactions and core server API (rippled) +serialization and transaction signing while providing idiomatic Rust +functionality for XRP Ledger transactions and core server API (rippled) objects. Interactions with this crate occur using data structures from this crate or -core [alloc](https://doc.rust-lang.org/alloc) types with the exception of -serde for JSON handling and indexmap for dictionaries. The goal is to ensure +core [alloc](https://doc.rust-lang.org/alloc) types with the exception of +serde for JSON handling and indexmap for dictionaries. The goal is to ensure this library can be used on devices without the ability to use a [std](hhttps://doc.rust-lang.org/std) environment. @@ -53,23 +52,25 @@ version = "0.1.1" # 🕮 Documentation [![docs_status]][docs] -Documentation is available [here](https://docs.rs/xrpl-rust). +Documentation is available [here](https://docs.rs/xrpl-rust). ## ⛮ Quickstart -TODO - Most core functionality is in place and working. -In Progres: -* Models -* Asynchronous ledger interactions - * JSON RPC - * API - * Websocket -* Benchmarks -* Integration tests +TODO - Most core functionality is in place and working. + +In Progress: + +- Models +- Asynchronous ledger interactions + - JSON RPC + - API + - Websocket +- Benchmarks +- Integration tests # ⚐ Flags -By default, the `std` and `core` features are enabled. +By default, the `std` and `core` features are enabled. To operate in a `#![no_std]` environment simply disable the defaults and enable features manually: @@ -88,7 +89,7 @@ This project exports [serde](https://serde.rs) for handling JSON. ### Indexmap -This project exports [indexmap](https://docs.rs/crate/indexmap) as `HashMap` is +This project exports [indexmap](https://docs.rs/crate/indexmap) as `HashMap` is not supported in the `alloc` crate. TODO: Support both. ## ⚙ #![no_std] @@ -101,5 +102,5 @@ If you want to contribute to this project, see [CONTRIBUTING](CONTRIBUTING.md). # 🗎 License [![license_status]][license] -The `xrpl-rust` library is licensed under the ISC License. +The `xrpl-rust` library is licensed under the ISC License. See [LICENSE](LICENSE) for more information. diff --git a/assets/xrpl-rust_black.png b/assets/xrpl-rust_black.png new file mode 100644 index 0000000000000000000000000000000000000000..6380a13e0f16ce568c540424623e1519b999b4cb GIT binary patch literal 229432 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

VG$L{yc!>o3?vrV# zZ2>MEb<8`KVUj5-4ACh0(Sa3*fuqlwywS6Y(8J#ODC`;McxhMN;ga!-o`W1x>2=nz zvud-&t*oVQ=2&UnD=cO1(2)y~5kB$6pO84eZn|f!)2R>P`?T#fKi%CpoU@uUe0U?O zHY*tx&&ykHrSAm%?{W^JLwsC?HlC#>AbmAUNJdx+UHf@(&_M#FJ?q z%84Agu}1;;F12Crytt3EKRE1pUQ2MC$;UIU6tEZ!<=!)f(+l-aas5%oolHim4=Ps) zW!Fao9-Wyzfwu<-hh>(3+-B-sq7Go+G9unej=#KpFKgB9=jNp0AQQG+S!e{(mX)Wg zX=5k9J>RtV{Aqq$1-|P17Hdi)v&Kva-e`M8oBA=u9)~^Ns@o>U;1ep}bJF=JD$^Tt9GN9K6RoL52|U zoK)}LrBs}mz|@G@`laJa#ecwctvUZL+|O4|yRWZ~D9c)v3>#>7)0f+lmo$s)!1#X% zr=xp9T`+3)LYc-^U$X_yvPbp)&H{bkJ*ZUT2V!MacA>}3R}ZP3!!-Bl;Gg7<5^z+k zZ|shoMiB-2&EWgF?Z#N2Fbn1ax|ORu>da+!P_f=(rmn zEv6QWF+WF8_|K`gr)eaEfuopV2~q0H@_v9yHKGp+Cm%g}RL&w>q;J2&N$F~&0=Tqi zrrsRel>g~?r=*UdUv)EitMgj+)9(?ZMh5ein-Jn5HqI1B9%x`$3bgtmk%_R(su=N$ zTW#f6(QJ}h-0e}xU(zS>>S-6^ryTD7sWSx|u3~KD)(l>tARgli@-_em?D+a?x0=~$ zT=JK$0Rfdvr((c8$ojvOYBn@BACEN?GP!MA`3ofR&vg@oVF!km0zLv}UG@6mqLn=` z*b^N}NoYm0KZ!a4hV8AiI9_{((J%k}tjT}{9b|~}C$2b{TG1pmjPAW5vpd{^@HELN zU?;VB=0c3p##bs_jjhUM8{razfn|+Cg1ZC=!#*b?hGds(rWlv(~?B$K02t>3eHirh^ks z@1l@mu`A{RT@xAr)uvea{y*`sdE6;o`&`_T7f{{tV2wzxGx*ZS*d_(M618kkVf4Of zWYU1dWPbX-y#7U7pbtV}4YsvKyJhl74Yz2wW?Cq9FT>AjjQPpm{_RjnEtgLhkFVIj zt&=K$%76cjm+f!By6^<0)2m-CqrqfArv$?Sc)5Nw0wM%ZcD=LO>X zcwS#FhKmXf1wRq3gx0_IvGxTo;;B4I{04h#ZO)iO_Q(32Ax%+#2tm*Uaau)P7joS$ zcfRv+Me%$rLOSZpnJo$CT^R&F_B>*x9KGJ{Ey44}%*mIuYWLADbDIX#^Dm~Sln8(Y zaCq2HQ#3Wh(}|->mu<;El}-P1&5huw{umRbwJl6qNHf*EU`!wUOZhr(?J)2A3VAlr zm2)Q?{3DCJ+rXip+|Y%&*I3zfZ3S#2xpTxN6RU)F7e<%8ZYbES<_1a1N5b32J58Tlui0** zM9es=iD>XD1;W%;WZ4!1#aHmz8F=uWsFf_(JJ@yDSrAe+Xw&CS436(K;0|4+YSf4G zRR}7W1M#E>UeTX`K*H-0MvRvT&^WUx<2ar}U97lD{Uof3fFIJ!_VWGl2B&i(8{y%8 zvhvE7KJt4D8;|`a>AV5l1j+#$ly>9=(Lt4<7_RO1sqCSKFYmiKzo;z&fbrr)vH4oP zrq+jQV;k?fJ{{dX?pRm1(H`}uamqaQ0F=Me{Wi#;1ZhHR%dtj5l5LtlUy!uj?~m8|a1Pfy=WHsgVSNu|>87s&ZX|3z_16{j`?j|d1F0H(+~ z?K;#C9Fdy7qHGF3^;-DazOG#TAAKM-xdaw~WqP(=6jrO%V{WyS1O8XwY5 zdkbGYj-M`flxC<*|2FbG*e_kmwopSd9@z+dGTWvtG0(Q84IW_YZ!M27>kem|p{KDk zVX}2ehIw+qrZ4QnN57;IX%9Q%3$t*hKN3}ChlckKPA7|*Q}HKfp|goGv-rShs4wIz zDnz@FUD3{_?d~(MmZ9 z5~Jnd5x!7OVD#6d(KKVg*W_M%?EVt<`%W;S-=EK1+d8m&hR>zXpf#c|4|VZ+-}rPb zUiIbyZqU~=VMMp9Qo->H!l;L_AA=ipt{=>J;EC`v7mWG-9#+fpzUC2OgFF45bTx{p zhd&ZX|BzBL#$QtKaCO89IhHKJN7zt2gn0WOmG_K`kZ*@wOSe&t2wvr6;-$6|4t=j% z9{G%8S?zP@4Ds8z-})d>5ni#k#C2NWk)>G)Nw`?BbWgMT`e0KGX%lp{MEk;tGL-@d zWVCblyTe_=)ePX-m4j0j~$KJ;~^unEbW(cRaxi*T3yN17HM8 zM3Onii2GjmWBiDkvnL;3%xi+V?j%!v9&p5)jy#|Q+8{V!?7ne0*OQbMqK53ZvJYcj z#&_UwAU?zN;4tZ=Co`yYZR}Bwt{zxw6P>m`ip@W{b1nVblDNlxX#Nv|E&`yR?r*S7 znu}Y%RD0k%U5xzHqWq*OZgms${8kme0*@xLR8_~`5=|YM#<@z5dQZjdp3uI)THN-o zq+6m-Xbn?~JZJVuipD;dp4X}t6+&6@_KtA}DV_Vf00?12sx!}t<8deTe&0cxq$I&y4%vygHUHZaL2LD7l z0ifht9x2r}tD#}wv6(S`Y%Wweq= z@i(p3dpjJmA%pUbeNaluWA9R_`ks4a*(38YmNZstU$ZKyg*!Vgk_~i@yW1qAsmRx3 z)gM9>F25NhCDAut+vRn@PHon16HEB@<$_}ZO)AGVP&fiqhlwr}Tv-rr0(M?Co%s+k zvUgwvJ%2ALr_y2|G79^9Y>}i2_}tHcez9x(=;x`B3KM}wf+3yway#80;V{ZM-etbg z!s`g)?xG~|g)BM)&^4O3qW5Qi%`eg3ho5Ci9ys8pEGjAD)Z;&YUe$4O;Jrz7AZmM{ zk$0IOVo!3kH{-a<)2Z_}C9`Pk_$|qHHl7VfK=EnfG#y{)&K6Y#opTCI1c>{*7dQ}ocitY!@4b;&owW#fYSUFI5^x5=-M zC8hCodhQ*v!1t+3jF?X3p2I1ze$o}FvC?aS!^iV9n~!4mhQdGOI~hM_kMcxn)?lBx z;L~dcNmy)byKVfj_Xi5RsZ0h^VGCd`b32z2xG6t!iD*)0=ScX%h`?-eo8cPCd#q#yR_;vl`I~FF}E9csj8w>Z2RU^@fTrfEM!? zIT|Ji7kZjEIr!{cSq3ENMO+dH@i=d z1km@_7m*e1bht>4+ZUAqCK)FErb!C4%sse&EQDMsh_Wk;;}J%AJadc2dg?4Jf|dWo zDgJ1>@4c>-Y$2Ej@|x(I?+m3&TOsTJ%D@|wBjWpxzd)#ut&*J%mLB(RATiW~-x&XQ z%j;C<7>|Op-;ISV@#92b7R=V>@}hi~07H zYBpEIiQ?YB(^`dyH_h~`7N&iRHy#A#@7b_*%W7)$838S}s<^SPz#Z4gI+0&r>85%7 zstXXTM_wh?I$3LXLfj=Ev!L!krARzr3#C&GX~el~ETX-Au9I!QCGIFOY||-7#O~lm zX&kA^xZ@x?etEK!3=zoyl;;2^-f=?EYo7ucV-i+D1$t$Fd&|rSv1d>m>ZAHY>=|*F z6#0Yf3V{x4gmear&>^toF*?IB#^R4dBA{f;%g!)?HSao>t;uS_7l-+e-Akb^Oc)#=Do#ls@jgd*sm3p zG|vpf^@c-or#twBLO0XcsDDr15(67Yx zCR)8D+X;Y6z%~nqnWgp@05&$#V8N=sC+T*$08D#bk z$ZuU!DE`m^`Xaogwk>83qFJbXKJR`N{ zkbxngOV}2CY>I_x9_V>nf52cGZjcLJWo=G)eH?_E7UMI~D;#*wUd@qb*UT6~gl~T* zmv-fQZ}n(-G&ST2Tt@HnNY=WCc%Sx$LFKvc`uy-26^)HDe&a&+ijOJ9@8TaY>u4Bq!N~5iTBTFlXcd4;h-GsA6cnJ-LZ^yIA>s&9z zM8j@c;hqH%V$y=A#1Up3Y~z6-@$mG7@w7YI+6AO&rKS7qv}x2t13aYV%ZMmYYY}zt zNnQs25#v)frsnT_qD-hG*`PIWmngxj>>&6N^@D2MPD;VDOkk{#Qhsv=bx+rZN>M z3-b*Hls1|d&dIN$UBR9;iqC&RZT4446{uuYbh`YsB?xb4!AQ^@Qr@*nf5{AQ`yPYL zMOxupcS9ym%js8PCl7N(z>B?f<`)s3R7YgR&8hAt*N%rQ=o+!BjmV0#prAsDA#q z%ZypoHk-u|b9WTyAq*T5;g@@S`vYOd$H7k;pJ}q7zFtW2oa2u+vQUvh7zFrHfx8p> zbpaI79YXx~?Y;XC$_L@Dui`&CWp+Irq5gV@c(3$SZa4xZmvtY~4NFt4!wdkn@xiLT zK>?55S(h03JT^`=hz_vXJtL?NRbmLFCHj?_0ru63bh>VST1Y5JjI4J_G#Ypj&Ukyq ztRMl{In4Lhd^t_Yffh5ifov?(ho1W#VlFSZSWYWKsgTx0*dT(pY~9Dbb^OU@=vh9y z8$dvu!?Zrj(?DjSM14%`Wx}vv=o58%lU!fXB+-ppeYb&eSYBRSC}9J)#MFqU`C5rF zh7%Fl4|=>#zH5jmHi?;shTS3PIvmB@{L9ScF$X$%9OOdLHvqM)9c@6Go4Ga4C%nJB zLAphne>=L;9XMn+Q26L4GGZD_M($-AE>!sLJd-hn$R8O8RalQP|5+=34j;n2-1_#K1%LjS58NrPRkBAFkPugha(eqchcU9Umr|;vMM!BlV=|Rs;Gb@$G6| zwcWv9?k4~+p2f}gpZmL#8>|hfH@M;|AmOGLx)NrO6|km#%^~w3D)0${CvwTESn)lV z(6#E3%B@8)yC`+9a)any(L`|$lqf2mmy>(Asm@1=NmTKz%4BP)JVoh2 z_p6mEfs8d5euN?wE5SbQr}ro8n#LM=AluX|yM*KMBguXWHjg#*F-M-9)<^m#+=UvH zTzS|J8J1+tLsR&Rn%uQ@f93MeOJ_ZC?^YH_^AMR<9L|KQkfoJt(#Tp(>v%Lfw6V(? zU9WiWOX~-^NB`hg-(VrAR_-(5pmg4=cz$RDvhdN%Kbgi`lMda$>`q@ARzfF5OzGrih)rBDN2mwbqEd-xD@ocuX5YR z{fUBhIYF`9Pv^N=7PcA{sIi!sGVzoqQ9S7|gBP1+a4Zuj*6Kc0n~YOA)6$vVsOgjR z?I_sbni)eKxgM^8(|8*cFZ*&Q^k6ynUAn%keGW=!q4jW+&mWoL6g51df82wQ{c_gy zACaZ=nLXn+JxSsa!Olf{ukcvt59$S9Jbxt8oMys!ElSn5qNOLKF=y>de;24l#_pLf zb&qPt-2sW&E3+UOA_j(-6*9|wi5b}1W|ZD5+*&{xGvhVYl^x3DQ!@Lk1@|L*VPZ-9 z32&~r9;nnlPLSz_iKr6l7Wc$6;+UuyE5Eead+}2c+C9!g7uG;CL8#;o$D*Eef&{PI z=kBw^neI0g_k)hMBK1YFHze{Dh{Zl(yJP$ipWF&ceuQZRk1%p<-RJ!;BZ+~)e5hwQg>hL!SKw45HDvH0 z@0*^*DlW5;7V1jTUw`hZr^Dn^+(1B7D%WyU6N7xAoi+}9)j1}-=4HW8#D8bbtN+d% z19etML#zawQ5w~~P^<0y2%PIH7vBW_Huhz^lpvuay79xdalr1H-fAJUqh)ys)h)*5#YJz0%85z30T zq>8}2!)X-*lTd@Gd^3@#Uy*v$q)`|sB}kanfjuXgwfZV4*8*C*gBv=Tayf@(o50AT z^=P3w@(bxFI70w%n^S06NG%j2E0Z8@YrULcrl!rUMR}YZGmO@XvY-h4r?OAiL335g zGqaqkylw;aL=T|)^0}rCmf3FhRP;QL&0;brHK|eYW(<6Nu77dw^_*UAy$W7&MB2&; zXn1K}$f3@Cmt&`&WNf^I@+Zr#(xQ*FUtFeG2rDSK!iUg}E4RX>@~iFKRL=+IpN{pC zbF0!*2hpzP<(W)itC|Ju)gQku@b`c{N}D!0*pSQ>7fn9&vv~3RY1VtPhm?-*UfCG_ zV*|eYuF4WtKpmZ>(rM6x{_=WMWPERLB;}a8JKAtpfo8&2<=oGdygr`2<#v{Gw&>>| zk4eJKL;43%)zm%wLv($u&;?Z5hUw9^j1n{nuM1$Y}Hi8zU-ed z?oxehTrFyGwhj_ced-Q-FVv@Qn)@(UEstuLXcV8PK&ykjrw|)UGz4~#D>LJni7|=v zc^BLCpvEPTYX7QOtWhsbK{dE;N>+Z%CUQL=vO?X?&{`U+c~?~ccwChQ$NEZ@^1 zXmUN}h5l?mj!&l31DfM>UiWWy(L(ajQREdz*CWBhxsGO<-Ov@mm!;DiSxXvQ@7)(B ziQo)(hEJ%D;yyC-ERl!y}Xne9p<2N$Im zK~Ej`&&OH*u#%r+mIZcXKd|>^6b7!c`Q93HbE48B?gJ|d#&O)B&oy*z+Xs)%fog@y z7lV$ts;N|{%{r;SYQ|mBBpdMohT8YC0=PL?V4rqA-;0^h-Ug~#oT-&$i4s?t^v2>9-`cMad1moXY1lLp0 zUgC6v=hvW?9vuGRy>Yw1c8ZoJFs!y6@D4ItTBb~-E2 z^)8zOXmire`y!Y6qhJ$l;AZ;_r>c(D<(1B8Nm*94aV?}`nv`4Wfx?UQ9dJWQ_1|6i8t@WO=ih$s@l^{Wp4{Q+@s z?G%Ihui39cx=+dj5~o;sU(i~bZNau-u5fOVo-wV;pTNpja{<%vPS(~JJH&YhGL!?H z1X1`lxKAC4l?(*qm2G9pES!`w&@&St63sh;*FAdSww^-7o5OSq`!4I9#!+qeLN~Be zT>3%|U*M|6_i@gjBLO$##GS1s8#zZtOH+U!==n}I{2PrXD)^)t+zyT^3xsh;JFqUv++V9x-NzHw5U1Uz}jsM;Flj!!55?oPMV2?IB}6eR#L zBA~S$rIFWoeYHMjgw1aIm}()yQWQXs3D?iTjk%>iKgCEt4*8Ot3LiFgDE)gV1t(>sQ+qlmUUFfT0PUV0l8)q4gEYKUnt%uHqFsRt;n2Ib2_D^eQDrB%-X4%2nta?CIT^ zvmU!UgY2e339LHrp^rEphvbOi4{2bc`FqEniwpKPg^KvE|2h6=!gKSpDk?^$1+`gn zQ^o;aYV>QF&&?cCv^xgH@KqJx0zofy#ZjyEFR|YMV%E4}c|w10xOr!bcBS-)^*Yw; zChaaaX6>b*pjY=vldCY5Ni1!)L{NpOJlru;BNp)WIMztu>53k<@&krlAz%2R3t zWFS~$?-PZ>a$r1DBu?Mbs3@-5XRhWz<1|B#ekMd@N=%ZQb3x)4X3 zHp>?F4v$p~*pJ%5I2}%77Kg;dH6n|N?^2|++sAl_zlF_ZBfg)a2_^`3!a01M{qF*J z?TIFO+X2Yk)!?l?Nm!X7$NI7;Oc7rIBdIO0SmW#42*9;!r6_6@JI~X|Pt1@*GJpSoGCC#%~;zI}|^Me^0o2drixCiwglgji{uj^IY`SZz=@B+zu#sxY-G z!U*7G22XQ%cyGV36r{(m;HN(Tu!0V*lM_ifd?B{UYPt(M^96xS`29mr!iA4WozzB3 zOy@ROVw=4y`RbA_iDGtfUp*Yb(%)E4hf)!gxRm4t>>52(~$b7cH({WWQB2z&9K*6aV# zbe(~0w_iJAuc%o{slE3~P}FMesy(V!?Gc;O5~Eh^+OevoYS&&BE2uqMn-E27)7s?C z^F06et1t1p?{lAXUFTfqoZ?>fUAmt#)IZ)2uMBYzXoRm4ANGU=Ws(2=s?T7^Oh6)! z%Zx47RPqDL@-Yv3-25ciBQp{93^AP#{uyO0*gu}5wfi%}HPvC`%(<)T8^g6`qlchK zk}$`0T+cvTmM!S1m1f5Fz%6BTK51c)y?`ubeL;7Lj|%+4=}B-? z&XY#F>p7ucH{=Jb9tlop&;}S!Jz6@n^_iZo)Rx~@3sPHqgyxC*u)tT2;utAH7_Xg; z9qn**AZ{Usf2~b8wdF23e=;LALY?}4zRob=xwFE)UXJv}#pnUMS01uM)hVE;s`WF0 zoyq6L)cJS?{)fl0KCWm3w;L~pkhHBYvQiR0)TsVbd5&2Y2S7`tuB*|N19Z?MTB zhBZ^5UXpzxBPvWD5H>~SHl-Nu40q9sU>ooWVZM&}`xPH)*00oTy$0zpC}gPF-WKGN zg5RZB;ion)=1OD1qlStCc(OK!Y9IrPBA`sv%!MDJEZx|hCtq%|h4S-tM67aj zO+t-PIWzHltr%pJQAQ1R*W{1V*Kyt^nC-}V{9{rp8KLv^WiFh%&KdIN`PjkC^~6n0 zqr#74!+4N7m9WjoS$z=iHP9n9&3A&dL9l_WLG7z0vgiH-^YbKb5M%xMyPYQzX0|mg z(Ijt6*Bbf3#_-4h?LSx=n-KErU-xkBij||G7mPTL?CYY7Oi~E#noR}-*#or%w=bSO z3W_ossuQ8vH~hKtS>eI{v}fe0`wugtYlS0O1ZKz;neI#dSRQ0S+&Rp&q`JOI{wMy+ z{TFJ}Uq-Y^VCj4@@58e!HJGSP%$U#3<@)NF%@Q7Zg@++nacga~`W}}pW|yBujC}iV zeCi0<%ngCzrRwcdf-)_ReS%t0UR@*O6j?hgS&MY#$1+P6y8upzXLosoYYZ4FT>0i( z9^S$CkgWN;uZATHK!+5^a;pgtVSchsOz*$NlWQ_g&%N0#JmJACW~JT)P8;$~{EEs% z)+L3Ds2qR(6JNr-t?Y8w*#Ntgdxt$Hjh2M&3mO04=z8YlAF0 zrdgg@4}XR>Vol}N#))uKur~6lkD75SC%AA^Xll!dMKag?@W$>Iw*jl47R;;Kb!3IM z^)bA=!v86cG(D`J+~cV1CW)ffDL-cI!Ms<<34u4O@9O(=x0h+^LOcyxREx5H3KtpR zk~XH^3i9de+hKC;Uj(}S@5r7BJRCdtNI4Vo@Gp;|{w^G8NG95d&8)+916ArY!T#B; zQ)$%B?`XaC-A+wi*(k`&rs5J}X`&l^!=h5wlaDklm6iCc3a@I=^256s;&OVHuTZ8U zs6}3zAs3kTluhhfgR3K>>hOnT2xCYPVWO>=BjMyq07@S&x%@p*5Pss#uFjFypApYR zlafO4$cQ7Zg!UsdJ6m^~vRPyr{wo7HL`q`Oiy}hqJKg1ldCcgF$225(@#q?F{9XSs z#uGxiPa4oKB%-=K1BBV2ERplVJKNgbXP4;YS>z~W(Bg14o1S+M=MfcVNC%wOqVJ%F zM?+wr9+a_hOfRc9kJRb1D8;_>(txIu?CD-5RJj`vlz>(!?svt0Q;`3N*ltE+=_w^g8g%ODST6(V8Z)sHYchFv;1F-LpHObaEE_d)^f!%N5r}a5|0f**gWf zltDEsnwUf^1u93bfsu=VvR=wqAZTxKwYEq&_;$neG7Ue82iCfeN)oAPj%@z3kGxeMR*v ziBy7~UGs^y5&H|8w6RqG6pbf$rXos=i=Riy7%$|9EyB({f$tseyrPpk9Mbw>F~xYo zltCh5gzkj|1v2nd+!2`+X#m{kZ)wK=I*otUpsHv)uZgNu(+hP<#!X1>01m8=ab!4p zpgXf1oM3xK=cUMIqj_$z@D%mE>(6-6KyGvn-}+s4ipD1I&wp`4?7QyC2c`-VyYp;- z-U**OtpD`0$QU57*57s5Si={r{EM~5?PgXcSX4JFQE{X%`}j$j|Cu6L6t4g^+aCqb zD(vV~-M||;!YcOs0zlIM=){nXK!^J%;>MW!NUqx#*Uq9$O<*3Oo;^H(*KIRk__x`7m zkG1FvjsD8&ZTM0CPVkqhN6gT4KKXUB#aGW;;*i25^#*ChthOvIPYC)n)02_BCIh`wO;;tUXA`bbiVA zm`_aeJzpA)FF~sQ!c`6%h)zgQ438PSbz-4SL&&nOa0QriIyg89E#yTB7^_rM3Np?c zWV0D>bEsAf2=#4CIs2N$N)kJPY6puc2SfI5!34cLc^a@FFF~_ap#5zT_kYkA+*%Twf&ZgSs~%bE3U5Mr-q(xK;*lVLnEpj`vLQNz*Qai zN1fHhMhxBk0K%ZsT-&V>c_yu^zb)6qao%wE-AX)}r#k}kpX_YlNOJEHNn!P*#mINl zAg$kdWB}X><$K|<>BruOF}p*&*RsAeG3}H;3>FE?rH2x1CScxq-TTtCxXmWyF~96N z_F6=mt)GBL`o=ATWs*oh*hw^zYd&D=a(-xC2-k(&2l+u>_NvS?swQ@X!V~=`Y%}oY zkWI1nZqMaMID8ZXRpc-fz^%Rg5|>ITt_zyyOG!*$C{)#{S0B-KZGUF5lw*%#fkV>6 zJO^Cxa=qKc$*cu6E;m@53oq-#uD8Z-P1T+~)k8;ebbtwB5)2nJ+xa61gST_Y0|Qk9 zK^*ybYWFnUh-8|2h4T-`_&St`O1?LgT=s2rcGVH?C`JDTl$7`-sk@KxEJYvb>YK?! z?D(?+I*wae_?Sae7^(8=s&;qXhb1dFuosJDld1JWe_|=|elYv#U>3{>!JCIhM3=;AjHJX_514M^EeyjNQG1(})sv)aIdX!AEvt-G4gbXmc&qZSUy^O^J zuSQQk10clL!uy!OXNHV0cWKaauk$_SVChX;tkk`;g@B41^&oqKQ(fdXoy=?^MW#&m z* znP96OR4Ll(mqyZ`w$33_dGZb~R60?YywzG_<04b}fM=AUi#zwcdxqQSIZ^FIS@U9l z5pjVmCc0aQI7|XbAwb&(-<+o7tt3ZjAs~9aNzW^Y&H+KSb`(+`*q*vTj;xITtW#IX zK3Z5oRzi>4$sycJzVbtzrT6pC6)<_>VZGI!l)vAdm}}aLF3)HYgAFIO{SElOu$LdQ zW&~R5$uN02*|b;otiPgtJO7$NL^%w9-!2=yTHAP4yU*%=K=Q-952ZmWY+UJrCu7!4 zMw(e;$0^RxIB8K;OEYM$LW=^DH$WPg{8}boPPk5x2_0J0JqLX8BtY;nrphzHZNhms zmIuwM+Lg0ev?o51zvRfO+=W(KXy0~;lW~48Z+{X?vH8cQ&i2Qy7{HjZtGecNcL-^RPNq;RHE&`0Grp}Mr*BfU;%rXAbY_&5GG=>?A#D&M zLRk*K-?`_+{I~_iK zW;#t6o> zo^AF*39TQR3Tm}2DSG}|CErDbZ!hIZ$-d0F%Pln8BLM$?k)5$GgugJtA5_Ft##F*o z7Jf`1nJ-UqPvO@glx{NYQmSl$_4D{Bv7yHe|AHVCAGWh(Za3Xf-d*fUdSIuZnIU@u zlkD_wgLQX*d%2J%Br7M>to&)*_)Cpc(vcM~8D4;S{#2y^00;+Ysy#ANJHvlB1$uHX zW@Szt=Y8MZkoVr=2i&!16)Pdj?j@4~q3!5s%xuRVIYdG;@Q?A+72j_Cuk$r3tS)hYf*d(Bp(TM`pV?%{YBB7+w4lk zrR&)-{Zi5TAxwT8pUQHiLbnUk+kl@pDnRP{5b6Z2{_|_+tvM-56R{OLugK`V8@BMg zkdOr=(-A$UliG-9LR^L^dH<70a=cXP3_`9C1J6>)^8{1 zdB=7sp`ONBWhjDNqE-9EguKI;Wo@}<6&h9eeM{l;iv2VYa;wiA#7*mb5~VQld%|wI zW98w6YHq)o3+Wjbs$FThZ>W$$4CYy>=J57n2#{e7Zhu35+Rg7ix$y}PU*#f=~2E zWuzs*x~G%nGbHjQ4Z+~5)-Lz-TRHcQ}Yr!eRl)W*KOR{LjvBZfR33;AZk*^*2RrhHr8~KN<+1b zPW>B*y`P&Svd+gA_f-xTI^PtD(?aXbBP3zo6Gf}}t2|@MZp=>8Aa|Lofy4%MrP!89 zV5T0%Fgdu#(ox~HI(d!9y`oE{k2iKY8NyLL-~{n%T?C;gWw4S#KV(fq36@1=3T~@5 zQ5KQ#FakW_;Fv;EAl}56y+JDbW=!(gfN�KM7wBUnEd$q7J3Mgd*8lBQAvi0I?@; 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 z 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 {{ + let error = $err.to_string().replace("\"", ""); + let boxed_error = ::alloc::boxed::Box::new(error); + let leaked_error: &'static str = ::alloc::boxed::Box::leak(boxed_error); + Err(anyhow::anyhow!(leaked_error)) + }}; +} diff --git a/src/_serde/mod.rs b/src/_serde/mod.rs new file mode 100644 index 00000000..8a7584e3 --- /dev/null +++ b/src/_serde/mod.rs @@ -0,0 +1,311 @@ +//! Serde functionalities + +use alloc::string::String; +use alloc::vec::Vec; +use core::fmt::Debug; +use core::hash::BuildHasherDefault; +use fnv::FnvHasher; +use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; +use strum::IntoEnumIterator; + +pub type HashMap = hashbrown::HashMap>; + +fn serialize_flag(flags: &Vec, s: S) -> Result +where + F: Serialize, + S: Serializer, +{ + let flags_value_result: Result = serde_json::to_value(flags); + match flags_value_result { + Ok(flags_as_value) => { + let flag_vec_result: Result, serde_json::Error> = + serde_json::from_value(flags_as_value); + match flag_vec_result { + Ok(flags_vec) => s.serialize_u32(flags_vec.iter().sum()), + Err(_) => { + // TODO: Find a way to use custom errors + Err(ser::Error::custom("SerdeIntermediateStepError: Failed to turn flags into `Vec` during serialization")) + } + } + } + Err(_) => Err(ser::Error::custom( + "SerdeIntermediateStepError: Failed to turn flags into `Value` during serialization", + )), + } +} + +fn deserialize_flags<'de, D, F>(d: D) -> Result, D::Error> +where + F: Serialize + IntoEnumIterator + Debug, + D: Deserializer<'de>, +{ + let flags_u32 = u32::deserialize(d)?; + + let mut flags_vec = Vec::new(); + for flag in F::iter() { + let check_flag_string_result: Result = + serde_json::to_string(&flag); + match check_flag_string_result { + Ok(check_flag_string) => { + let check_flag_u32_result = check_flag_string.parse::(); + match check_flag_u32_result { + Ok(check_flag) => { + if check_flag & flags_u32 == check_flag { + flags_vec.push(flag); + } else { + continue; + } + } + Err(_) => { + return Err(de::Error::custom("SerdeIntermediateStepError: Failed to turn flag into `u32` during deserialization")); + } + }; + } + Err(_) => { + return Err(de::Error::custom("SerdeIntermediateStepError: Failed to turn flag into `String` during deserialization")); + } + }; + } + + Ok(flags_vec) +} + +/// A `mod` to be used on transaction `flags` fields. It serializes the `Vec` into a `u32`, +/// representing the bit-flags, and deserializes the `u32` back into `Vec` for internal uses. +pub(crate) mod txn_flags { + use core::fmt::Debug; + + use crate::_serde::{deserialize_flags, serialize_flag}; + use alloc::vec::Vec; + + use serde::{Deserializer, Serialize, Serializer}; + + use strum::IntoEnumIterator; + + pub fn serialize(flags: &Option>, s: S) -> Result + where + F: Serialize, + S: Serializer, + { + if let Some(f) = flags { + serialize_flag(f, s) + } else { + s.serialize_u32(0) + } + } + + pub fn deserialize<'de, F, D>(d: D) -> Result>, D::Error> + where + F: Serialize + IntoEnumIterator + Debug, + D: Deserializer<'de>, + { + let flags_vec_result: Result, D::Error> = deserialize_flags(d); + match flags_vec_result { + Ok(flags_vec) => { + if flags_vec.is_empty() { + Ok(None) + } else { + Ok(Some(flags_vec)) + } + } + Err(error) => Err(error), + } + } +} + +pub(crate) mod lgr_obj_flags { + use core::fmt::Debug; + + use crate::_serde::{deserialize_flags, serialize_flag}; + use alloc::vec::Vec; + use serde::{Deserializer, Serialize, Serializer}; + use strum::IntoEnumIterator; + + pub fn serialize(flags: &Vec, s: S) -> Result + where + F: Serialize, + S: Serializer, + { + if !flags.is_empty() { + serialize_flag(flags, s) + } else { + s.serialize_u32(0) + } + } + + pub fn deserialize<'de, F, D>(d: D) -> Result, D::Error> + where + F: Serialize + IntoEnumIterator + Debug, + D: Deserializer<'de>, + { + deserialize_flags(d) + } +} + +/// A macro to tag a struct externally. With `serde` attributes, unfortunately it is not possible to +/// serialize a struct to json with its name as `key` and its fields as `value`. Example: +/// `{"Example":{"Field1":"hello","Field2":"world"}}` +/// +/// Several models need to be serialized in that format. This macro uses a helper to serialize and +/// deserialize to/from that format. +/// +/// Resource: https://github.com/serde-rs/serde/issues/554#issuecomment-249211775 +// TODO: Find a way to `#[skip_serializing_none]` +#[macro_export] +macro_rules! serde_with_tag { + ( + $(#[$attr:meta])* + pub struct $name:ident<$lt:lifetime> { + $( + $(#[$doc:meta])* + pub $field:ident: $ty:ty, + )* + } + ) => { + $(#[$attr])* + pub struct $name<$lt> { + $( + $(#[$doc])* + pub $field: $ty, + )* + } + + impl<$lt> ::serde::Serialize for $name<$lt> { + fn serialize(&self, serializer: S) -> ::core::result::Result + where + S: ::serde::Serializer + { + #[derive(::serde::Serialize)] + #[serde(rename_all = "PascalCase")] + #[::serde_with::skip_serializing_none] + struct Helper<$lt> { + $( + $field: $ty, + )* + } + + let helper = Helper { + $( + $field: self.$field.clone(), + )* + }; + + let mut state = serializer.serialize_map(Some(1))?; + state.serialize_key(stringify!($name))?; + state.serialize_value(&helper)?; + state.end() + } + } + + impl<'de: $lt, $lt> ::serde::Deserialize<'de> for $name<$lt> { + #[allow(non_snake_case)] + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(::serde::Deserialize)] + #[serde(rename_all = "PascalCase")] + #[::serde_with::skip_serializing_none] + struct Helper<$lt> { + $( + $field: $ty, + )* + } + + let hash_map: $crate::_serde::HashMap<&$lt str, Helper<$lt>> = $crate::_serde::HashMap::deserialize(deserializer)?; + let helper_result = hash_map.get(stringify!($name)); + + match helper_result { + Some(helper) => { + Ok(Self { + $( + $field: helper.$field.clone().into(), + )* + }) + } + None => { + Err(::serde::de::Error::custom("SerdeIntermediateStepError: Unable to find model name as json key.")) + } + } + } + } + }; + ( + $(#[$attr:meta])* + pub struct $name:ident { + $( + $(#[$doc:meta])* + pub $field:ident: $ty:ty, + )* + } + ) => { + $(#[$attr])* + pub struct $name { + $( + $(#[$doc])* + pub $field: $ty, + )* + } + + impl ::serde::Serialize for $name { + fn serialize(&self, serializer: S) -> ::core::result::Result + where + S: ::serde::Serializer + { + #[derive(::serde::Serialize)] + #[serde(rename_all = "PascalCase")] + #[::serde_with::skip_serializing_none] + struct Helper { + $( + $field: $ty, + )* + } + + let helper = Helper { + $( + $field: self.$field.clone(), + )* + }; + + let mut state = serializer.serialize_map(Some(1))?; + state.serialize_key(stringify!($name))?; + state.serialize_value(&helper)?; + state.end() + } + } + + impl<'de> ::serde::Deserialize<'de> for $name { + #[allow(non_snake_case)] + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(::serde::Deserialize)] + #[serde(rename_all = "PascalCase")] + #[::serde_with::skip_serializing_none] + struct Helper { + $( + $field: $ty, + )* + } + + let hash_map: $crate::_serde::HashMap<&'de str, Helper> = $crate::_serde::HashMap::deserialize(deserializer)?; + let helper_result = hash_map.get(stringify!($name)); + + match helper_result { + Some(helper) => { + Ok(Self { + $( + $field: helper.$field.clone().into(), + )* + }) + } + None => { + Err(::serde::de::Error::custom("SerdeIntermediateStepError: Unable to find model name as json key.")) + } + } + } + } + }; +} diff --git a/src/constants.rs b/src/constants.rs index 77c5051d..a651965e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,7 +1,7 @@ //! Collection of public constants for XRPL. -use alloc::string::String; -use alloc::string::ToString; +use serde::{Deserialize, Serialize}; +use strum_macros::Display; use strum_macros::EnumIter; /// Regular expression for determining ISO currency codes. @@ -12,19 +12,24 @@ pub const HEX_CURRENCY_REGEX: &str = r"^[A-F0-9]{40}$"; /// Length of an account id. pub const ACCOUNT_ID_LENGTH: usize = 20; +pub const MAX_TICK_SIZE: u32 = 15; +pub const MIN_TICK_SIZE: u32 = 3; +pub const DISABLE_TICK_SIZE: u32 = 0; + +pub const MAX_TRANSFER_RATE: u32 = 2000000000; +pub const MIN_TRANSFER_RATE: u32 = 1000000000; +pub const SPECIAL_CASE_TRANFER_RATE: u32 = 0; + +pub const MAX_TRANSFER_FEE: u32 = 50000; +pub const MAX_URI_LENGTH: usize = 512; + +pub const MAX_DOMAIN_LENGTH: usize = 256; + /// Represents the supported cryptography algorithms. -#[derive(Debug, PartialEq, Clone, EnumIter)] +#[derive(Debug, PartialEq, Eq, Clone, EnumIter, Display, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] pub enum CryptoAlgorithm { ED25519, SECP256K1, } - -impl ToString for CryptoAlgorithm { - /// Return the String representation of an algorithm. - fn to_string(&self) -> String { - match *self { - CryptoAlgorithm::ED25519 => "ed25519".to_string(), - CryptoAlgorithm::SECP256K1 => "secp256k1".to_string(), - } - } -} diff --git a/src/core/addresscodec/exceptions.rs b/src/core/addresscodec/exceptions.rs index e0a092a5..0d3dc68a 100644 --- a/src/core/addresscodec/exceptions.rs +++ b/src/core/addresscodec/exceptions.rs @@ -2,8 +2,9 @@ use crate::core::binarycodec::exceptions::XRPLBinaryCodecException; use crate::utils::exceptions::ISOCodeException; +use strum_macros::Display; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] pub enum XRPLAddressCodecException { InvalidXAddressPrefix, @@ -17,8 +18,8 @@ pub enum XRPLAddressCodecException { UnsupportedXAddress, UnknownSeedEncoding, UnexpectedPayloadLength { expected: usize, found: usize }, + FromHexError, Base58DecodeError(bs58::decode::Error), - HexError(hex::FromHexError), XRPLBinaryCodecError(XRPLBinaryCodecException), ISOError(ISOCodeException), SerdeJsonError(serde_json::error::Category), @@ -44,8 +45,8 @@ impl From for XRPLAddressCodecException { } impl From for XRPLAddressCodecException { - fn from(err: hex::FromHexError) -> Self { - XRPLAddressCodecException::HexError(err) + fn from(_: hex::FromHexError) -> Self { + XRPLAddressCodecException::FromHexError } } @@ -61,11 +62,5 @@ impl From> for XRPLAddressCodecException { } } -impl core::fmt::Display for XRPLAddressCodecException { - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - write!(f, "XRPLAddressCodecException: {:?}", self) - } -} - #[cfg(feature = "std")] impl alloc::error::Error for XRPLAddressCodecException {} diff --git a/src/core/addresscodec/mod.rs b/src/core/addresscodec/mod.rs index 8ad195fb..ddfb525c 100644 --- a/src/core/addresscodec/mod.rs +++ b/src/core/addresscodec/mod.rs @@ -1,4 +1,5 @@ //! This module contains commonly-used constants. + pub mod exceptions; #[cfg(test)] pub mod test_cases; @@ -193,14 +194,14 @@ pub fn classic_address_to_xaddress( is_test_network: bool, ) -> Result { let classic_address_bytes = decode_classic_address(classic_address)?; - let flag: bool = tag != None; + let flag: bool = tag.is_some(); let tag_val: u64; if classic_address_bytes.len() != CLASSIC_ADDRESS_ID_LENGTH { Err(XRPLAddressCodecException::InvalidCAddressIdLength { length: CLASSIC_ADDRESS_ID_LENGTH, }) - } else if tag != None && tag > Some(u32::max_value().into()) { + } else if tag.is_some() && tag > Some(u32::max_value().into()) { Err(XRPLAddressCodecException::InvalidCAddressTag) } else { if let Some(tval) = tag { diff --git a/src/core/addresscodec/utils.rs b/src/core/addresscodec/utils.rs index 6ed2620a..82d45f93 100644 --- a/src/core/addresscodec/utils.rs +++ b/src/core/addresscodec/utils.rs @@ -98,6 +98,7 @@ pub fn decode_base58( /// Returns the base58 encoding of the bytestring, with the /// given data prefix (which indicates type) and while /// ensuring the bytestring is the expected length. +/// TODO Analyze Security implication for time-based side channels /// /// See [`bs58::encode`] /// diff --git a/src/core/binarycodec/exceptions.rs b/src/core/binarycodec/exceptions.rs index fb7595e6..fd1a6931 100644 --- a/src/core/binarycodec/exceptions.rs +++ b/src/core/binarycodec/exceptions.rs @@ -2,8 +2,10 @@ use crate::utils::exceptions::ISOCodeException; use crate::utils::exceptions::XRPRangeException; +use strum_macros::Display; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] +#[non_exhaustive] pub enum XRPLBinaryCodecException { UnexpectedParserSkipOverflow { max: usize, found: usize }, UnexpectedLengthPrefixRange { min: usize, max: usize }, @@ -80,11 +82,5 @@ impl From for XRPLBinaryCodecException { } } -impl core::fmt::Display for XRPLBinaryCodecException { - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - write!(f, "XRPLBinaryCodecException: {:?}", self) - } -} - #[cfg(feature = "std")] impl alloc::error::Error for XRPLBinaryCodecException {} diff --git a/src/core/definitions/types.rs b/src/core/definitions/types.rs index 5d419ee0..e03c9844 100644 --- a/src/core/definitions/types.rs +++ b/src/core/definitions/types.rs @@ -832,7 +832,7 @@ mod test { let field_instance = FieldInstance::new(&field_info, "Generic", field_header); let test_field_instance = get_field_instance("Generic"); - assert!(!test_field_instance.is_none()); + assert!(test_field_instance.is_some()); let test_field_instance = test_field_instance.unwrap(); diff --git a/src/core/keypairs/algorithms.rs b/src/core/keypairs/algorithms.rs index ec3ca39b..f905cbe2 100644 --- a/src/core/keypairs/algorithms.rs +++ b/src/core/keypairs/algorithms.rs @@ -16,9 +16,11 @@ use alloc::string::String; use alloc::vec::Vec; use core::convert::TryInto; use core::str::FromStr; +use crypto_bigint::Encoding; +use crypto_bigint::U256; use ed25519_dalek::Verifier; -use num_bigint::BigUint; -use rust_decimal::prelude::One; +use secp256k1::ecdsa; +use secp256k1::Scalar; /// Methods for using the ECDSA cryptographic system with /// the SECP256K1 elliptic curve. @@ -40,10 +42,11 @@ impl Secp256k1 { /// Format a provided key. fn _format_key(keystr: &str) -> String { - format!("{:0>pad$}", keystr, pad = SECP256K1_KEY_LENGTH) + format!("{keystr:0>SECP256K1_KEY_LENGTH$}") } /// Format the public and private keys. + /// TODO Make function constant time fn _format_keys( public: secp256k1::PublicKey, private: secp256k1::SecretKey, @@ -60,10 +63,11 @@ impl Secp256k1 { } /// Determing if the provided secret key is valid. - fn _is_secret_valid(key: &[u8]) -> bool { - let key_bytes = BigUint::from_bytes_be(key); - key_bytes >= BigUint::one() - && key_bytes <= BigUint::from_bytes_be(&secp256k1::constants::CURVE_ORDER) + /// TODO Make function constant time + fn _is_secret_valid(key: [u8; u32::BITS as usize]) -> bool { + let key_bytes = U256::from_be_bytes(key); + key_bytes >= U256::ONE + && key_bytes <= U256::from_be_bytes(secp256k1::constants::CURVE_ORDER) } /// Concat candidate key. @@ -101,10 +105,9 @@ impl Secp256k1 { mid_public: secp256k1::PublicKey, mid_private: secp256k1::SecretKey, ) -> Result<(secp256k1::PublicKey, secp256k1::SecretKey), XRPLKeypairsException> { - let mut wrapped_private = root_private; + let wrapped_private = root_private.add_tweak(&Scalar::from(mid_private))?; let wrapped_public = root_public.combine(&mid_public)?; - wrapped_private.add_assign(mid_private.as_ref())?; Ok((wrapped_public, wrapped_private)) } @@ -122,7 +125,7 @@ impl Secp256k1 { let root = (raw_root as u32).to_be_bytes(); let candidate = sha512_first_half(&Self::_candidate_merger(input, &root, phase)); - if Self::_is_secret_valid(&candidate) { + if Self::_is_secret_valid(candidate) { return Ok(candidate); } else { continue; @@ -145,6 +148,7 @@ impl Ed25519 { } /// Format a provided key. + /// TODO Determine security implications fn _format_key(keystr: &str) -> String { format!("{}{}", ED25519_PREFIX, keystr.to_uppercase()) } @@ -264,7 +268,7 @@ impl CryptoImplementation for Secp256k1 { let message = Self::_get_message(message_bytes)?; let trimmed_key = private_key.trim_start_matches(SECP256K1_PREFIX); let private = secp256k1::SecretKey::from_str(trimmed_key)?; - let signature = secp.sign(&message, &private); + let signature = secp.sign_ecdsa(&message, &private); Ok(signature.serialize_der().to_vec()) } @@ -300,11 +304,11 @@ impl CryptoImplementation for Secp256k1 { let msg = Self::_get_message(message_bytes); if let Ok(value) = hex::decode(signature) { - let sig = secp256k1::Signature::from_der(&value); + let sig = ecdsa::Signature::from_der(&value); let public = secp256k1::PublicKey::from_str(public_key); if let (&Ok(m), &Ok(s), &Ok(p)) = (&msg.as_ref(), &sig.as_ref(), &public.as_ref()) { - secp.verify(m, s, p).is_ok() + secp.verify_ecdsa(m, s, p).is_ok() } else { false } diff --git a/src/core/keypairs/exceptions.rs b/src/core/keypairs/exceptions.rs index 60a39249..c72a10d9 100644 --- a/src/core/keypairs/exceptions.rs +++ b/src/core/keypairs/exceptions.rs @@ -2,8 +2,9 @@ use crate::constants::CryptoAlgorithm; use crate::core::addresscodec::exceptions::XRPLAddressCodecException; +use strum_macros::Display; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Display)] #[non_exhaustive] pub enum XRPLKeypairsException { InvalidSignature, @@ -39,11 +40,5 @@ impl From for XRPLKeypairsException { } } -impl core::fmt::Display for XRPLKeypairsException { - fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { - write!(f, "XRPLKeypairsException: {:?}", self) - } -} - #[cfg(feature = "std")] impl alloc::error::Error for XRPLKeypairsException {} diff --git a/src/core/keypairs/mod.rs b/src/core/keypairs/mod.rs index 19815b32..06c69475 100644 --- a/src/core/keypairs/mod.rs +++ b/src/core/keypairs/mod.rs @@ -93,13 +93,12 @@ pub fn generate_seed( algorithm: Option, ) -> Result { let mut random_bytes: [u8; SEED_LENGTH] = [0u8; SEED_LENGTH]; - let algo: CryptoAlgorithm; - if let Some(value) = algorithm { - algo = value; + let algo: CryptoAlgorithm = if let Some(value) = algorithm { + value } else { - algo = CryptoAlgorithm::ED25519; - } + CryptoAlgorithm::ED25519 + }; if let Some(value) = entropy { random_bytes = value; diff --git a/src/core/keypairs/utils.rs b/src/core/keypairs/utils.rs index 20a3976f..f48fbbd7 100644 --- a/src/core/keypairs/utils.rs +++ b/src/core/keypairs/utils.rs @@ -2,7 +2,7 @@ use crate::constants::ACCOUNT_ID_LENGTH; use core::convert::TryInto; -use ripemd160::Ripemd160; +use ripemd::Ripemd160; use sha2::{Digest, Sha256, Sha512}; /// Intermediate private keys are always padded with @@ -90,7 +90,7 @@ pub fn get_account_id(public_key: &[u8]) -> [u8; ACCOUNT_ID_LENGTH] { let mut ripemd160 = Ripemd160::new(); sha256.update(public_key); - ripemd160.update(&sha256.finalize()); + ripemd160.update(sha256.finalize()); ripemd160.finalize()[..ACCOUNT_ID_LENGTH] .try_into() diff --git a/src/core/types/account_id.rs b/src/core/types/account_id.rs index 83d393a6..0aa7c247 100644 --- a/src/core/types/account_id.rs +++ b/src/core/types/account_id.rs @@ -127,7 +127,7 @@ mod test { let serialize = serde_json::to_string(&account).unwrap(); let deserialize: AccountId = serde_json::from_str(&serialize).unwrap(); - assert_eq!(format!("\"{}\"", BASE58_ENCODING), serialize); + assert_eq!(format!("\"{BASE58_ENCODING}\""), serialize); assert_eq!(account.to_string(), deserialize.to_string()); } } diff --git a/src/core/types/amount.rs b/src/core/types/amount.rs index 52c69ca6..4e6c1039 100644 --- a/src/core/types/amount.rs +++ b/src/core/types/amount.rs @@ -67,7 +67,7 @@ fn _serialize_issued_currency_value(decimal: Decimal) -> Result<[u8; 8], XRPRang let is_positive: bool = decimal.is_sign_positive(); let mut exp: i32 = -(decimal.normalize().scale() as i32); - let mut mantissa: u128 = decimal.normalize().mantissa().abs() as u128; + let mut mantissa: u128 = decimal.normalize().mantissa().unsigned_abs(); while mantissa < _MIN_MANTISSA && exp > MIN_IOU_EXPONENT { mantissa *= 10; @@ -186,7 +186,7 @@ impl IssuedCurrency { } else { let hex_mantissa = hex::encode([&[bytes[1] & 0x3F], &bytes[2..]].concat()); let int_mantissa = i128::from_str_radix(&hex_mantissa, 16)?; - value = Decimal::from_i128_with_scale(int_mantissa, exp.abs() as u32); + value = Decimal::from_i128_with_scale(int_mantissa, exp.unsigned_abs()); if bytes[0] & 0x40 > 0 { value.set_sign_positive(true); @@ -421,7 +421,7 @@ mod test { let amount: Amount = Amount::new(Some(&bytes)).unwrap(); let serialize = serde_json::to_string(&amount).unwrap(); - assert_eq!(serialize, format!("\"{}\"", xrp)); + assert_eq!(serialize, format!("\"{xrp}\"")); } } diff --git a/src/core/types/currency.rs b/src/core/types/currency.rs index 735adf5a..2cf2bf32 100644 --- a/src/core/types/currency.rs +++ b/src/core/types/currency.rs @@ -1,20 +1,18 @@ //! Codec for currency property inside an XRPL //! issued currency amount json. -use crate::constants::HEX_CURRENCY_REGEX; -use crate::constants::ISO_CURRENCY_REGEX; use crate::core::types::exceptions::XRPLHashException; use crate::core::types::utils::CURRENCY_CODE_LENGTH; use crate::core::types::*; use crate::core::BinaryParser; use crate::utils::exceptions::ISOCodeException; +use crate::utils::*; use alloc::string::String; use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; use core::convert::TryFrom; use core::convert::TryInto; -use regex::Regex; use serde::Serializer; use serde::{Deserialize, Serialize}; @@ -27,24 +25,12 @@ pub const NATIVE_CODE: &str = "XRP"; #[serde(try_from = "&str")] pub struct Currency(Hash160); -/// Tests if value is a valid 3-char iso code. -fn _is_iso_code(value: &str) -> bool { - let regex = Regex::new(ISO_CURRENCY_REGEX).expect("_is_iso_code"); - regex.is_match(value) -} - -/// Tests if value is a valid 40-char hex string. -fn _is_hex(value: &str) -> bool { - let regex = Regex::new(HEX_CURRENCY_REGEX).expect("_is_hex"); - regex.is_match(value) -} - fn _iso_code_from_hex(value: &[u8]) -> Result, ISOCodeException> { let candidate_iso = alloc::str::from_utf8(&value[12..15])?; if candidate_iso == NATIVE_CODE { Err(ISOCodeException::InvalidXRPBytes) - } else if _is_iso_code(candidate_iso) { + } else if is_iso_code(candidate_iso) { Ok(Some(candidate_iso.to_string())) } else { Ok(None) @@ -57,7 +43,7 @@ fn _iso_code_from_hex(value: &[u8]) -> Result, ISOCodeException> /// See "Currency codes" subheading in Amount Fields: /// `` fn _iso_to_bytes(value: &str) -> Result<[u8; CURRENCY_CODE_LENGTH], ISOCodeException> { - if !_is_iso_code(value) { + if !is_iso_code(value) { Err(ISOCodeException::InvalidISOCode) } else if value == NATIVE_CODE { Ok(Default::default()) @@ -115,11 +101,11 @@ impl TryFrom<&str> for Currency { /// Construct a Currency object from a string /// representation of a currency. fn try_from(value: &str) -> Result { - if _is_iso_code(value) { + if is_iso_code(value) { let iso_bytes = _iso_to_bytes(value)?; let hash160 = Hash160::new(Some(&iso_bytes))?; Ok(Currency(hash160)) - } else if _is_hex(value) { + } else if is_iso_hex(value) { Ok(Currency(Hash160::new(Some(&hex::decode(value)?))?)) } else { Err(XRPLHashException::ISOCodeError( @@ -165,33 +151,6 @@ mod test { const NONSTANDARD_HEX_CODE: &str = "015841551A748AD2C1F76FF6ECB0CCCD00000000"; const USD_ISO: &str = "USD"; - #[test] - fn test_is_iso_code() { - let valid_code = "ABC"; - let valid_code_numeric = "123"; - let invalid_code_long = "LONG"; - let invalid_code_short = "NO"; - - assert!(_is_iso_code(valid_code)); - assert!(_is_iso_code(valid_code_numeric)); - assert!(!_is_iso_code(invalid_code_long)); - assert!(!_is_iso_code(invalid_code_short)); - } - - #[test] - fn test_is_hex() { - // Valid = 40 char length and only valid hex chars - let valid_hex: &str = "0000000000000000000000005553440000000000"; - let invalid_hex_chars: &str = "USD0000000000000000000005553440000000000"; - let invalid_hex_long: &str = "0000000000000000000000005553440000000000123455"; - let invalid_hex_short: &str = "1234"; - - assert!(_is_hex(valid_hex)); - assert!(!_is_hex(invalid_hex_long)); - assert!(!_is_hex(invalid_hex_short)); - assert!(!_is_hex(invalid_hex_chars)); - } - #[test] fn test_iso_to_bytes() { // Valid non-XRP @@ -234,7 +193,7 @@ mod test { let serialize = serde_json::to_string(¤cy).unwrap(); let deserialize: Currency = serde_json::from_str(&serialize).unwrap(); - assert_eq!(format!("\"{}\"", USD_ISO), serialize); + assert_eq!(format!("\"{USD_ISO}\""), serialize); assert_eq!(currency.to_string(), deserialize.to_string()); } } diff --git a/src/core/types/exceptions.rs b/src/core/types/exceptions.rs index e1cc3702..06f6aec0 100644 --- a/src/core/types/exceptions.rs +++ b/src/core/types/exceptions.rs @@ -5,37 +5,37 @@ use crate::core::binarycodec::exceptions::XRPLBinaryCodecException; use crate::utils::exceptions::ISOCodeException; use crate::utils::exceptions::JSONParseException; use crate::utils::exceptions::XRPRangeException; +use strum_macros::Display; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] pub enum XRPLTypeException { InvalidNoneValue, + FromHexError, XRPLBinaryCodecError(XRPLBinaryCodecException), XRPLHashError(XRPLHashException), XRPLRangeError(XRPRangeException), DecimalError(rust_decimal::Error), JSONParseError(JSONParseException), - HexError(hex::FromHexError), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] pub enum XRPLHashException { InvalidHashLength { expected: usize, found: usize }, + FromHexError, ISOCodeError(ISOCodeException), XRPLBinaryCodecError(XRPLBinaryCodecException), XRPLAddressCodecError(XRPLAddressCodecException), SerdeJsonError(serde_json::error::Category), - HexError(hex::FromHexError), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] pub enum XRPLVectorException { InvalidVector256Bytes, XRPLBinaryCodecError(XRPLBinaryCodecException), XRPLHashError(XRPLHashException), - HexError(hex::FromHexError), } impl From for XRPLTypeException { @@ -69,8 +69,8 @@ impl From for XRPLTypeException { } impl From for XRPLTypeException { - fn from(err: hex::FromHexError) -> Self { - XRPLTypeException::HexError(err) + fn from(_: hex::FromHexError) -> Self { + XRPLTypeException::FromHexError } } @@ -99,8 +99,8 @@ impl From for XRPLHashException { } impl From for XRPLHashException { - fn from(err: hex::FromHexError) -> Self { - XRPLHashException::HexError(err) + fn from(_: hex::FromHexError) -> Self { + XRPLHashException::FromHexError } } @@ -110,20 +110,11 @@ impl From for XRPLVectorException { } } -impl core::fmt::Display for XRPLTypeException { - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - write!(f, "XRPLTypeException: {:?}", self) - } -} +#[cfg(feature = "std")] +impl alloc::error::Error for XRPLTypeException {} -impl core::fmt::Display for XRPLHashException { - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - write!(f, "XRPLHashException: {:?}", self) - } -} +#[cfg(feature = "std")] +impl alloc::error::Error for XRPLHashException {} -impl core::fmt::Display for XRPLVectorException { - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - write!(f, "XRPLVectorException: {:?}", self) - } -} +#[cfg(feature = "std")] +impl alloc::error::Error for XRPLVectorException {} diff --git a/src/core/types/hash.rs b/src/core/types/hash.rs index be62655c..35ae0803 100644 --- a/src/core/types/hash.rs +++ b/src/core/types/hash.rs @@ -110,7 +110,7 @@ impl dyn Hash { /// }; /// ``` pub fn make(bytes: Option<&[u8]>) -> Result, XRPLHashException> { - let byte_value: &[u8] = bytes.or(Some(&[])).unwrap(); + let byte_value: &[u8] = bytes.unwrap_or(&[]); let hash_length: usize = T::get_length(); if byte_value.len() != hash_length { diff --git a/src/core/types/mod.rs b/src/core/types/mod.rs index 47176c6a..91d2ec80 100644 --- a/src/core/types/mod.rs +++ b/src/core/types/mod.rs @@ -114,7 +114,7 @@ pub trait TryFromParser { Self: Sized; } -impl<'value, T> From for SerializedType +impl From for SerializedType where T: XRPLType + AsRef<[u8]>, { diff --git a/src/core/types/paths.rs b/src/core/types/paths.rs index 2151343c..6248b293 100644 --- a/src/core/types/paths.rs +++ b/src/core/types/paths.rs @@ -7,8 +7,6 @@ use crate::constants::ACCOUNT_ID_LENGTH; use crate::core::binarycodec::exceptions::XRPLBinaryCodecException; use crate::core::types::exceptions::XRPLHashException; use crate::core::types::utils::CURRENCY_CODE_LENGTH; -use crate::core::types::AccountId; -use crate::core::types::Currency; use crate::core::types::*; use crate::core::BinaryParser; use crate::core::Parser; @@ -19,10 +17,9 @@ use alloc::vec; use alloc::vec::Vec; use core::convert::TryFrom; use indexmap::IndexMap; -use serde::ser::SerializeMap; -use serde::ser::SerializeSeq; -use serde::Serializer; -use serde::{Deserialize, Serialize}; +use serde::ser::{SerializeMap, SerializeSeq}; +use serde::{Deserialize, Serialize, Serializer}; +use serde_with::skip_serializing_none; // Constant Keys const _ACC_KEY: &str = "account"; @@ -38,28 +35,29 @@ const _TYPE_ISSUER: u8 = 0x20; const _PATHSET_END_BYTE: u8 = 0x00; const _PATH_SEPARATOR_BYTE: u8 = 0xFF; +#[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone)] struct PathStepData { #[serde(skip_serializing)] index: u8, - #[serde(skip_serializing_if = "Option::is_none")] account: Option, - #[serde(skip_serializing_if = "Option::is_none")] currency: Option, - #[serde(skip_serializing_if = "Option::is_none")] issuer: Option, } /// Serialize and deserialize a single step in a Path. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "&str")] pub struct PathStep(Vec); /// Class for serializing/deserializing Paths. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "&str")] pub struct Path(Vec); /// Class for serializing/deserializing Paths. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "&str")] pub struct PathSet(Vec); /// Helper function to determine if a dictionary represents @@ -161,31 +159,28 @@ impl TryFromParser for PathStepData { parser: &mut BinaryParser, _length: Option, ) -> Result { - let account: Option; - let currency: Option; - let issuer: Option; let data_type = parser.read_uint8()?; - if data_type & _TYPE_ACCOUNT != 0 { + let account: Option = if data_type & _TYPE_ACCOUNT != 0 { let data = AccountId::from_parser(parser, None)?; - account = Some(data.to_string()); + Some(data.to_string()) } else { - account = None; - } + None + }; - if data_type & _TYPE_CURRENCY != 0 { + let currency: Option = if data_type & _TYPE_CURRENCY != 0 { let data = Currency::from_parser(parser, None)?; - currency = Some(data.to_string()); + Some(data.to_string()) } else { - currency = None; - } + None + }; - if data_type & _TYPE_ISSUER != 0 { + let issuer: Option = if data_type & _TYPE_ISSUER != 0 { let data = AccountId::from_parser(parser, None)?; - issuer = Some(data.to_string()); + Some(data.to_string()) } else { - issuer = None; - } + None + }; Ok(PathStepData::new(account, currency, issuer)) } @@ -410,6 +405,16 @@ impl TryFrom<&str> for Path { } } +impl TryFrom<&str> for PathStep { + type Error = XRPLHashException; + + /// Construct a PathSet object from a string. + fn try_from(value: &str) -> Result { + let json: IndexMap = serde_json::from_str(value)?; + Self::try_from(json) + } +} + impl TryFrom<&str> for PathSet { type Error = XRPLBinaryCodecException; diff --git a/src/core/types/vector256.rs b/src/core/types/vector256.rs index 5f759d43..bc9a06bc 100644 --- a/src/core/types/vector256.rs +++ b/src/core/types/vector256.rs @@ -44,16 +44,14 @@ impl TryFromParser for Vector256 { length: Option, ) -> Result { let mut bytes = vec![]; - let num_bytes: usize; - let num_hashes: usize; - if let Some(value) = length { - num_bytes = value; + let num_bytes: usize = if let Some(value) = length { + value } else { - num_bytes = parser.len(); - } + parser.len() + }; - num_hashes = num_bytes / _HASH_LENGTH_BYTES; + let num_hashes: usize = num_bytes / _HASH_LENGTH_BYTES; for _ in 0..num_hashes { bytes.extend_from_slice(Hash256::from_parser(parser, None)?.as_ref()); @@ -151,7 +149,7 @@ mod test { let serialize = serde_json::to_string(&vector).unwrap(); let deserialize: Vector256 = serde_json::from_str(&serialize).unwrap(); - assert_eq!(format!("[\"{}\",\"{}\"]", HASH1, HASH2), serialize); + assert_eq!(format!("[\"{HASH1}\",\"{HASH2}\"]"), serialize); assert_eq!(SERIALIZED, deserialize.to_string()); } } diff --git a/src/lib.rs b/src/lib.rs index a3f32abf..58f3a239 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,9 +29,14 @@ pub mod constants; #[cfg(feature = "core")] pub mod core; pub mod macros; +#[cfg(feature = "models")] +pub mod models; #[cfg(feature = "utils")] pub mod utils; pub mod wallet; pub extern crate indexmap; pub extern crate serde_json; + +mod _anyhow; +mod _serde; diff --git a/src/models/amount/exceptions.rs b/src/models/amount/exceptions.rs new file mode 100644 index 00000000..07746a28 --- /dev/null +++ b/src/models/amount/exceptions.rs @@ -0,0 +1,10 @@ +use thiserror_no_std::Error; + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum XRPLAmountException { + #[error("Unable to convert amount `value` into `Decimal`.")] + ToDecimalError(#[from] rust_decimal::Error), +} + +#[cfg(feature = "std")] +impl alloc::error::Error for XRPLAmountException {} diff --git a/src/models/amount/issued_currency_amount.rs b/src/models/amount/issued_currency_amount.rs new file mode 100644 index 00000000..8a63c07a --- /dev/null +++ b/src/models/amount/issued_currency_amount.rs @@ -0,0 +1,37 @@ +use crate::models::amount::exceptions::XRPLAmountException; +use crate::models::Model; +use alloc::borrow::Cow; +use core::convert::TryInto; +use core::str::FromStr; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] +pub struct IssuedCurrencyAmount<'a> { + pub currency: Cow<'a, str>, + pub issuer: Cow<'a, str>, + pub value: Cow<'a, str>, +} + +impl<'a> Model for IssuedCurrencyAmount<'a> {} + +impl<'a> IssuedCurrencyAmount<'a> { + pub fn new(currency: Cow<'a, str>, issuer: Cow<'a, str>, value: Cow<'a, str>) -> Self { + Self { + currency, + issuer, + value, + } + } +} + +impl<'a> TryInto for IssuedCurrencyAmount<'a> { + type Error = XRPLAmountException; + + fn try_into(self) -> Result { + match Decimal::from_str(&self.value) { + Ok(decimal) => Ok(decimal), + Err(decimal_error) => Err(XRPLAmountException::ToDecimalError(decimal_error)), + } + } +} diff --git a/src/models/amount/mod.rs b/src/models/amount/mod.rs new file mode 100644 index 00000000..e690ac9a --- /dev/null +++ b/src/models/amount/mod.rs @@ -0,0 +1,64 @@ +pub mod exceptions; +pub mod issued_currency_amount; +pub mod xrp_amount; + +use core::convert::TryInto; +pub use issued_currency_amount::*; +use rust_decimal::Decimal; +pub use xrp_amount::*; + +use crate::models::amount::exceptions::XRPLAmountException; +use crate::models::Model; +use serde::{Deserialize, Serialize}; +use strum_macros::Display; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Display)] +#[serde(untagged)] +pub enum Amount<'a> { + IssuedCurrencyAmount(IssuedCurrencyAmount<'a>), + XRPAmount(XRPAmount<'a>), +} + +impl<'a> TryInto for Amount<'a> { + type Error = XRPLAmountException; + + fn try_into(self) -> Result { + match self { + Amount::IssuedCurrencyAmount(amount) => amount.try_into(), + Amount::XRPAmount(amount) => amount.try_into(), + } + } +} + +impl<'a> Model for Amount<'a> {} + +impl<'a> Default for Amount<'a> { + fn default() -> Self { + Self::XRPAmount("0".into()) + } +} + +impl<'a> Amount<'a> { + pub fn is_xrp(&self) -> bool { + match self { + Amount::IssuedCurrencyAmount(_) => false, + Amount::XRPAmount(_) => true, + } + } + + pub fn is_issued_currency(&self) -> bool { + !self.is_xrp() + } +} + +impl<'a> From> for Amount<'a> { + fn from(value: IssuedCurrencyAmount<'a>) -> Self { + Self::IssuedCurrencyAmount(value) + } +} + +impl<'a> From> for Amount<'a> { + fn from(value: XRPAmount<'a>) -> Self { + Self::XRPAmount(value) + } +} diff --git a/src/models/amount/xrp_amount.rs b/src/models/amount/xrp_amount.rs new file mode 100644 index 00000000..4bb01b54 --- /dev/null +++ b/src/models/amount/xrp_amount.rs @@ -0,0 +1,35 @@ +use crate::models::amount::exceptions::XRPLAmountException; +use crate::models::Model; +use alloc::borrow::Cow; +use core::convert::TryInto; +use core::str::FromStr; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] +pub struct XRPAmount<'a>(pub Cow<'a, str>); + +impl<'a> Model for XRPAmount<'a> {} + +impl<'a> From> for XRPAmount<'a> { + fn from(value: Cow<'a, str>) -> Self { + Self(value) + } +} + +impl<'a> From<&'a str> for XRPAmount<'a> { + fn from(value: &'a str) -> Self { + Self(value.into()) + } +} + +impl<'a> TryInto for XRPAmount<'a> { + type Error = XRPLAmountException; + + fn try_into(self) -> Result { + match Decimal::from_str(&self.0) { + Ok(decimal) => Ok(decimal), + Err(decimal_error) => Err(XRPLAmountException::ToDecimalError(decimal_error)), + } + } +} diff --git a/src/models/currency/issued_currency.rs b/src/models/currency/issued_currency.rs new file mode 100644 index 00000000..f3cf8359 --- /dev/null +++ b/src/models/currency/issued_currency.rs @@ -0,0 +1,61 @@ +use crate::models::amount::IssuedCurrencyAmount; +use crate::models::currency::ToAmount; +use crate::models::Model; +use alloc::borrow::Cow; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] +pub struct IssuedCurrency<'a> { + pub currency: Cow<'a, str>, + pub issuer: Cow<'a, str>, +} + +impl<'a> Model for IssuedCurrency<'a> {} + +impl<'a> ToAmount<'a, IssuedCurrencyAmount<'a>> for IssuedCurrency<'a> { + fn to_amount(&self, value: Cow<'a, str>) -> IssuedCurrencyAmount<'a> { + IssuedCurrencyAmount::new(self.currency.clone(), self.issuer.clone(), value) + } +} + +impl<'a> IssuedCurrency<'a> { + pub fn new(currency: Cow<'a, str>, issuer: Cow<'a, str>) -> Self { + Self { currency, issuer } + } +} + +impl<'a> From> for IssuedCurrency<'a> { + fn from(value: IssuedCurrencyAmount<'a>) -> Self { + Self { + currency: value.currency, + issuer: value.issuer, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let issued_currency = + IssuedCurrency::new("TST".into(), "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd".into()); + let issued_currency_json = serde_json::to_string(&issued_currency).unwrap(); + let actual = issued_currency_json.as_str(); + let expected = r#"{"currency":"TST","issuer":"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"}"#; + + assert_eq!(expected, actual); + } + + #[test] + fn test_deserialize() { + let issued_currency_json = + r#"{"currency":"TST","issuer":"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"}"#; + let actual = serde_json::from_str(issued_currency_json).unwrap(); + let expected = + IssuedCurrency::new("TST".into(), "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd".into()); + + assert_eq!(expected, actual); + } +} diff --git a/src/models/currency/mod.rs b/src/models/currency/mod.rs new file mode 100644 index 00000000..dc2a5331 --- /dev/null +++ b/src/models/currency/mod.rs @@ -0,0 +1,40 @@ +pub mod issued_currency; +pub mod xrp; + +use crate::models::Model; +use alloc::borrow::Cow; +pub use issued_currency::*; +use serde::{Deserialize, Serialize}; +use strum_macros::Display; +pub use xrp::*; + +pub trait ToAmount<'a, A> { + fn to_amount(&self, value: Cow<'a, str>) -> A; +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Display)] +#[serde(untagged)] +pub enum Currency<'a> { + IssuedCurrency(IssuedCurrency<'a>), + XRP(XRP<'a>), +} + +impl<'a> Model for Currency<'a> {} + +impl<'a> Default for Currency<'a> { + fn default() -> Self { + Self::XRP(XRP::new()) + } +} + +impl<'a> From> for Currency<'a> { + fn from(value: IssuedCurrency<'a>) -> Self { + Self::IssuedCurrency(value) + } +} + +impl<'a> From> for Currency<'a> { + fn from(value: XRP<'a>) -> Self { + Self::XRP(value) + } +} diff --git a/src/models/currency/xrp.rs b/src/models/currency/xrp.rs new file mode 100644 index 00000000..dcaf5103 --- /dev/null +++ b/src/models/currency/xrp.rs @@ -0,0 +1,90 @@ +use crate::models::amount::XRPAmount; +use crate::models::currency::ToAmount; +use crate::models::Model; +use alloc::borrow::Cow; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] +pub struct XRP<'a> { + pub currency: Cow<'a, str>, +} + +impl<'a> Model for XRP<'a> {} + +impl<'a> ToAmount<'a, XRPAmount<'a>> for XRP<'a> { + fn to_amount(&self, value: Cow<'a, str>) -> XRPAmount<'a> { + XRPAmount(value) + } +} + +impl<'a> XRP<'a> { + pub fn new() -> Self { + Self { + currency: "XRP".into(), + } + } +} + +impl<'a> From> for XRP<'a> { + fn from(_value: XRPAmount<'a>) -> Self { + Self { + currency: "XRP".into(), + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let xrp = XRP::new(); + let xrp_json = serde_json::to_string(&xrp).unwrap(); + let actual = xrp_json.as_str(); + let expected = r#"{"currency":"XRP"}"#; + + assert_eq!(expected, actual); + } + + #[test] + fn test_deserialize() { + let xrp_json = r#"{"currency":"XRP"}"#; + let actual = serde_json::from_str(xrp_json).unwrap(); + let expected = XRP::new(); + + assert_eq!(expected, actual); + } +} + +#[cfg(test)] +mod test_amount_currency_conversion { + use super::*; + + #[test] + fn test_currency_to_amount() { + let xrp = XRP::new(); + let actual = xrp.to_amount("10".into()); + let expected: XRPAmount = "10".into(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_currency_from_amount() { + let xrp_amount: XRPAmount = "10".into(); + let actual = XRP::from(xrp_amount); + let expected = XRP::new(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_amount_into_currency() { + let xrp_amount: XRPAmount = "10".into(); + let actual: XRP = xrp_amount.into(); + let expected = XRP::new(); + + assert_eq!(expected, actual); + } +} diff --git a/src/models/exceptions.rs b/src/models/exceptions.rs index 3c512056..45856761 100644 --- a/src/models/exceptions.rs +++ b/src/models/exceptions.rs @@ -1,7 +1,24 @@ -/// General XRPL Model Exception. +//! General XRPL Model Exception. -#[derive(Debug, PartialEq)] -pub enum XRPLModelException {} +use crate::models::requests::XRPLRequestException; +use crate::models::transactions::XRPLTransactionException; +use alloc::string::String; +use serde::{Deserialize, Serialize}; +use strum_macros::Display; + +#[derive(Debug, PartialEq, Display)] +#[non_exhaustive] +pub enum XRPLModelException<'a> { + InvalidICCannotBeXRP, + XRPLTransactionError(XRPLTransactionException<'a>), + XRPLRequestError(XRPLRequestException<'a>), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +pub struct JSONRPCException { + code: i32, + message: String, +} #[cfg(feature = "std")] -impl alloc::error::Error for XRPLModelException {} +impl<'a> alloc::error::Error for XRPLModelException<'a> {} diff --git a/src/models/ledger/mod.rs b/src/models/ledger/mod.rs new file mode 100644 index 00000000..bcfa65b2 --- /dev/null +++ b/src/models/ledger/mod.rs @@ -0,0 +1,2 @@ +pub mod objects; +pub use objects::*; diff --git a/src/models/ledger/objects/account_root.rs b/src/models/ledger/objects/account_root.rs new file mode 100644 index 00000000..b71cc418 --- /dev/null +++ b/src/models/ledger/objects/account_root.rs @@ -0,0 +1,243 @@ +use crate::_serde::lgr_obj_flags; +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::XRPAmount, Model}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +/// There are several options which can be either enabled or disabled for an account. +/// These options can be changed with an `AccountSet` transaction. +/// +/// See `AccountRoot` flags: +/// `` +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum AccountRootFlag { + /// This account is an Automated Market Maker instance. + LsfAmm = 0x02000000, + /// Enable rippling on this addresses's trust lines by default. + /// Required for issuing addresses; discouraged for others. + LsfDefaultRipple = 0x00800000, + /// This account can only receive funds from transactions it sends, and from preauthorized + /// accounts. (It has `DepositAuth` enabled.) + LsfDepositAuth = 0x01000000, + /// Disallows use of the master key to sign transactions for this account. + LsfDisableMaster = 0x00100000, + /// Client applications should not send XRP to this account. Not enforced by rippled. + LsfDisallowXRP = 0x00080000, + /// All assets issued by this address are frozen. + LsfGlobalFreeze = 0x00400000, + /// This address cannot freeze trust lines connected to it. Once enabled, cannot be disabled. + LsfNoFreeze = 0x00200000, + /// The account has used its free SetRegularKey transaction. + LsfPasswordSpent = 0x00010000, + /// This account must individually approve other users for those users to hold this account's + /// tokens. + LsfRequireAuth = 0x00040000, + /// Requires incoming payments to specify a Destination Tag. + LsfRequireDestTag = 0x00020000, +} + +/// The `AccountRoot` object type describes a single account, its settings, and XRP balance. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct AccountRoot<'a> { + /// The value `0x0061`, mapped to the string `AccountRoot`, indicates that this is an `AccountRoot` + /// object. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this account. + #[serde(with = "lgr_obj_flags")] + pub flags: Vec, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The identifying (classic) address of this account. + pub account: Cow<'a, str>, + /// The number of objects this account owns in the ledger, which contributes to its owner + /// reserve. + pub owner_count: u32, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently modified this object. + pub previous_txn_lgr_seq: u32, + /// The sequence number of the next valid transaction for this account. + pub sequence: u32, + /// The identifying hash of the transaction most recently sent by this account. This field must + /// be enabled to use the `AccountTxnID` transaction field. To enable it, send an `AccountSet` + /// transaction with the `asfAccountTxnID` flag enabled. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option>, + /// The account's current XRP balance in drops, represented as a string. + pub balance: Option>, + /// How many total of this account's issued non-fungible tokens have been burned. This number + /// is always equal or less than `MintedNFTokens`. + #[serde(rename = "BurnedNFTokens")] + pub burned_nftokens: Option, + /// A domain associated with this account. In JSON, this is the hexadecimal for the ASCII + /// representation of the domain. Cannot be more than 256 bytes in length. + pub domain: Option>, + /// The md5 hash of an email address. Clients can use this to look up an avatar through services + /// such as Gravatar + pub email_hash: Option>, + /// A public key that may be used to send encrypted messages to this account. In JSON, uses + /// hexadecimal. Must be exactly 33 bytes, with the first byte indicating the key type: 0x02 or + /// 0x03 for secp256k1 keys, 0xED for Ed25519 keys. + pub message_key: Option>, + /// How many total non-fungible tokens have been minted by and on behalf of this account. + #[serde(rename = "MintedNFTokens")] + pub minted_nftokens: Option, + /// Another account that can mint non-fungible tokens on behalf of this account. + #[serde(rename = "NFTokenMinter")] + pub nftoken_minter: Option>, + /// The address of a key pair that can be used to sign transactions for this account instead of + /// the master key. Use a `SetRegularKey` transaction to change this value. + pub regular_key: Option>, + /// How many `Tickets` this account owns in the ledger. This is updated automatically to ensure + /// that the account stays within the hard limit of 250 Tickets at a time. This field is omitted + /// if the account has zero `Tickets`. + pub ticket_count: Option, + /// How many significant digits to use for exchange rates of Offers involving currencies issued + /// by this address. Valid values are 3 to 15, inclusive. + pub tick_size: Option, + /// A transfer fee to charge other users for sending currency issued by this account to each other. + pub transfer_rate: Option, + /// An arbitrary 256-bit value that users can set. + pub wallet_locator: Option>, + /// Unused. (The code supports this field but there is no way to set it.) + pub wallet_size: Option, +} + +impl<'a> Default for AccountRoot<'a> { + fn default() -> Self { + Self { + flags: Default::default(), + index: Default::default(), + account: Default::default(), + ledger_entry_type: LedgerEntryType::AccountRoot, + owner_count: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + sequence: Default::default(), + account_txn_id: Default::default(), + balance: Default::default(), + burned_nftokens: Default::default(), + domain: Default::default(), + email_hash: Default::default(), + message_key: Default::default(), + minted_nftokens: Default::default(), + nftoken_minter: Default::default(), + regular_key: Default::default(), + ticket_count: Default::default(), + tick_size: Default::default(), + transfer_rate: Default::default(), + wallet_locator: Default::default(), + wallet_size: Default::default(), + } + } +} + +impl<'a> Model for AccountRoot<'a> {} + +impl<'a> AccountRoot<'a> { + pub fn new( + flags: Vec, + index: Cow<'a, str>, + account: Cow<'a, str>, + owner_count: u32, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + sequence: u32, + account_txn_id: Option>, + balance: Option>, + burned_nftokens: Option, + domain: Option>, + email_hash: Option>, + message_key: Option>, + minted_nftokens: Option, + nftoken_minter: Option>, + regular_key: Option>, + ticket_count: Option, + tick_size: Option, + transfer_rate: Option, + wallet_locator: Option>, + wallet_size: Option, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::AccountRoot, + flags, + index, + account, + owner_count, + previous_txn_id, + previous_txn_lgr_seq, + sequence, + account_txn_id, + balance, + burned_nftokens, + domain, + email_hash, + message_key, + minted_nftokens, + nftoken_minter, + regular_key, + ticket_count, + tick_size, + transfer_rate, + wallet_locator, + wallet_size, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::borrow::Cow; + use alloc::vec; + + #[test] + fn test_serialize() { + let account_root = AccountRoot::new( + vec![AccountRootFlag::LsfDefaultRipple], + Cow::from("13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8"), + Cow::from("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"), + 3, + Cow::from("0D5FB50FA65C9FE1538FD7E398FFFE9D1908DFA4576D8D7A020040686F93C77D"), + 14091160, + 336, + Some(Cow::from( + "0D5FB50FA65C9FE1538FD7E398FFFE9D1908DFA4576D8D7A020040686F93C77D", + )), + Some("148446663".into()), + None, + Some(Cow::from("6D64756F31332E636F6D")), + Some(Cow::from("98B4375E1D753E5B91627516F6D70977")), + Some(Cow::from("0000000000000000000000070000000300")), + None, + None, + None, + None, + None, + Some(1004999999), + None, + None, + ); + let account_root_json = serde_json::to_string(&account_root).unwrap(); + let actual = account_root_json.as_str(); + let expected = r#"{"LedgerEntryType":"AccountRoot","Flags":8388608,"index":"13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","OwnerCount":3,"PreviousTxnID":"0D5FB50FA65C9FE1538FD7E398FFFE9D1908DFA4576D8D7A020040686F93C77D","PreviousTxnLgrSeq":14091160,"Sequence":336,"AccountTxnID":"0D5FB50FA65C9FE1538FD7E398FFFE9D1908DFA4576D8D7A020040686F93C77D","Balance":"148446663","Domain":"6D64756F31332E636F6D","EmailHash":"98B4375E1D753E5B91627516F6D70977","MessageKey":"0000000000000000000000070000000300","TransferRate":1004999999}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/amendments.rs b/src/models/ledger/objects/amendments.rs new file mode 100644 index 00000000..c2f03c5d --- /dev/null +++ b/src/models/ledger/objects/amendments.rs @@ -0,0 +1,111 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use derive_new::new; +use serde::{ser::SerializeMap, Deserialize, Serialize}; + +use crate::serde_with_tag; +use serde_with::skip_serializing_none; + +serde_with_tag! { + /// `` + #[derive(Debug, PartialEq, Eq, Clone, new, Default)] + pub struct Majority<'a> { + /// The Amendment ID of the pending amendment. + pub amendment: Cow<'a, str>, + /// The `close_time` field of the ledger version where this amendment most recently gained a + /// majority. + pub close_time: u32, + } +} + +/// The `Amendments` object type contains a list of `Amendments` that are currently active. +/// Each ledger version contains at most one Amendments`` object. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Amendments<'a> { + /// The value `0x0066`, mapped to the string `Amendments`, indicates that this object describes + /// the status of `amendments` to the XRP Ledger. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines no flags + /// for `Amendments` objects. The value is always 0. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// Array of 256-bit amendment IDs for all currently enabled amendments. If omitted, there are + /// no enabled amendments. + pub amendments: Option>>, + /// Array of objects describing the status of amendments that have majority support but are not + /// yet enabled. If omitted, there are no pending amendments with majority support. + #[serde(borrow = "'a")] + pub majorities: Option>>, +} + +impl<'a> Model for Amendments<'a> {} + +impl<'a> Default for Amendments<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::Amendments, + flags: Default::default(), + index: Default::default(), + amendments: Default::default(), + majorities: Default::default(), + } + } +} + +impl<'a> Amendments<'a> { + pub fn new( + index: Cow<'a, str>, + amendments: Option>>, + majorities: Option>>, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::Amendments, + flags: 0, + index, + amendments, + majorities, + } + } +} + +#[cfg(test)] +mod test_serde { + use crate::models::ledger::{Amendments, Majority}; + use alloc::borrow::Cow; + use alloc::vec; + + #[test] + fn test_serialize() { + let amendments = Amendments::new( + Cow::from("7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4"), + Some(vec![ + Cow::from("42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE"), + Cow::from("4C97EBA926031A7CF7D7B36FDE3ED66DDA5421192D63DE53FFB46E43B9DC8373"), + Cow::from("6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC"), + Cow::from("740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D628A06927F11"), + ]), + Some(vec![Majority { + amendment: Cow::from( + "1562511F573A19AE9BD103B5D6B9E01B3B46805AEC5D3C4805C902B514399146", + ), + close_time: 535589001, + }]), + ); + let amendments_json = serde_json::to_string(&amendments).unwrap(); + let actual = amendments_json.as_str(); + let expected = r#"{"LedgerEntryType":"Amendments","Flags":0,"index":"7DB0788C020F02780A673DC74757F23823FA3014C1866E72CC4CD8B226CD6EF4","Amendments":["42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE","4C97EBA926031A7CF7D7B36FDE3ED66DDA5421192D63DE53FFB46E43B9DC8373","6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC","740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D628A06927F11"],"Majorities":[{"Majority":{"Amendment":"1562511F573A19AE9BD103B5D6B9E01B3B46805AEC5D3C4805C902B514399146","CloseTime":535589001}}]}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/amm.rs b/src/models/ledger/objects/amm.rs new file mode 100644 index 00000000..15b6bf14 --- /dev/null +++ b/src/models/ledger/objects/amm.rs @@ -0,0 +1,184 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::Amount, Currency, Model}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use derive_new::new; +use serde::{ser::SerializeMap, Deserialize, Serialize}; + +use crate::serde_with_tag; +use serde_with::skip_serializing_none; + +serde_with_tag! { + #[derive(Debug, PartialEq, Eq, Clone, new, Default)] + pub struct AuthAccount<'a> { + pub account: Cow<'a, str>, + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, new, Default)] +#[serde(rename_all = "PascalCase")] +/// `` +pub struct AuctionSlot<'a> { + /// The current owner of this auction slot. + pub account: Cow<'a, str>, + /// The trading fee to be charged to the auction owner, in the same format as TradingFee. By + /// default this is 0, meaning that the auction owner can trade at no fee instead of the + /// standard fee for this AMM. + pub discounted_fee: u32, + /// The time when this slot expires, in seconds since the Ripple Epoch. + pub expiration: u32, + /// The amount the auction owner paid to win this slot, in LP Tokens. + pub price: Amount<'a>, + /// A list of at most 4 additional accounts that are authorized to trade at the discounted fee + /// for this AMM instance. + #[serde(borrow = "'a")] + pub auth_accounts: Option>>, +} + +serde_with_tag! { + #[derive(Debug, PartialEq, Eq, Clone, new, Default)] + pub struct VoteEntry<'a> { + pub account: Cow<'a, str>, + pub trading_fee: u16, + pub vote_weight: u32, + } +} + +/// The `AMM` object type describes a single Automated Market Maker (`AMM`) instance. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct AMM<'a> { + /// The value `0x0079`, mapped to the string `AMM`, indicates that this is an `AMM` object. + pub ledger_entry_type: LedgerEntryType, + /// Currently there are no flags for the AMM ledger object + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The address of the special account that holds this `AMM's` assets. + #[serde(rename = "AMMAccount")] + pub amm_account: Cow<'a, str>, + /// The definition for one of the two assets this `AMM` holds. In JSON, this is an object with + /// `currency` and `issuer` fields. + pub asset: Currency<'a>, + /// The definition for the other asset this `AMM` holds. In JSON, this is an object with + /// `currency` and `issuer` fields. + pub asset2: Currency<'a>, + /// The total outstanding balance of liquidity provider tokens from this `AMM` instance. + /// The holders of these tokens can vote on the `AMM's` trading fee in proportion to their + /// holdings, or redeem the tokens for a share of the `AMM's` assets which grows with the + /// trading fees collected. + #[serde(rename = "LPTokenBalance")] + pub lptoken_balance: Amount<'a>, + /// The percentage fee to be charged for trades against this `AMM` instance, + /// in units of 1/100,000. The maximum value is 1000, for a 1% fee. + pub trading_fee: u16, + /// Details of the current owner of the auction slot, as an `AuctionSlot` object. + #[serde(borrow = "'a")] + pub auction_slot: Option>, + /// A list of vote objects, representing votes on the pool's trading fee. + pub vote_slots: Option>>, +} + +impl<'a> Default for AMM<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::AMM, + flags: Default::default(), + index: Default::default(), + amm_account: Default::default(), + asset: Default::default(), + asset2: Default::default(), + lptoken_balance: Default::default(), + trading_fee: Default::default(), + auction_slot: Default::default(), + vote_slots: Default::default(), + } + } +} + +impl<'a> Model for AMM<'a> {} + +impl<'a> AMM<'a> { + pub fn new( + index: Cow<'a, str>, + amm_account: Cow<'a, str>, + asset: Currency<'a>, + asset2: Currency<'a>, + lptoken_balance: Amount<'a>, + trading_fee: u16, + auction_slot: Option>, + vote_slots: Option>>, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::AMM, + flags: 0, + index, + amm_account, + asset, + asset2, + lptoken_balance, + trading_fee, + auction_slot, + vote_slots, + } + } +} + +#[cfg(test)] +mod test_serde { + use crate::models::amount::{Amount, IssuedCurrencyAmount}; + use crate::models::currency::{Currency, IssuedCurrency, XRP}; + use crate::models::ledger::amm::{AuctionSlot, AuthAccount, VoteEntry, AMM}; + use alloc::borrow::Cow; + use alloc::vec; + + #[test] + fn test_serialize() { + let amm = AMM::new( + Cow::from("ForTest"), + Cow::from("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S"), + Currency::XRP(XRP::new()), + Currency::IssuedCurrency(IssuedCurrency::new( + "TST".into(), + "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd".into(), + )), + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "039C99CD9AB0B70B32ECDA51EAAE471625608EA2".into(), + "rE54zDvgnghAoPopCgvtiqWNq3dU5y836S".into(), + "71150.53584131501".into(), + )), + 600, + Some(AuctionSlot::new( + Cow::from("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"), + 0, + 721870180, + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "039C99CD9AB0B70B32ECDA51EAAE471625608EA2".into(), + "rE54zDvgnghAoPopCgvtiqWNq3dU5y836S".into(), + "0.8696263565463045".into(), + )), + Some(vec![ + AuthAccount::new(Cow::from("rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg")), + AuthAccount::new(Cow::from("rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv")), + ]), + )), + Some(vec![VoteEntry::new( + Cow::from("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"), + 600, + 100000, + )]), + ); + let amm_json = serde_json::to_string(&amm).unwrap(); + let actual = amm_json.as_str(); + let expected = r#"{"LedgerEntryType":"AMM","Flags":0,"index":"ForTest","AMMAccount":"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S","Asset":{"currency":"XRP"},"Asset2":{"currency":"TST","issuer":"rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"},"LPTokenBalance":{"currency":"039C99CD9AB0B70B32ECDA51EAAE471625608EA2","issuer":"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S","value":"71150.53584131501"},"TradingFee":600,"AuctionSlot":{"Account":"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm","DiscountedFee":0,"Expiration":721870180,"Price":{"currency":"039C99CD9AB0B70B32ECDA51EAAE471625608EA2","issuer":"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S","value":"0.8696263565463045"},"AuthAccounts":[{"AuthAccount":{"Account":"rMKXGCbJ5d8LbrqthdG46q3f969MVK2Qeg"}},{"AuthAccount":{"Account":"rBepJuTLFJt3WmtLXYAxSjtBWAeQxVbncv"}}]},"VoteSlots":[{"VoteEntry":{"Account":"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm","TradingFee":600,"VoteWeight":100000}}]}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/check.rs b/src/models/ledger/objects/check.rs new file mode 100644 index 00000000..41ceefb1 --- /dev/null +++ b/src/models/ledger/objects/check.rs @@ -0,0 +1,152 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::Amount, Model}; +use alloc::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// A Check object describes a check, similar to a paper personal check, which can be cashed by its +/// destination to get money from its sender. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Check<'a> { + /// The value `0x0043`, mapped to the string `Check`, indicates that this object is a `Check` object. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines no flags + /// for `Check` objects. The value is always 0. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The sender of the `Check`. Cashing the `Check` debits this address's balance. + pub account: Cow<'a, str>, + /// The intended recipient of the `Check`. Only this address can cash the `Check`, using a + /// `CheckCash` transaction. + pub destination: Cow<'a, str>, + /// A hint indicating which page of the sender's owner directory links to this object, in case + /// the directory consists of multiple pages. + pub owner_node: Cow<'a, str>, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently modified this object. + pub previous_txn_lgr_seq: u32, + /// The maximum amount of currency this Check can debit the sender. If the Check is successfully + /// cashed, the destination is credited in the same currency for up to this amount. + pub send_max: Amount<'a>, + /// The sequence number of the `CheckCreate` transaction that created this check. + pub sequence: u32, + /// A hint indicating which page of the destination's owner directory links to this object, in + /// case the directory consists of multiple pages. + pub destination_node: Option>, + /// An arbitrary tag to further specify the destination for this `Check`, such as a hosted + /// recipient at the destination address. + pub destination_tag: Option, + /// Indicates the time after which this `Check` is considered expired. + pub expiration: Option, + /// Arbitrary 256-bit hash provided by the sender as a specific reason or identifier for this Check. + #[serde(rename = "InvoiceID")] + pub invoice_id: Option>, + /// An arbitrary tag to further specify the source for this Check, such as a hosted recipient at + /// the sender's address. + pub source_tag: Option, +} + +impl<'a> Default for Check<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::Check, + flags: Default::default(), + index: Default::default(), + account: Default::default(), + destination: Default::default(), + destination_node: Default::default(), + destination_tag: Default::default(), + expiration: Default::default(), + invoice_id: Default::default(), + owner_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + send_max: Default::default(), + sequence: Default::default(), + source_tag: Default::default(), + } + } +} + +impl<'a> Model for Check<'a> {} + +impl<'a> Check<'a> { + pub fn new( + index: Cow<'a, str>, + account: Cow<'a, str>, + destination: Cow<'a, str>, + owner_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + send_max: Amount<'a>, + sequence: u32, + destination_node: Option>, + destination_tag: Option, + expiration: Option, + invoice_id: Option>, + source_tag: Option, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::Check, + flags: 0, + index, + account, + destination, + owner_node, + previous_txn_id, + previous_txn_lgr_seq, + send_max, + sequence, + destination_node, + destination_tag, + expiration, + invoice_id, + source_tag, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::borrow::Cow; + + #[test] + fn test_serialize() { + let check = Check::new( + Cow::from("49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0"), + Cow::from("rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo"), + Cow::from("rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy"), + Cow::from("0000000000000000"), + Cow::from("5463C6E08862A1FAE5EDAC12D70ADB16546A1F674930521295BC082494B62924"), + 6, + Amount::XRPAmount("100000000".into()), + 2, + Some(Cow::from("0000000000000000")), + Some(1), + Some(570113521), + Some(Cow::from( + "46060241FABCF692D4D934BA2A6C4427CD4279083E38C77CBE642243E43BE291", + )), + None, + ); + let check_json = serde_json::to_string(&check).unwrap(); + let actual = check_json.as_str(); + let expected = r#"{"LedgerEntryType":"Check","Flags":0,"index":"49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0","Account":"rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo","Destination":"rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy","OwnerNode":"0000000000000000","PreviousTxnID":"5463C6E08862A1FAE5EDAC12D70ADB16546A1F674930521295BC082494B62924","PreviousTxnLgrSeq":6,"SendMax":"100000000","Sequence":2,"DestinationNode":"0000000000000000","DestinationTag":1,"Expiration":570113521,"InvoiceID":"46060241FABCF692D4D934BA2A6C4427CD4279083E38C77CBE642243E43BE291"}"#; + + assert_eq!(expected, actual) + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/deposit_preauth.rs b/src/models/ledger/objects/deposit_preauth.rs new file mode 100644 index 00000000..2ace6388 --- /dev/null +++ b/src/models/ledger/objects/deposit_preauth.rs @@ -0,0 +1,101 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// A `DepositPreauth` object tracks a preauthorization from one account to another. +/// `DepositPreauth` transactions create these objects. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct DepositPreauth<'a> { + /// The value `0x0070`, mapped to the string `DepositPreauth`, indicates that this is a + /// `DepositPreauth` object. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines no flags + /// for `DepositPreauth` objects. The value is always 0. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The account that granted the preauthorization. + pub account: Cow<'a, str>, + /// The account that received the preauthorization. + pub authorize: Cow<'a, str>, + /// A hint indicating which page of the sender's owner directory links to this object, in case + /// the directory consists of multiple pages. + pub owner_node: Cow<'a, str>, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently modified this object. + pub previous_txn_lgr_seq: u32, +} + +impl<'a> Default for DepositPreauth<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::DepositPreauth, + flags: Default::default(), + index: Default::default(), + account: Default::default(), + authorize: Default::default(), + owner_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + } + } +} + +impl<'a> Model for DepositPreauth<'a> {} + +impl<'a> DepositPreauth<'a> { + pub fn new( + index: Cow<'a, str>, + account: Cow<'a, str>, + authorize: Cow<'a, str>, + owner_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::DepositPreauth, + flags: 0, + index, + account, + authorize, + owner_node, + previous_txn_id, + previous_txn_lgr_seq, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let deposit_preauth = DepositPreauth::new( + Cow::from("4A255038CC3ADCC1A9C91509279B59908251728D0DAADB248FFE297D0F7E068C"), + Cow::from("rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8"), + Cow::from("rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de"), + Cow::from("0000000000000000"), + Cow::from("3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702"), + 7, + ); + let deposit_preauth_json = serde_json::to_string(&deposit_preauth).unwrap(); + let actual = deposit_preauth_json.as_str(); + let expected = r#"{"LedgerEntryType":"DepositPreauth","Flags":0,"index":"4A255038CC3ADCC1A9C91509279B59908251728D0DAADB248FFE297D0F7E068C","Account":"rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8","Authorize":"rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de","OwnerNode":"0000000000000000","PreviousTxnID":"3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702","PreviousTxnLgrSeq":7}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/directory_node.rs b/src/models/ledger/objects/directory_node.rs new file mode 100644 index 00000000..09dfe038 --- /dev/null +++ b/src/models/ledger/objects/directory_node.rs @@ -0,0 +1,148 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// The `DirectoryNode` object type provides a list of links to other objects in the ledger's state +/// tree. A single conceptual Directory takes the form of a doubly linked list, with one or more +/// `DirectoryNode` objects each containing up to 32 IDs of other objects. The first object is called +/// the root of the directory, and all objects other than the root object can be added or deleted +/// as necessary. +/// +/// There are two kinds of Directories: +/// - `Owner` directories list other objects owned by an account, such as `RippleState` (trust line) +/// or `Offer` objects. +/// - `Offer` directories list the offers available in the decentralized exchange. A single `Offer` +/// directory contains all the offers that have the same exchange rate for the same token. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct DirectoryNode<'a> { + /// The value 0x0064, mapped to the string `DirectoryNode`, indicates that this object is part + /// of a Directory. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines no flags + /// for `DirectoryNode` objects. The value is always 0. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// (`Offer` Directories only) DEPRECATED. Do not use. + pub exchange_rate: Option>, + /// The contents of this `Directory`: an array of IDs of other objects. + pub indexes: Vec>, + /// If this `Directory` consists of multiple pages, this ID links to the next object in the chain, + /// wrapping around at the end. + pub index_next: Option, + /// If this `Directory` consists of multiple pages, this ID links to the previous object in the + /// chain, wrapping around at the beginning. + pub index_previous: Option, + /// (Owner Directories only) The address of the account that owns the objects in this directory. + pub owner: Option>, + /// The ID of root object for this directory. + pub root_index: Cow<'a, str>, + /// (`Offer` `Directories` only) The currency code of the `TakerGets` amount from the offers in this + /// directory. + pub taker_gets_currency: Option>, + /// (`Offer` `Directories` only) The issuer of the `TakerGets` amount from the offers in this + /// directory. + pub taker_gets_issuer: Option>, + /// (`Offer` `Directories` only) The currency code of the `TakerPays` amount from the offers in this + /// directory. + pub taker_pays_currency: Option>, + /// (`Offer` `Directories` only) The issuer of the `TakerPays` amount from the offers in this + /// directory. + pub taker_pays_issuer: Option>, +} + +impl<'a> Default for DirectoryNode<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::DirectoryNode, + flags: Default::default(), + index: Default::default(), + exchange_rate: Default::default(), + indexes: Default::default(), + index_next: Default::default(), + index_previous: Default::default(), + owner: Default::default(), + root_index: Default::default(), + taker_gets_currency: Default::default(), + taker_gets_issuer: Default::default(), + taker_pays_currency: Default::default(), + taker_pays_issuer: Default::default(), + } + } +} + +impl<'a> Model for DirectoryNode<'a> {} + +impl<'a> DirectoryNode<'a> { + pub fn new( + index: Cow<'a, str>, + indexes: Vec>, + root_index: Cow<'a, str>, + exchange_rate: Option>, + index_next: Option, + index_previous: Option, + owner: Option>, + taker_gets_currency: Option>, + taker_gets_issuer: Option>, + taker_pays_currency: Option>, + taker_pays_issuer: Option>, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::DirectoryNode, + flags: 0, + index, + exchange_rate, + indexes, + index_next, + index_previous, + owner, + root_index, + taker_gets_currency, + taker_gets_issuer, + taker_pays_currency, + taker_pays_issuer, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::vec; + + #[test] + fn test_serialize() { + let directory_node = DirectoryNode::new( + Cow::from("1BBEF97EDE88D40CEE2ADE6FEF121166AFE80D99EBADB01A4F069BA8FF484000"), + vec![Cow::from( + "AD7EAE148287EF12D213A251015F86E6D4BD34B3C4A0A1ED9A17198373F908AD", + )], + Cow::from("1BBEF97EDE88D40CEE2ADE6FEF121166AFE80D99EBADB01A4F069BA8FF484000"), + Some(Cow::from("4F069BA8FF484000")), + None, + None, + None, + Some(Cow::from("0000000000000000000000000000000000000000")), + Some(Cow::from("0000000000000000000000000000000000000000")), + Some(Cow::from("0000000000000000000000004A50590000000000")), + Some(Cow::from("5BBC0F22F61D9224A110650CFE21CC0C4BE13098")), + ); + let directory_node_json = serde_json::to_string(&directory_node).unwrap(); + let actual = directory_node_json.as_str(); + let expected = r#"{"LedgerEntryType":"DirectoryNode","Flags":0,"index":"1BBEF97EDE88D40CEE2ADE6FEF121166AFE80D99EBADB01A4F069BA8FF484000","ExchangeRate":"4F069BA8FF484000","Indexes":["AD7EAE148287EF12D213A251015F86E6D4BD34B3C4A0A1ED9A17198373F908AD"],"RootIndex":"1BBEF97EDE88D40CEE2ADE6FEF121166AFE80D99EBADB01A4F069BA8FF484000","TakerGetsCurrency":"0000000000000000000000000000000000000000","TakerGetsIssuer":"0000000000000000000000000000000000000000","TakerPaysCurrency":"0000000000000000000000004A50590000000000","TakerPaysIssuer":"5BBC0F22F61D9224A110650CFE21CC0C4BE13098"}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/escrow.rs b/src/models/ledger/objects/escrow.rs new file mode 100644 index 00000000..58f07061 --- /dev/null +++ b/src/models/ledger/objects/escrow.rs @@ -0,0 +1,167 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::Amount, Model}; +use alloc::borrow::Cow; +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// The `Escrow` object type represents a held payment of XRP waiting to be executed or canceled. +/// An `EscrowCreate` transaction creates an `Escrow` object in the ledger. A successful `EscrowFinish` +/// or `EscrowCancel` transaction deletes the object. If the `Escrow` object has a crypto-condition, +/// the payment can only succeed if an `EscrowFinish` transaction provides the corresponding +/// fulfillment that satisfies the condition. +/// (The only supported crypto-condition type is PREIMAGE-SHA-256.) If the `Escrow` object has a +/// `FinishAfter` time, the held payment can only execute after that time. +/// +/// An `Escrow` object is associated with two addresses: +/// - The owner, who provides the XRP when creating the `Escrow` object. If the held payment is +/// canceled, the XRP returns to the owner. +/// - The destination, where the XRP is paid when the held payment succeeds. The destination can +/// be the same as the owner. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Escrow<'a> { + /// The value `0x0075`, mapped to the string `Escrow`, indicates that this object is an + /// `Escrow` object. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines no + /// flags for `Escrow` objects. The value is always `0`. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The address of the owner (sender) of this held payment. This is the account that provided + /// the XRP, and gets it back if the held payment is canceled. + pub account: Cow<'a, str>, + /// The amount of XRP, in drops, to be delivered by the held payment. + pub amount: Amount<'a>, + /// The destination address where the XRP is paid if the held payment is successful. + pub destination: Cow<'a, str>, + /// A hint indicating which page of the owner directory links to this object, in case the + /// directory consists of multiple pages. Note: The object does not contain a direct link + /// to the owner directory containing it, since that value can be derived from the Account. + pub owner_node: Cow<'a, str>, + #[serde(rename = "PreviousTxnID")] + /// The identifying hash of the transaction that most recently modified this object. + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently modified this object. + pub previous_txn_lgr_seq: u32, + /// The held payment can be canceled if and only if this field is present and the time it + /// specifies has passed. Specifically, this is specified as seconds since the Ripple Epoch + /// and it "has passed" if it's earlier than the close time of the previous validated ledger. + pub cancel_after: Option, + /// A PREIMAGE-SHA-256 crypto-condition, as hexadecimal. If present, the `EscrowFinish` + /// transaction must contain a fulfillment that satisfies this condition. + pub condition: Option>, + /// A hint indicating which page of the destination's owner directory links to this object, + /// in case the directory consists of multiple pages. Omitted on escrows created before + /// enabling the fix1523 amendment. + pub destination_node: Option>, + /// An arbitrary tag to further specify the destination for this held payment, such as a + /// hosted recipient at the destination address. + pub destination_tag: Option, + /// The time, in seconds since the Ripple Epoch, after which this held payment can be finished. + /// Any `EscrowFinish` transaction before this time fails. + pub finish_after: Option, + /// An arbitrary tag to further specify the source for this held payment, such as a hosted + /// recipient at the owner's address. + pub source_tag: Option, +} + +impl<'a> Default for Escrow<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::Escrow, + flags: Default::default(), + index: Default::default(), + account: Default::default(), + amount: Default::default(), + cancel_after: Default::default(), + condition: Default::default(), + destination: Default::default(), + destination_node: Default::default(), + destination_tag: Default::default(), + finish_after: Default::default(), + owner_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + source_tag: Default::default(), + } + } +} + +impl<'a> Model for Escrow<'a> {} + +impl<'a> Escrow<'a> { + pub fn new( + index: Cow<'a, str>, + account: Cow<'a, str>, + amount: Amount<'a>, + destination: Cow<'a, str>, + owner_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + cancel_after: Option, + condition: Option>, + destination_node: Option>, + destination_tag: Option, + finish_after: Option, + source_tag: Option, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::Escrow, + flags: 0, + index, + account, + amount, + destination, + owner_node, + previous_txn_id, + previous_txn_lgr_seq, + cancel_after, + condition, + destination_node, + destination_tag, + finish_after, + source_tag, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::borrow::Cow; + + #[test] + fn test_serialize() { + let escrow = Escrow::new( + Cow::from("DC5F3851D8A1AB622F957761E5963BC5BD439D5C24AC6AD7AC4523F0640244AC"), + Cow::from("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"), + Amount::XRPAmount("10000".into()), + Cow::from("ra5nK24KXen9AHvsdFTKHSANinZseWnPcX"), + Cow::from("0000000000000000"), + Cow::from("C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7"), + 28991004, + Some(545440232), + Some(Cow::from( + "A0258020A82A88B2DF843A54F58772E4A3861866ECDB4157645DD9AE528C1D3AEEDABAB6810120", + )), + Some(Cow::from("0000000000000000")), + Some(23480), + Some(545354132), + Some(11747), + ); + let escrow_json = serde_json::to_string(&escrow).unwrap(); + let actual = escrow_json.as_str(); + let expected = r#"{"LedgerEntryType":"Escrow","Flags":0,"index":"DC5F3851D8A1AB622F957761E5963BC5BD439D5C24AC6AD7AC4523F0640244AC","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Amount":"10000","Destination":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","OwnerNode":"0000000000000000","PreviousTxnID":"C44F2EB84196B9AD820313DBEBA6316A15C9A2D35787579ED172B87A30131DA7","PreviousTxnLgrSeq":28991004,"CancelAfter":545440232,"Condition":"A0258020A82A88B2DF843A54F58772E4A3861866ECDB4157645DD9AE528C1D3AEEDABAB6810120","DestinationNode":"0000000000000000","DestinationTag":23480,"FinishAfter":545354132,"SourceTag":11747}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/fee_settings.rs b/src/models/ledger/objects/fee_settings.rs new file mode 100644 index 00000000..52bf1db1 --- /dev/null +++ b/src/models/ledger/objects/fee_settings.rs @@ -0,0 +1,93 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// The `FeeSettings` object type contains the current base transaction cost and reserve amounts +/// as determined by fee voting. Each ledger version contains at most one `FeeSettings` object. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct FeeSettings<'a> { + /// The value `0x0073`, mapped to the string `FeeSettings`, indicates that this object contains + /// the ledger's fee settings. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines no flags + /// for `FeeSettings` objects. The value is always `0`. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The transaction cost of the "reference transaction" in drops of XRP as hexadecimal. + pub base_fee: Cow<'a, str>, + /// The BaseFee translated into "fee units". + pub reference_fee_units: u32, + /// The base reserve for an account in the XRP Ledger, as drops of XRP. + pub reserve_base: u32, + /// The incremental owner reserve for owning objects, as drops of XRP. + pub reserve_increment: u32, +} + +impl<'a> Default for FeeSettings<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::FeeSettings, + flags: Default::default(), + index: Default::default(), + base_fee: Default::default(), + reference_fee_units: Default::default(), + reserve_base: Default::default(), + reserve_increment: Default::default(), + } + } +} + +impl<'a> Model for FeeSettings<'a> {} + +impl<'a> FeeSettings<'a> { + pub fn new( + index: Cow<'a, str>, + base_fee: Cow<'a, str>, + reference_fee_units: u32, + reserve_base: u32, + reserve_increment: u32, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::FeeSettings, + flags: 0, + index, + base_fee, + reference_fee_units, + reserve_base, + reserve_increment, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let fee_settings = FeeSettings::new( + Cow::from("4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A651"), + Cow::from("000000000000000A"), + 10, + 20000000, + 5000000, + ); + let fee_settings_json = serde_json::to_string(&fee_settings).unwrap(); + let actual = fee_settings_json.as_str(); + let expected = r#"{"LedgerEntryType":"FeeSettings","Flags":0,"index":"4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A651","BaseFee":"000000000000000A","ReferenceFeeUnits":10,"ReserveBase":20000000,"ReserveIncrement":5000000}"#; + + assert_eq!(expected, actual) + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/ledger_hashes.rs b/src/models/ledger/objects/ledger_hashes.rs new file mode 100644 index 00000000..70331f3e --- /dev/null +++ b/src/models/ledger/objects/ledger_hashes.rs @@ -0,0 +1,103 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// The `LedgerHashes` object type contains a history of prior ledgers that led up to this +/// ledger version, in the form of their hashes. Objects of this ledger type are modified +/// automatically when closing a ledger. The `LedgerHashes` objects exist to make it possible +/// to look up a previous ledger's hash with only the current ledger version and at most one +/// lookup of a previous ledger version. +/// +/// There are two kinds of LedgerHashes object. Both types have the same fields. +/// Each ledger version contains: +/// - Exactly one "recent history" LedgerHashes object +/// - A number of "previous history" `LedgerHashes` objects based on the current ledger index. +/// Specifically, the XRP Ledger adds a new "previous history" object every 65536 ledger versions. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct LedgerHashes<'a> { + /// The value `0x0068`, mapped to the string `LedgerHashes`, indicates that this object is a + /// list of ledger hashes. + pub ledger_entry_type: LedgerEntryType, + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// **DEPRECATED** Do not use. + pub first_ledger_sequence: u32, + /// An array of up to 256 ledger hashes. The contents depend on which sub-type of `LedgerHashes` + /// object this is. + pub hashes: Vec>, + /// The Ledger Index of the last entry in this object's `Hashes` array. + pub last_ledger_sequence: u32, +} + +impl<'a> Default for LedgerHashes<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::LedgerHashes, + flags: Default::default(), + index: Default::default(), + first_ledger_sequence: Default::default(), + hashes: Default::default(), + last_ledger_sequence: Default::default(), + } + } +} + +impl<'a> Model for LedgerHashes<'a> {} + +impl<'a> LedgerHashes<'a> { + pub fn new( + index: Cow<'a, str>, + first_ledger_sequence: u32, + hashes: Vec>, + last_ledger_sequence: u32, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::LedgerHashes, + flags: 0, + index, + first_ledger_sequence, + hashes, + last_ledger_sequence, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::vec; + + #[test] + fn test_serialize() { + let ledger_hashes = LedgerHashes::new( + Cow::from("B4979A36CDC7F3D3D5C31A4EAE2AC7D7209DDA877588B9AFC66799692AB0D66B"), + 2, + vec![ + Cow::from("D638208ADBD04CBB10DE7B645D3AB4BA31489379411A3A347151702B6401AA78"), + Cow::from("254D690864E418DDD9BCAC93F41B1F53B1AE693FC5FE667CE40205C322D1BE3B"), + Cow::from("A2B31D28905E2DEF926362822BC412B12ABF6942B73B72A32D46ED2ABB7ACCFA"), + Cow::from("AB4014846DF818A4B43D6B1686D0DE0644FE711577C5AB6F0B2A21CCEE280140"), + Cow::from("3383784E82A8BA45F4DD5EF4EE90A1B2D3B4571317DBAC37B859836ADDE644C1"), + ], + 33872029, + ); + let ledger_hashes_json = serde_json::to_string(&ledger_hashes).unwrap(); + let actual = ledger_hashes_json.as_str(); + let expected = r#"{"LedgerEntryType":"LedgerHashes","Flags":0,"index":"B4979A36CDC7F3D3D5C31A4EAE2AC7D7209DDA877588B9AFC66799692AB0D66B","FirstLedgerSequence":2,"Hashes":["D638208ADBD04CBB10DE7B645D3AB4BA31489379411A3A347151702B6401AA78","254D690864E418DDD9BCAC93F41B1F53B1AE693FC5FE667CE40205C322D1BE3B","A2B31D28905E2DEF926362822BC412B12ABF6942B73B72A32D46ED2ABB7ACCFA","AB4014846DF818A4B43D6B1686D0DE0644FE711577C5AB6F0B2A21CCEE280140","3383784E82A8BA45F4DD5EF4EE90A1B2D3B4571317DBAC37B859836ADDE644C1"],"LastLedgerSequence":33872029}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/mod.rs b/src/models/ledger/objects/mod.rs new file mode 100644 index 00000000..e0f64908 --- /dev/null +++ b/src/models/ledger/objects/mod.rs @@ -0,0 +1,59 @@ +pub mod account_root; +pub mod amendments; +pub mod amm; +pub mod check; +pub mod deposit_preauth; +pub mod directory_node; +pub mod escrow; +pub mod fee_settings; +pub mod ledger_hashes; +pub mod negative_unl; +pub mod nftoken_offer; +pub mod nftoken_page; +pub mod offer; +pub mod pay_channel; +pub mod ripple_state; +pub mod signer_list; +pub mod ticket; + +pub use account_root::*; +pub use amendments::*; +pub use amm::*; +pub use check::*; +pub use deposit_preauth::*; +pub use directory_node::*; +pub use escrow::*; +pub use fee_settings::*; +pub use ledger_hashes::*; +pub use negative_unl::*; +pub use nftoken_offer::*; +pub use nftoken_page::*; +pub use offer::*; +pub use pay_channel::*; +pub use ripple_state::*; +pub use ripple_state::*; +pub use ticket::*; + +use serde::{Deserialize, Serialize}; +use strum_macros::Display; + +#[derive(Debug, Clone, Serialize, Deserialize, Display, PartialEq, Eq)] +pub enum LedgerEntryType { + AccountRoot = 0x0061, + Amendments = 0x0066, + AMM = 0x0079, + Check = 0x0043, + DepositPreauth = 0x0070, + DirectoryNode = 0x0064, + Escrow = 0x0075, + FeeSettings = 0x0073, + LedgerHashes = 0x0068, + NegativeUNL = 0x004E, + NFTokenOffer = 0x0037, + NFTokenPage = 0x0050, + Offer = 0x006F, + PayChannel = 0x0078, + RippleState = 0x0072, + SignerList = 0x0053, + Ticket = 0x0054, +} diff --git a/src/models/ledger/objects/negative_unl.rs b/src/models/ledger/objects/negative_unl.rs new file mode 100644 index 00000000..7e954136 --- /dev/null +++ b/src/models/ledger/objects/negative_unl.rs @@ -0,0 +1,110 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; + +use alloc::vec::Vec; +use derive_new::new; +use serde::{ser::SerializeMap, Deserialize, Serialize}; + +use crate::serde_with_tag; +use serde_with::skip_serializing_none; + +serde_with_tag! { + /// Each `DisabledValidator` object represents one disabled validator. + #[derive(Debug, PartialEq, Eq, Clone, new, Default)] + pub struct DisabledValidator<'a> { + /// The ledger index when the validator was added to the Negative UNL. + pub first_ledger_sequence: u32, + /// The master public key of the validator, in hexadecimal. + pub public_key: Cow<'a, str>, + } +} + +/// The NegativeUNL object type contains the current status of the Negative UNL, a list of trusted +/// validators currently believed to be offline. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NegativeUNL<'a> { + /// The value `0x004E`, mapped to the string `NegativeUNL`, indicates that this object is the + /// Negative UNL. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags. No flags are defined for the NegativeUNL object type, so this + /// value is always 0. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// A list of `DisabledValidator` objects (see below), each representing a trusted validator + /// that is currently disabled. + #[serde(borrow = "'a")] + pub disabled_validators: Option>>, + /// The public key of a trusted validator that is scheduled to be disabled in the + /// next flag ledger. + pub validator_to_disable: Option>, + /// The public key of a trusted validator in the Negative UNL that is scheduled to be + /// re-enabled in the next flag ledger. + pub validator_to_re_enable: Option>, +} + +impl<'a> Default for NegativeUNL<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::NegativeUNL, + flags: Default::default(), + index: Default::default(), + disabled_validators: Default::default(), + validator_to_disable: Default::default(), + validator_to_re_enable: Default::default(), + } + } +} + +impl<'a> Model for NegativeUNL<'a> {} + +impl<'a> NegativeUNL<'a> { + pub fn new( + index: Cow<'a, str>, + disabled_validators: Option>>, + validator_to_disable: Option>, + validator_to_re_enable: Option>, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::NegativeUNL, + flags: 0, + index, + disabled_validators, + validator_to_disable, + validator_to_re_enable, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::vec; + + #[test] + fn test_serialize() { + let negative_unl = NegativeUNL::new( + Cow::from("2E8A59AA9D3B5B186B0B9E0F62E6C02587CA74A4D778938E957B6357D364B244"), + Some(vec![DisabledValidator::new( + 1609728, + Cow::from("ED6629D456285AE3613B285F65BBFF168D695BA3921F309949AFCD2CA7AFEC16FE"), + )]), + None, + None, + ); + let negative_unl_json = serde_json::to_string(&negative_unl).unwrap(); + let actual = negative_unl_json.as_str(); + let expected = r#"{"LedgerEntryType":"NegativeUNL","Flags":0,"index":"2E8A59AA9D3B5B186B0B9E0F62E6C02587CA74A4D778938E957B6357D364B244","DisabledValidators":[{"DisabledValidator":{"FirstLedgerSequence":1609728,"PublicKey":"ED6629D456285AE3613B285F65BBFF168D695BA3921F309949AFCD2CA7AFEC16FE"}}]}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/nftoken_offer.rs b/src/models/ledger/objects/nftoken_offer.rs new file mode 100644 index 00000000..e7498089 --- /dev/null +++ b/src/models/ledger/objects/nftoken_offer.rs @@ -0,0 +1,155 @@ +use crate::_serde::lgr_obj_flags; +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::Amount, Model}; +use alloc::borrow::Cow; + +use alloc::vec::Vec; + +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum NFTokenOfferFlag { + /// If enabled, the `NFTokenOffer` is a sell offer. Otherwise, the `NFTokenOffer` is a buy offer. + LsfSellNFToken = 0x00000001, +} + +/// The `NFTokenOffer` object represents an offer to buy, sell or transfer an `NFToken` object. +/// The owner of a `NFToken` can use `NFTokenCreateOffer` to start a transaction. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NFTokenOffer<'a> { + /// The value `0x0037`, mapped to the string `NFTokenOffer`, indicates that this is an offer + /// to trade a `NFToken`. + pub ledger_entry_type: LedgerEntryType, + /// A set of flags associated with this object, used to specify various options or settings. + #[serde(with = "lgr_obj_flags")] + pub flags: Vec, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// Amount expected or offered for the `NFToken`. If the token has the `lsfOnlyXRP` flag set, + /// the amount must be specified in XRP. Sell offers that specify assets other than XRP + /// must specify a non-zero amount. Sell offers that specify XRP can be 'free' + /// (that is, the Amount field can be equal to "0"). + pub amount: Amount<'a>, + /// The `NFTokenID` of the `NFToken` object referenced by this offer. + #[serde(rename = "NFTokenID")] + pub nftoken_id: Cow<'a, str>, + /// Owner of the account that is creating and owns the offer. Only the current Owner + /// of an `NFToken` can create an offer to sell an `NFToken`, but any account can create + /// an offer to buy an NFToken. + pub owner: Cow<'a, str>, + /// Identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// Index of the ledger that contains the transaction that most recently modified this object. + pub previous_txn_lgr_seq: u32, + /// The `AccountID` for which this offer is intended. If present, only that account can + /// accept the offer. + pub destination: Option>, + /// The time after which the offer is no longer active. The value is the number of + /// seconds since the Ripple Epoch. + pub expiration: Option, + /// Internal bookkeeping, indicating the page inside the token buy or sell offer directory, + /// as appropriate, where this token is being tracked. This field allows the efficient + /// deletion of offers. + #[serde(rename = "NFTokenOfferNode")] + pub nftoken_offer_node: Option>, + /// Internal bookkeeping, indicating the page inside the owner directory where this token + /// is being tracked. This field allows the efficient deletion of offers. + pub owner_node: Option>, +} + +impl<'a> Default for NFTokenOffer<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::NFTokenOffer, + flags: Default::default(), + index: Default::default(), + amount: Default::default(), + nftoken_id: Default::default(), + owner: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + destination: Default::default(), + expiration: Default::default(), + nftoken_offer_node: Default::default(), + owner_node: Default::default(), + } + } +} + +impl<'a> Model for NFTokenOffer<'a> {} + +impl<'a> NFTokenOffer<'a> { + pub fn new( + flags: Vec, + index: Cow<'a, str>, + amount: Amount<'a>, + nftoken_id: Cow<'a, str>, + owner: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + destination: Option>, + expiration: Option, + nftoken_offer_node: Option>, + owner_node: Option>, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::NFTokenOffer, + flags, + index, + amount, + nftoken_id, + owner, + previous_txn_id, + previous_txn_lgr_seq, + destination, + expiration, + nftoken_offer_node, + owner_node, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::borrow::Cow; + use alloc::vec; + + #[test] + fn test_serialization() { + let nftoken_offer = NFTokenOffer::new( + vec![NFTokenOfferFlag::LsfSellNFToken], + Cow::from("AEBABA4FAC212BF28E0F9A9C3788A47B085557EC5D1429E7A8266FB859C863B3"), + Amount::XRPAmount("1000000".into()), + Cow::from("00081B5825A08C22787716FA031B432EBBC1B101BB54875F0002D2A400000000"), + Cow::from("rhRxL3MNvuKEjWjL7TBbZSDacb8PmzAd7m"), + Cow::from("BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769"), + 75443565, + None, + None, + Some(Cow::from("0")), + Some(Cow::from("17")), + ); + let nftoken_offer_json = serde_json::to_string(&nftoken_offer).unwrap(); + let actual = nftoken_offer_json.as_str(); + let expected = r#"{"LedgerEntryType":"NFTokenOffer","Flags":1,"index":"AEBABA4FAC212BF28E0F9A9C3788A47B085557EC5D1429E7A8266FB859C863B3","Amount":"1000000","NFTokenID":"00081B5825A08C22787716FA031B432EBBC1B101BB54875F0002D2A400000000","Owner":"rhRxL3MNvuKEjWjL7TBbZSDacb8PmzAd7m","PreviousTxnID":"BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769","PreviousTxnLgrSeq":75443565,"NFTokenOfferNode":"0","OwnerNode":"17"}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/nftoken_page.rs b/src/models/ledger/objects/nftoken_page.rs new file mode 100644 index 00000000..ff86133d --- /dev/null +++ b/src/models/ledger/objects/nftoken_page.rs @@ -0,0 +1,121 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use derive_new::new; +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, new, Default)] +#[serde(rename_all = "PascalCase")] +pub struct NFToken<'a> { + #[serde(rename = "NFTokenID")] + nftoken_id: Cow<'a, str>, + #[serde(rename = "URI")] + uri: Cow<'a, str>, +} + +/// The `NFTokenPage` object represents a collection of `NFToken` objects owned by the same account. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NFTokenPage<'a> { + /// The value `0x0050`, mapped to the string `NFTokenPage`, indicates that this is a page + /// containing `NFToken` objects. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags. No flags are defined for the NegativeUNL object type, so this + /// value is always 0. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The collection of NFToken objects contained in this `NFTokenPage` object. + /// This specification places an upper bound of 32 `NFToken` objects per page. + /// Objects are sorted from low to high with the `NFTokenID` used as the sorting parameter. + #[serde(rename = "NFTokens")] + pub nftokens: Vec>, + /// The locator of the next page, if any. Details about this field and how it should be + /// used are outlined below. + pub next_page_min: Option>, + /// The locator of the previous page, if any. Details about this field and how it should + /// be used are outlined below. + pub previous_page_min: Option>, + /// Identifies the transaction ID of the transaction that most recently modified + /// this `NFTokenPage` object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Option>, + /// The sequence of the ledger that contains the transaction that most recently + /// modified this `NFTokenPage` object. + pub previous_txn_lgr_seq: Option, +} + +impl<'a> Default for NFTokenPage<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::NFTokenPage, + flags: Default::default(), + index: Default::default(), + nftokens: Default::default(), + next_page_min: Default::default(), + previous_page_min: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + } + } +} + +impl<'a> Model for NFTokenPage<'a> {} + +impl<'a> NFTokenPage<'a> { + pub fn new( + index: Cow<'a, str>, + nftokens: Vec>, + next_page_min: Option>, + previous_page_min: Option>, + previous_txn_id: Option>, + previous_txn_lgr_seq: Option, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::NFTokenPage, + flags: 0, + index, + nftokens, + next_page_min, + previous_page_min, + previous_txn_id, + previous_txn_lgr_seq, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::vec; + + #[test] + fn test_serialize() { + let nftoken_page = NFTokenPage::new( + Cow::from("ForTest"), + vec![NFToken::new( + Cow::from("000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"), + Cow::from("697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469") + )], + Some(Cow::from("598EDFD7CF73460FB8C695d6a9397E9073781BA3B78198904F659AAA252A")), + Some(Cow::from("598EDFD7CF73460FB8C695d6a9397E907378C8A841F7204C793DCBEF5406")), + Some(Cow::from("95C8761B22894E328646F7A70035E9DFBECC90EDD83E43B7B973F626D21A0822")), + Some(42891441), + ); + let nftoken_page_json = serde_json::to_string(&nftoken_page).unwrap(); + let actual = nftoken_page_json.as_str(); + let expected = r#"{"LedgerEntryType":"NFTokenPage","Flags":0,"index":"ForTest","NFTokens":[{"NFTokenID":"000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65","URI":"697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469"}],"NextPageMin":"598EDFD7CF73460FB8C695d6a9397E9073781BA3B78198904F659AAA252A","PreviousPageMin":"598EDFD7CF73460FB8C695d6a9397E907378C8A841F7204C793DCBEF5406","PreviousTxnID":"95C8761B22894E328646F7A70035E9DFBECC90EDD83E43B7B973F626D21A0822","PreviousTxnLgrSeq":42891441}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/offer.rs b/src/models/ledger/objects/offer.rs new file mode 100644 index 00000000..d3db408c --- /dev/null +++ b/src/models/ledger/objects/offer.rs @@ -0,0 +1,162 @@ +use crate::_serde::lgr_obj_flags; +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::Amount, Model}; +use alloc::borrow::Cow; + +use alloc::vec::Vec; + +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use serde_with::skip_serializing_none; + +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum OfferFlag { + /// The object was placed as a passive Offer. + LsfPassive = 0x00010000, + /// The object was placed as a sell Offer. + LsfSell = 0x00020000, +} + +/// The Offer ledger entry describes an Offer to exchange currencies in the XRP Ledger's +/// decentralized exchange. (In finance, this is more traditionally known as an order.) +/// An OfferCreate transaction only creates an Offer entry in the ledger when the Offer +/// cannot be fully executed immediately by consuming other Offers already in the ledger. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Offer<'a> { + /// The value `0x006F`, mapped to the string `Offer`, indicates that this object + /// describes an `Offer`. + ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this offer. + #[serde(with = "lgr_obj_flags")] + flags: Vec, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The address of the account that owns this `Offer`. + pub account: Cow<'a, str>, + /// The ID of the `Offer Directory` that links to this Offer. + pub book_directory: Cow<'a, str>, + /// A hint indicating which page of the offer directory links to this object, in case + /// the directory consists of multiple pages. + pub book_node: Cow<'a, str>, + /// A hint indicating which page of the owner directory links to this object, in case + /// the directory consists of multiple pages. + pub owner_node: Cow<'a, str>, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently modified + /// this object. + pub previous_txn_lgr_seq: u32, + /// The `Sequence` value of the `OfferCreate` transaction that created this `Offer` object. + /// Used in combination with the `Account` to identify this `Offer`. + pub sequence: u32, + /// The remaining amount and type of currency being provided by the `Offer` creator. + pub taker_gets: Amount<'a>, + /// The remaining amount and type of currency requested by the `Offer` creator. + pub taker_pays: Amount<'a>, + /// Indicates the time after which this Offer is considered unfunded. + pub expiration: Option, +} + +impl<'a> Default for Offer<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::Offer, + flags: Default::default(), + index: Default::default(), + account: Default::default(), + book_directory: Default::default(), + book_node: Default::default(), + owner_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + sequence: Default::default(), + taker_gets: Default::default(), + taker_pays: Default::default(), + expiration: Default::default(), + } + } +} + +impl<'a> Model for Offer<'a> {} + +impl<'a> Offer<'a> { + pub fn new( + flags: Vec, + index: Cow<'a, str>, + account: Cow<'a, str>, + book_directory: Cow<'a, str>, + book_node: Cow<'a, str>, + owner_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + sequence: u32, + taker_gets: Amount<'a>, + taker_pays: Amount<'a>, + expiration: Option, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::Offer, + flags, + index, + account, + book_directory, + book_node, + owner_node, + previous_txn_id, + previous_txn_lgr_seq, + sequence, + taker_gets, + taker_pays, + expiration, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use crate::models::amount::IssuedCurrencyAmount; + use alloc::borrow::Cow; + use alloc::vec; + + #[test] + fn test_serialize() { + let offer = Offer::new( + vec![OfferFlag::LsfSell], + Cow::from("96F76F27D8A327FC48753167EC04A46AA0E382E6F57F32FD12274144D00F1797"), + Cow::from("rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt"), + Cow::from("ACC27DE91DBA86FC509069EAF4BC511D73128B780F2E54BF5E07A369E2446000"), + Cow::from("0000000000000000"), + Cow::from("0000000000000000"), + Cow::from("F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF"), + 14524914, + 866, + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "XAG".into(), + "r9Dr5xwkeLegBeXq6ujinjSBLQzQ1zQGjH".into(), + "37".into(), + )), + Amount::XRPAmount("79550000000".into()), + None, + ); + let offer_json = serde_json::to_string(&offer).unwrap(); + let actual = offer_json.as_str(); + let expected = r#"{"LedgerEntryType":"Offer","Flags":131072,"index":"96F76F27D8A327FC48753167EC04A46AA0E382E6F57F32FD12274144D00F1797","Account":"rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt","BookDirectory":"ACC27DE91DBA86FC509069EAF4BC511D73128B780F2E54BF5E07A369E2446000","BookNode":"0000000000000000","OwnerNode":"0000000000000000","PreviousTxnID":"F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF","PreviousTxnLgrSeq":14524914,"Sequence":866,"TakerGets":{"currency":"XAG","issuer":"r9Dr5xwkeLegBeXq6ujinjSBLQzQ1zQGjH","value":"37"},"TakerPays":"79550000000"}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/pay_channel.rs b/src/models/ledger/objects/pay_channel.rs new file mode 100644 index 00000000..d334cfb8 --- /dev/null +++ b/src/models/ledger/objects/pay_channel.rs @@ -0,0 +1,167 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::Amount, Model}; +use alloc::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// The `PayChannel` object type represents a payment channel. Payment channels enable small, +/// rapid off-ledger payments of XRP that can be later reconciled with the consensus ledger. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct PayChannel<'a> { + /// The value `0x0078`, mapped to the string `PayChannel`, indicates that this object is a + /// payment channel object. + ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines + /// no flags for PayChannel objects. The value is always 0. + flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The source address that owns this payment channel. + pub account: Cow<'a, str>, + /// Total XRP, in drops, that has been allocated to this channel. This includes XRP + /// that has been paid to the destination address. + pub amount: Amount<'a>, + /// Total XRP, in drops, already paid out by the channel. The difference between + /// this value and the `Amount` field is how much XRP can still be paid to the destination + /// address with `PaymentChannelClaim` transactions. + pub balance: Amount<'a>, + /// The destination address for this payment channel. While the payment channel is open, + /// this address is the only one that can receive XRP from the channel. + pub destination: Cow<'a, str>, + /// A hint indicating which page of the source address's owner directory links to this + /// object, in case the directory consists of multiple pages. + pub owner_node: Cow<'a, str>, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently modified + /// this object. + pub previous_txn_lgr_seq: u32, + /// Public key, in hexadecimal, of the key pair that can be used to sign claims against + /// this channel. This can be any valid secp256k1 or Ed25519 public key. + pub public_key: Cow<'a, str>, + /// Number of seconds the source address must wait to close the channel if it still has + /// any XRP in it. + pub settle_delay: u32, + /// The immutable expiration time for this payment channel, in seconds since the Ripple Epoch. + pub cancel_after: Option, + /// An arbitrary tag to further specify the destination for this payment channel, such + /// as a hosted recipient at the `destination` address. + pub destination_tag: Option, + /// A hint indicating which page of the destination's owner directory links to this object, + /// in case the directory consists of multiple pages. + pub destination_node: Option>, + /// The mutable expiration time for this payment channel, in seconds since the Ripple Epoch. + pub expiration: Option, + /// An arbitrary tag to further specify the source for this payment channel, such as a + /// hosted recipient at the owner's address. + pub source_tag: Option, +} + +impl<'a> Default for PayChannel<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::PayChannel, + flags: Default::default(), + index: Default::default(), + account: Default::default(), + amount: Default::default(), + balance: Default::default(), + destination: Default::default(), + owner_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + public_key: Default::default(), + settle_delay: Default::default(), + cancel_after: Default::default(), + destination_tag: Default::default(), + destination_node: Default::default(), + expiration: Default::default(), + source_tag: Default::default(), + } + } +} + +impl<'a> Model for PayChannel<'a> {} + +impl<'a> PayChannel<'a> { + pub fn new( + index: Cow<'a, str>, + account: Cow<'a, str>, + amount: Amount<'a>, + balance: Amount<'a>, + destination: Cow<'a, str>, + owner_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + public_key: Cow<'a, str>, + settle_delay: u32, + cancel_after: Option, + destination_tag: Option, + destination_node: Option>, + expiration: Option, + source_tag: Option, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::PayChannel, + flags: 0, + index, + account, + amount, + balance, + destination, + owner_node, + previous_txn_id, + previous_txn_lgr_seq, + public_key, + settle_delay, + cancel_after, + destination_tag, + destination_node, + expiration, + source_tag, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::borrow::Cow; + + #[test] + fn test_serialize() { + let pay_channel = PayChannel::new( + Cow::from("96F76F27D8A327FC48753167EC04A46AA0E382E6F57F32FD12274144D00F1797"), + Cow::from("rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt"), + Amount::XRPAmount("4325800".into()), + Amount::XRPAmount("2323423".into()), + Cow::from("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"), + Cow::from("0000000000000000"), + Cow::from("F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF"), + 14524914, + Cow::from("32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A"), + 3600, + Some(536891313), + Some(1002341), + Some(Cow::from("0000000000000000")), + Some(536027313), + Some(0), + ); + let pay_channel_json = serde_json::to_string(&pay_channel).unwrap(); + let actual = pay_channel_json.as_str(); + let expected = r#"{"LedgerEntryType":"PayChannel","Flags":0,"index":"96F76F27D8A327FC48753167EC04A46AA0E382E6F57F32FD12274144D00F1797","Account":"rBqb89MRQJnMPq8wTwEbtz4kvxrEDfcYvt","Amount":"4325800","Balance":"2323423","Destination":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","OwnerNode":"0000000000000000","PreviousTxnID":"F0AB71E777B2DA54B86231E19B82554EF1F8211F92ECA473121C655BFC5329BF","PreviousTxnLgrSeq":14524914,"PublicKey":"32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A","SettleDelay":3600,"CancelAfter":536891313,"DestinationTag":1002341,"DestinationNode":"0000000000000000","Expiration":536027313,"SourceTag":0}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/ripple_state.rs b/src/models/ledger/objects/ripple_state.rs new file mode 100644 index 00000000..269572ba --- /dev/null +++ b/src/models/ledger/objects/ripple_state.rs @@ -0,0 +1,191 @@ +use crate::_serde::lgr_obj_flags; +use crate::models::ledger::LedgerEntryType; +use crate::models::{amount::Amount, Model}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use serde_with::skip_serializing_none; + +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum RippleStateFlag { + /// This RippleState object contributes to the low account's owner reserve. + LsfLowReserve = 0x00010000, + /// This RippleState object contributes to the high account's owner reserve. + LsfHighReserve = 0x00020000, + /// The low account has authorized the high account to hold tokens issued by the low account. + LsfLowAuth = 0x00040000, + /// The high account has authorized the low account to hold tokens issued by the high account. + LsfHighAuth = 0x00080000, + /// The low account has disabled rippling from this trust line. + LsfLowNoRipple = 0x00100000, + /// The high account has disabled rippling from this trust line. + LsfHighNoRipple = 0x00200000, + /// The low account has frozen the trust line, preventing the high account from + /// transferring the asset. + LsfLowFreeze = 0x00400000, + /// The high account has frozen the trust line, preventing the low account from + /// transferring the asset. + LsfHighFreeze = 0x00800000, +} + +/// The RippleState object type connects two accounts in a single currency. Conceptually, +/// a RippleState object represents two trust lines between the accounts, one from each side. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct RippleState<'a> { + /// The value 0x0072, mapped to the string RippleState, indicates that this object + /// is a RippleState object. + ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean options enabled for this object. + #[serde(with = "lgr_obj_flags")] + flags: Vec, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The balance of the trust line, from the perspective of the low account. A negative + /// balance indicates that the high account holds tokens issued by the low account. + pub balance: Amount<'a>, + /// The limit that the high account has set on the trust line. The issuer is the address + /// of the high account that set this limit. + pub high_limit: Amount<'a>, + /// (Omitted in some historical ledgers) A hint indicating which page of the high account's + /// owner directory links to this object, in case the directory consists of multiple pages. + pub high_node: Cow<'a, str>, + /// The limit that the low account has set on the trust line. The issuer is the address of + /// the low account that set this limit. + pub low_limit: Amount<'a>, + /// Omitted in some historical ledgers) A hint indicating which page of the low account's + /// owner directory links to this object, in case the directory consists of multiple pages. + pub low_node: Cow<'a, str>, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently + /// modified this object. + pub previous_txn_lgr_seq: u32, + /// The inbound quality set by the high account, as an integer in the implied ratio + /// HighQualityIn: 1,000,000,000. + pub high_quality_in: Option, + /// The outbound quality set by the high account, as an integer in the implied ratio + /// HighQualityOut: 1,000,000,000. + pub high_quality_out: Option, + /// The inbound quality set by the low account, as an integer in the implied ratio + /// LowQualityIn: 1,000,000,000. + pub low_quality_in: Option, + /// The outbound quality set by the low account, as an integer in the implied ratio + /// LowQualityOut: 1,000,000,000. + pub low_quality_out: Option, +} + +impl<'a> Default for RippleState<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::RippleState, + flags: Default::default(), + index: Default::default(), + balance: Default::default(), + high_limit: Default::default(), + high_node: Default::default(), + low_limit: Default::default(), + low_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + high_quality_in: Default::default(), + high_quality_out: Default::default(), + low_quality_in: Default::default(), + low_quality_out: Default::default(), + } + } +} + +impl<'a> Model for RippleState<'a> {} + +impl<'a> RippleState<'a> { + pub fn new( + flags: Vec, + index: Cow<'a, str>, + balance: Amount<'a>, + high_limit: Amount<'a>, + high_node: Cow<'a, str>, + low_limit: Amount<'a>, + low_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + high_quality_in: Option, + high_quality_out: Option, + low_quality_in: Option, + low_quality_out: Option, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::RippleState, + flags, + index, + balance, + high_limit, + high_node, + low_limit, + low_node, + previous_txn_id, + previous_txn_lgr_seq, + high_quality_in, + high_quality_out, + low_quality_in, + low_quality_out, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use crate::models::amount::IssuedCurrencyAmount; + use alloc::{borrow::Cow, vec}; + + #[test] + fn test_serialize() { + let ripple_state = RippleState::new( + vec![RippleStateFlag::LsfHighReserve, RippleStateFlag::LsfLowAuth], + Cow::from("9CA88CDEDFF9252B3DE183CE35B038F57282BC9503CDFA1923EF9A95DF0D6F7B"), + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rrrrrrrrrrrrrrrrrrrrBZbvji".into(), + "-10".into(), + )), + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(), + "110".into(), + )), + Cow::from("0000000000000000"), + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW".into(), + "0".into(), + )), + Cow::from("0000000000000000"), + Cow::from("E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879"), + 14090896, + None, + None, + None, + None, + ); + let ripple_state_json = serde_json::to_string(&ripple_state).unwrap(); + let actual = ripple_state_json.as_str(); + let expected = r#"{"LedgerEntryType":"RippleState","Flags":393216,"index":"9CA88CDEDFF9252B3DE183CE35B038F57282BC9503CDFA1923EF9A95DF0D6F7B","Balance":{"currency":"USD","issuer":"rrrrrrrrrrrrrrrrrrrrBZbvji","value":"-10"},"HighLimit":{"currency":"USD","issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","value":"110"},"HighNode":"0000000000000000","LowLimit":{"currency":"USD","issuer":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","value":"0"},"LowNode":"0000000000000000","PreviousTxnID":"E3FE6EA3D48F0C2B639448020EA4F03D4F4F8FFDB243A852A0F59177921B4879","PreviousTxnLgrSeq":14090896}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/signer_list.rs b/src/models/ledger/objects/signer_list.rs new file mode 100644 index 00000000..53c0a93a --- /dev/null +++ b/src/models/ledger/objects/signer_list.rs @@ -0,0 +1,152 @@ +use crate::_serde::lgr_obj_flags; +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; + +use alloc::vec::Vec; +use derive_new::new; +use serde::{ser::SerializeMap, Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use crate::serde_with_tag; +use serde_with::skip_serializing_none; + +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum SignerListFlag { + /// If this flag is enabled, this SignerList counts as one item for purposes of the owner reserve. + LsfOneOwnerCount = 0x00010000, +} + +serde_with_tag! { + /// Each member of the SignerEntries field is an object that describes that signer in the list. + /// + /// `` + #[derive(Debug, PartialEq, Eq, Clone, new, Default)] + pub struct SignerEntry<'a>{ + /// An XRP Ledger address whose signature contributes to the multi-signature. + pub account: Cow<'a, str>, + /// The weight of a signature from this signer. + pub signer_weight: u16, + /// Arbitrary hexadecimal data. This can be used to identify the signer or for + /// other, related purposes. + pub wallet_locator: Option>, + } +} + +/// The SignerList object type represents a list of parties that, as a group, are authorized +/// to sign a transaction in place of an individual account. +/// +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct SignerList<'a> { + /// The value 0x0053, mapped to the string SignerList, indicates that this object is a + /// SignerList object. + ledger_entry_type: LedgerEntryType, + /// A bit-map of Boolean flags enabled for this signer list. + #[serde(with = "lgr_obj_flags")] + flags: Vec, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// A hint indicating which page of the owner directory links to this object, in case + /// the directory consists of multiple pages. + pub owner_node: Cow<'a, str>, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently + /// modified this object. + pub previous_txn_lgr_seq: u32, + /// An array of Signer Entry objects representing the parties who are part of this + /// signer list. + #[serde(borrow = "'a")] + pub signer_entries: Vec>, + /// An ID for this signer list. Currently always set to 0. + #[serde(rename = "SignerListID")] + pub signer_list_id: u32, + /// A target number for signer weights. To produce a valid signature for the owner of + /// this SignerList, the signers must provide valid signatures whose weights sum to this + /// value or more. + pub signer_quorum: u32, +} + +impl<'a> Default for SignerList<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::SignerList, + flags: Default::default(), + index: Default::default(), + owner_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + signer_entries: Default::default(), + signer_list_id: Default::default(), + signer_quorum: Default::default(), + } + } +} + +impl<'a> Model for SignerList<'a> {} + +impl<'a> SignerList<'a> { + pub fn new( + flags: Vec, + index: Cow<'a, str>, + owner_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + signer_entries: Vec>, + signer_list_id: u32, + signer_quorum: u32, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::SignerList, + flags, + index, + owner_node, + previous_txn_id, + previous_txn_lgr_seq, + signer_entries, + signer_list_id, + signer_quorum, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::vec; + + #[test] + fn test_serialize() { + let signer_list = SignerList::new( + vec![], + Cow::from("A9C28A28B85CD533217F5C0A0C7767666B093FA58A0F2D80026FCC4CD932DDC7"), + Cow::from("0000000000000000"), + Cow::from("5904C0DC72C58A83AEFED2FFC5386356AA83FCA6A88C89D00646E51E687CDBE4"), + 16061435, + vec![ + SignerEntry::new(Cow::from("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), 2, None), + SignerEntry::new(Cow::from("raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n"), 1, None), + SignerEntry::new(Cow::from("rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v"), 1, None), + ], + 0, + 3, + ); + let signer_list_json = serde_json::to_string(&signer_list).unwrap(); + let actual = signer_list_json.as_str(); + let expected = r#"{"LedgerEntryType":"SignerList","Flags":0,"index":"A9C28A28B85CD533217F5C0A0C7767666B093FA58A0F2D80026FCC4CD932DDC7","OwnerNode":"0000000000000000","PreviousTxnID":"5904C0DC72C58A83AEFED2FFC5386356AA83FCA6A88C89D00646E51E687CDBE4","PreviousTxnLgrSeq":16061435,"SignerEntries":[{"SignerEntry":{"Account":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","SignerWeight":2,"WalletLocator":null}},{"SignerEntry":{"Account":"raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n","SignerWeight":1,"WalletLocator":null}},{"SignerEntry":{"Account":"rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v","SignerWeight":1,"WalletLocator":null}}],"SignerListID":0,"SignerQuorum":3}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/ledger/objects/ticket.rs b/src/models/ledger/objects/ticket.rs new file mode 100644 index 00000000..09e9d10d --- /dev/null +++ b/src/models/ledger/objects/ticket.rs @@ -0,0 +1,101 @@ +use crate::models::ledger::LedgerEntryType; +use crate::models::Model; +use alloc::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use serde_with::skip_serializing_none; + +/// The `Ticket` object type represents a `Ticket`, which tracks an account sequence number that +/// has been set aside for future use. You can create new tickets with a `TicketCreate` transaction. +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Ticket<'a> { + /// The value 0x0054, mapped to the string Ticket, indicates that this object + /// is a Ticket object. + pub ledger_entry_type: LedgerEntryType, + /// A bit-map of boolean flags enabled for this object. Currently, the protocol defines + /// no flags for Ticket objects. The value is always 0. + pub flags: u32, + /// The object ID of a single object to retrieve from the ledger, as a + /// 64-character (256-bit) hexadecimal string. + #[serde(rename = "index")] + pub index: Cow<'a, str>, + /// The account that owns this Ticket. + pub account: Cow<'a, str>, + /// A hint indicating which page of the owner directory links to this object, in case the + /// directory consists of multiple pages. + pub owner_node: Cow<'a, str>, + /// The identifying hash of the transaction that most recently modified this object. + #[serde(rename = "PreviousTxnID")] + pub previous_txn_id: Cow<'a, str>, + /// The index of the ledger that contains the transaction that most recently + /// modified this object. + pub previous_txn_lgr_seq: u32, + /// The Sequence Number this Ticket sets aside. + pub ticket_sequence: u32, +} + +impl<'a> Default for Ticket<'a> { + fn default() -> Self { + Self { + ledger_entry_type: LedgerEntryType::Ticket, + flags: Default::default(), + index: Default::default(), + account: Default::default(), + owner_node: Default::default(), + previous_txn_id: Default::default(), + previous_txn_lgr_seq: Default::default(), + ticket_sequence: Default::default(), + } + } +} + +impl<'a> Model for Ticket<'a> {} + +impl<'a> Ticket<'a> { + pub fn new( + index: Cow<'a, str>, + account: Cow<'a, str>, + owner_node: Cow<'a, str>, + previous_txn_id: Cow<'a, str>, + previous_txn_lgr_seq: u32, + ticket_sequence: u32, + ) -> Self { + Self { + ledger_entry_type: LedgerEntryType::Ticket, + flags: 0, + index, + account, + owner_node, + previous_txn_id, + previous_txn_lgr_seq, + ticket_sequence, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let ticket = Ticket::new( + Cow::from("ForTest"), + Cow::from("rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de"), + Cow::from("0000000000000000"), + Cow::from("F19AD4577212D3BEACA0F75FE1BA1644F2E854D46E8D62E9C95D18E9708CBFB1"), + 4, + 3, + ); + let ticket_json = serde_json::to_string(&ticket).unwrap(); + let actual = ticket_json.as_str(); + let expected = r#"{"LedgerEntryType":"Ticket","Flags":0,"index":"ForTest","Account":"rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de","OwnerNode":"0000000000000000","PreviousTxnID":"F19AD4577212D3BEACA0F75FE1BA1644F2E854D46E8D62E9C95D18E9708CBFB1","PreviousTxnLgrSeq":4,"TicketSequence":3}"#; + + assert_eq!(expected, actual); + } + + // TODO: test_deserialize +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 3ffeb1a0..b3c6a22d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,8 +1,110 @@ //! Top-level modules for the models package. +//! +//! Order of models: +//! 1. Type of model +//! 2. Required common fields in alphabetical order +//! 3. Optional common fields in alphabetical order +//! 4. Required specific fields in alphabetical order +//! 5. Optional specific fields in alphabetical order + pub mod exceptions; +#[cfg(feature = "ledger")] +pub mod ledger; +pub mod model; +#[cfg(feature = "requests")] +#[allow(clippy::too_many_arguments)] +pub mod requests; +#[cfg(feature = "transactions")] +#[allow(clippy::too_many_arguments)] +pub mod transactions; + +#[cfg(feature = "amounts")] +pub mod amount; +#[cfg(feature = "currencies")] +pub mod currency; pub mod utils; -/// TODO -pub trait FromXrpl { - pub fn from_xrpl() +use derive_new::new; +pub use model::Model; + +use crate::models::currency::{Currency, XRP}; +use serde::{Deserialize, Serialize}; +use strum_macros::Display; + +/// Represents the object types that an AccountObjects +/// Request can ask for. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Display)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum AccountObjectType { + Check, + DepositPreauth, + Escrow, + Offer, + PaymentChannel, + SignerList, + RippleState, + Ticket, +} + +/// A PathStep represents an individual step along a Path. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone, new)] +#[serde(rename_all = "PascalCase")] +pub struct PathStep<'a> { + account: Option<&'a str>, + currency: Option<&'a str>, + issuer: Option<&'a str>, + r#type: Option, + type_hex: Option<&'a str>, } + +/// Returns a Currency as XRP for the currency, without a value. +fn default_xrp_currency<'a>() -> Currency<'a> { + Currency::XRP(XRP::new()) +} + +/// For use with serde defaults. +fn default_true() -> Option { + Some(true) +} + +/// For use with serde defaults. +fn default_false() -> Option { + Some(false) +} + +/// For use with serde defaults. +fn default_limit_200() -> Option { + Some(200) +} + +/// For use with serde defaults. +fn default_limit_300() -> Option { + Some(300) +} + +/// For use with serde defaults. +fn default_fee_mult_max() -> Option { + Some(10) +} + +/// For use with serde defaults. +fn default_fee_div_max() -> Option { + Some(1) +} + +// pub trait SignAndSubmitError { +// fn _get_field_error(&self) -> Result<(), XRPLSignAndSubmitException>; +// fn _get_key_type_error(&self) -> Result<(), XRPLSignAndSubmitException>; +// } +// +// pub trait SignForError { +// fn _get_field_error(&self) -> Result<(), XRPLSignForException>; +// fn _get_key_type_error(&self) -> Result<(), XRPLSignForException>; +// } +// +// pub trait SignError { +// fn _get_field_error(&self) -> Result<(), XRPLSignException>; +// fn _get_key_type_error(&self) -> Result<(), XRPLSignException>; +// } diff --git a/src/models/model.rs b/src/models/model.rs new file mode 100644 index 00000000..a4325db2 --- /dev/null +++ b/src/models/model.rs @@ -0,0 +1,27 @@ +//! Base model + +use anyhow::Result; + +/// A trait that implements basic functions to every model. +pub trait Model { + /// Collects a models errors and returns the first error that occurs. + fn get_errors(&self) -> Result<()> { + Ok(()) + } + + /// Simply forwards the error from `get_errors` if there was one. + fn validate(&self) -> Result<()> { + match self.get_errors() { + Ok(_no_error) => Ok(()), + Err(error) => Err(error), + } + } + + /// Returns whether the structure is valid. + fn is_valid(&self) -> bool { + match self.get_errors() { + Ok(_no_error) => true, + Err(_error) => false, + } + } +} diff --git a/src/models/requests/account_channels.rs b/src/models/requests/account_channels.rs new file mode 100644 index 00000000..705d5cd2 --- /dev/null +++ b/src/models/requests/account_channels.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This request returns information about an account's Payment +/// Channels. This includes only channels where the specified +/// account is the channel's source, not the destination. +/// (A channel's "source" and "owner" are the same.) All +/// information retrieved is relative to a particular version +/// of the ledger. +/// +/// See Account Channels: +/// `` +/// +/// # Examples +/// +/// ## Basic usage +/// +/// ``` +/// use xrpl::models::requests::AccountChannels; +/// +/// let json = r#"{"account":"rH6ZiHU1PGamME2LvVTxrgvfjQpppWKGmr","marker":12345678,"command":"account_channels"}"#.to_string(); +/// let model: AccountChannels = serde_json::from_str(&json).expect(""); +/// let revert: Option = match serde_json::to_string(&model) { +/// Ok(model) => Some(model), +/// Err(_) => None, +/// }; +/// +/// assert_eq!(revert, Some(json)); +/// ``` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountChannels<'a> { + /// The unique identifier of an account, typically the + /// account's Address. The request returns channels where + /// this account is the channel's owner/source. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// Limit the number of transactions to retrieve. Cannot + /// be less than 10 or more than 400. The default is 200. + pub limit: Option, + /// The unique identifier of an account, typically the + /// account's Address. If provided, filter results to + /// payment channels whose destination is this account. + pub destination_account: Option<&'a str>, + /// Value from a previous paginated response. + /// Resume retrieving data where that response left off. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::account_channels")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountChannels<'a> { + fn default() -> Self { + AccountChannels { + account: "", + id: None, + ledger_hash: None, + ledger_index: None, + limit: None, + destination_account: None, + marker: None, + command: RequestMethod::AccountChannels, + } + } +} + +impl<'a> Model for AccountChannels<'a> {} + +impl<'a> AccountChannels<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + limit: Option, + destination_account: Option<&'a str>, + marker: Option, + ) -> Self { + Self { + account, + id, + ledger_hash, + ledger_index, + limit, + destination_account, + marker, + command: RequestMethod::AccountChannels, + } + } +} diff --git a/src/models/requests/account_currencies.rs b/src/models/requests/account_currencies.rs new file mode 100644 index 00000000..35a91428 --- /dev/null +++ b/src/models/requests/account_currencies.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{default_false, requests::RequestMethod, Model}; + +/// This request retrieves a list of currencies that an account +/// can send or receive, based on its trust lines. This is not +/// a thoroughly confirmed list, but it can be used to populate +/// user interfaces. +/// +/// See Account Currencies: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountCurrencies<'a> { + /// A unique identifier for the account, most commonly + /// the account's Address. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// If true, then the account field only accepts a public + /// key or XRP Ledger address. Otherwise, account can be + /// a secret or passphrase (not recommended). + /// The default is false. + #[serde(default = "default_false")] + pub strict: Option, + /// The request method. + #[serde(default = "RequestMethod::account_currencies")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountCurrencies<'a> { + fn default() -> Self { + AccountCurrencies { + account: "", + id: None, + ledger_hash: None, + ledger_index: None, + strict: None, + command: RequestMethod::AccountCurrencies, + } + } +} + +impl<'a> Model for AccountCurrencies<'a> {} + +impl<'a> AccountCurrencies<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + strict: Option, + ) -> Self { + Self { + account, + id, + ledger_hash, + ledger_index, + strict, + command: RequestMethod::AccountCurrencies, + } + } +} diff --git a/src/models/requests/account_info.rs b/src/models/requests/account_info.rs new file mode 100644 index 00000000..4f2ca2d7 --- /dev/null +++ b/src/models/requests/account_info.rs @@ -0,0 +1,82 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This request retrieves information about an account, its +/// activity, and its XRP balance. All information retrieved +/// is relative to a particular version of the ledger. +/// +/// See Account Info: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountInfo<'a> { + /// A unique identifier for the account, most commonly the + /// account's Address. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// If true, then the account field only accepts a public + /// key or XRP Ledger address. Otherwise, account can be + /// a secret or passphrase (not recommended). + /// The default is false. + pub strict: Option, + /// If true, and the FeeEscalation amendment is enabled, + /// also returns stats about queued transactions associated + /// with this account. Can only be used when querying for the + /// data from the current open ledger. New in: rippled 0.33.0 + /// Not available from servers in Reporting Mode. + pub queue: Option, + /// If true, and the MultiSign amendment is enabled, also + /// returns any SignerList objects associated with this account. + pub signer_lists: Option, + /// The request method. + #[serde(default = "RequestMethod::account_info")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountInfo<'a> { + fn default() -> Self { + AccountInfo { + account: "", + id: None, + ledger_hash: None, + ledger_index: None, + strict: None, + queue: None, + signer_lists: None, + command: RequestMethod::AccountInfo, + } + } +} + +impl<'a> Model for AccountInfo<'a> {} + +impl<'a> AccountInfo<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + strict: Option, + queue: Option, + signer_lists: Option, + ) -> Self { + Self { + account, + id, + ledger_hash, + ledger_index, + strict, + queue, + signer_lists, + command: RequestMethod::AccountInfo, + } + } +} diff --git a/src/models/requests/account_lines.rs b/src/models/requests/account_lines.rs new file mode 100644 index 00000000..d08e88f1 --- /dev/null +++ b/src/models/requests/account_lines.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This request returns information about an account's trust +/// lines, including balances in all non-XRP currencies and +/// assets. All information retrieved is relative to a particular +/// version of the ledger. +/// +/// See Account Lines: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountLines<'a> { + /// A unique identifier for the account, most commonly the + /// account's Address. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// Limit the number of trust lines to retrieve. The server + /// is not required to honor this value. Must be within the + /// inclusive range 10 to 400. + pub limit: Option, + /// The Address of a second account. If provided, show only + /// lines of trust connecting the two accounts. + pub peer: Option<&'a str>, + /// Value from a previous paginated response. Resume retrieving + /// data where that response left off. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::account_lines")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountLines<'a> { + fn default() -> Self { + AccountLines { + account: "", + id: None, + ledger_hash: None, + ledger_index: None, + limit: None, + peer: None, + marker: None, + command: RequestMethod::AccountLines, + } + } +} + +impl<'a> Model for AccountLines<'a> {} + +impl<'a> AccountLines<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + limit: Option, + peer: Option<&'a str>, + marker: Option, + ) -> Self { + Self { + account, + id, + ledger_hash, + ledger_index, + limit, + peer, + marker, + command: RequestMethod::AccountLines, + } + } +} diff --git a/src/models/requests/account_nfts.rs b/src/models/requests/account_nfts.rs new file mode 100644 index 00000000..2cf7fb3a --- /dev/null +++ b/src/models/requests/account_nfts.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This method retrieves all of the NFTs currently owned +/// by the specified account. +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountNfts<'a> { + /// The unique identifier of an account, typically the + /// account's Address. The request returns a list of + /// NFTs owned by this account. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// Limit the number of token pages to retrieve. Each page + /// can contain up to 32 NFTs. The limit value cannot be + /// lower than 20 or more than 400. The default is 100. + pub limit: Option, + /// Value from a previous paginated response. Resume + /// retrieving data where that response left off. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::account_nfts")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountNfts<'a> { + fn default() -> Self { + AccountNfts { + account: "", + id: None, + limit: None, + marker: None, + command: RequestMethod::AccountNfts, + } + } +} + +impl<'a> Model for AccountNfts<'a> {} + +impl<'a> AccountNfts<'a> { + fn new(account: &'a str, id: Option<&'a str>, limit: Option, marker: Option) -> Self { + Self { + account, + id, + limit, + marker, + command: RequestMethod::AccountNfts, + } + } +} diff --git a/src/models/requests/account_objects.rs b/src/models/requests/account_objects.rs new file mode 100644 index 00000000..0868a200 --- /dev/null +++ b/src/models/requests/account_objects.rs @@ -0,0 +1,104 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use strum_macros::Display; + +use crate::models::{requests::RequestMethod, Model}; + +/// Represents the object types that an AccountObjects +/// Request can ask for. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Display)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum AccountObjectType { + Check, + DepositPreauth, + Escrow, + Offer, + PaymentChannel, + SignerList, + State, + Ticket, +} + +/// This request returns the raw ledger format for all objects +/// owned by an account. For a higher-level view of an account's +/// trust lines and balances, see AccountLines Request instead. +/// +/// See Account Objects: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountObjects<'a> { + /// A unique identifier for the account, most commonly the + /// account's address. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// If included, filter results to include only this type + /// of ledger object. The valid types are: check, deposit_preauth, + /// escrow, offer, payment_channel, signer_list, ticket, + /// and state (trust line). + pub r#type: Option, + /// If true, the response only includes objects that would block + /// this account from being deleted. The default is false. + pub deletion_blockers_only: Option, + /// The maximum number of objects to include in the results. + /// Must be within the inclusive range 10 to 400 on non-admin + /// connections. The default is 200. + pub limit: Option, + /// Value from a previous paginated response. Resume retrieving + /// data where that response left off. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::account_objects")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountObjects<'a> { + fn default() -> Self { + AccountObjects { + account: "", + id: None, + ledger_hash: None, + ledger_index: None, + r#type: None, + deletion_blockers_only: None, + limit: None, + marker: None, + command: RequestMethod::AccountObjects, + } + } +} + +impl<'a> Model for AccountObjects<'a> {} + +impl<'a> AccountObjects<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + r#type: Option, + deletion_blockers_only: Option, + limit: Option, + marker: Option, + ) -> Self { + Self { + account, + id, + ledger_hash, + ledger_index, + r#type, + deletion_blockers_only, + limit, + marker, + command: RequestMethod::AccountObjects, + } + } +} diff --git a/src/models/requests/account_offers.rs b/src/models/requests/account_offers.rs new file mode 100644 index 00000000..90500ebf --- /dev/null +++ b/src/models/requests/account_offers.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This request retrieves a list of offers made by a given account +/// that are outstanding as of a particular ledger version. +/// +/// See Account Offers: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountOffers<'a> { + /// A unique identifier for the account, most commonly the + /// account's Address. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string identifying the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or "current", + /// "closed", or "validated" to select a ledger dynamically. + pub ledger_index: Option<&'a str>, + /// Limit the number of transactions to retrieve. The server is + /// not required to honor this value. Must be within the inclusive + /// range 10 to 400. + pub limit: Option, + /// If true, then the account field only accepts a public key or + /// XRP Ledger address. Otherwise, account can be a secret or + /// passphrase (not recommended). The default is false. + pub strict: Option, + /// Value from a previous paginated response. Resume retrieving + /// data where that response left off. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::account_offers")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountOffers<'a> { + fn default() -> Self { + AccountOffers { + account: "", + id: None, + ledger_hash: None, + ledger_index: None, + limit: None, + strict: None, + marker: None, + command: RequestMethod::AccountOffers, + } + } +} + +impl<'a> Model for AccountOffers<'a> {} + +impl<'a> AccountOffers<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + limit: Option, + strict: Option, + marker: Option, + ) -> Self { + Self { + account, + id, + ledger_hash, + ledger_index, + limit, + strict, + marker, + command: RequestMethod::AccountOffers, + } + } +} diff --git a/src/models/requests/account_tx.rs b/src/models/requests/account_tx.rs new file mode 100644 index 00000000..d3c63107 --- /dev/null +++ b/src/models/requests/account_tx.rs @@ -0,0 +1,100 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This request retrieves from the ledger a list of +/// transactions that involved the specified account. +/// +/// See Account Tx: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct AccountTx<'a> { + /// A unique identifier for the account, most commonly the + /// account's address. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// Use to look for transactions from a single ledger only. + pub ledger_hash: Option<&'a str>, + /// Use to look for transactions from a single ledger only. + pub ledger_index: Option<&'a str>, + /// Defaults to false. If set to true, returns transactions + /// as hex strings instead of JSON. + pub binary: Option, + /// Defaults to false. If set to true, returns values indexed + /// with the oldest ledger first. Otherwise, the results are + /// indexed with the newest ledger first. + /// (Each page of results may not be internally ordered, but + /// the pages are overall ordered.) + pub forward: Option, + /// Use to specify the earliest ledger to include transactions + /// from. A value of -1 instructs the server to use the earliest + /// validated ledger version available. + pub ledger_index_min: Option, + /// Use to specify the most recent ledger to include transactions + /// from. A value of -1 instructs the server to use the most + /// recent validated ledger version available. + pub ledger_index_max: Option, + /// Default varies. Limit the number of transactions to retrieve. + /// The server is not required to honor this value. + pub limit: Option, + /// Value from a previous paginated response. Resume retrieving + /// data where that response left off. This value is stable even + /// if there is a change in the server's range of available + /// ledgers. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::account_tx")] + pub command: RequestMethod, +} + +impl<'a> Default for AccountTx<'a> { + fn default() -> Self { + AccountTx { + account: "", + id: None, + ledger_hash: None, + ledger_index: None, + binary: None, + forward: None, + ledger_index_min: None, + ledger_index_max: None, + limit: None, + marker: None, + command: RequestMethod::AccountTx, + } + } +} + +impl<'a> Model for AccountTx<'a> {} + +impl<'a> AccountTx<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + binary: Option, + forward: Option, + ledger_index_min: Option, + ledger_index_max: Option, + limit: Option, + marker: Option, + ) -> Self { + Self { + account, + id, + ledger_hash, + ledger_index, + binary, + forward, + ledger_index_min, + ledger_index_max, + limit, + marker, + command: RequestMethod::AccountTx, + } + } +} diff --git a/src/models/requests/book_offers.rs b/src/models/requests/book_offers.rs new file mode 100644 index 00000000..75551e4a --- /dev/null +++ b/src/models/requests/book_offers.rs @@ -0,0 +1,111 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{currency::Currency, requests::RequestMethod, Model}; + +/// The book_offers method retrieves a list of offers, also known +/// as the order book, between two currencies. +/// +/// See Book Offers: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct BookOffers<'a> { + /// Specification of which currency the account taking + /// the offer would receive, as an object with currency + /// and issuer fields (omit issuer for XRP), + /// like currency amounts. + pub taker_gets: Currency<'a>, + /// Specification of which currency the account taking + /// the offer would pay, as an object with currency and + /// issuer fields (omit issuer for XRP), + /// like currency amounts. + pub taker_pays: Currency<'a>, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// If provided, the server does not provide more than + /// this many offers in the results. The total number of + /// results returned may be fewer than the limit, + /// because the server omits unfunded offers. + pub limit: Option, + /// The Address of an account to use as a perspective. + /// Unfunded offers placed by this account are always + /// included in the response. (You can use this to look + /// up your own orders to cancel them.) + pub taker: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::book_offers")] + pub command: RequestMethod, +} + +impl<'a> Default for BookOffers<'a> { + fn default() -> Self { + BookOffers { + taker_gets: Default::default(), + taker_pays: Default::default(), + id: None, + ledger_hash: None, + ledger_index: None, + limit: None, + taker: None, + command: RequestMethod::BookOffers, + } + } +} + +impl<'a> Model for BookOffers<'a> {} + +impl<'a> BookOffers<'a> { + fn new( + taker_gets: Currency<'a>, + taker_pays: Currency<'a>, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + limit: Option, + taker: Option<&'a str>, + ) -> Self { + Self { + taker_gets, + taker_pays, + id, + ledger_hash, + ledger_index, + limit, + taker, + command: RequestMethod::BookOffers, + } + } +} + +#[cfg(test)] +mod test { + use crate::models::currency::{Currency, IssuedCurrency, XRP}; + + use super::BookOffers; + + #[test] + fn test_serde() { + let req = BookOffers { + taker_gets: Currency::IssuedCurrency(IssuedCurrency::new( + "EUR".into(), + "rTestIssuer".into(), + )), + taker_pays: Currency::XRP(XRP::new()), + ..Default::default() + }; + let req_as_string = serde_json::to_string(&req).unwrap(); + let req_json = req_as_string.as_str(); + let expected_json = r#"{"taker_gets":{"currency":"EUR","issuer":"rTestIssuer"},"taker_pays":{"currency":"XRP"},"command":"book_offers"}"#; + let deserialized_req: BookOffers = serde_json::from_str(req_json).unwrap(); + + assert_eq!(req_json, expected_json); + assert_eq!(req, deserialized_req); + assert_eq!(Currency::XRP(XRP::new()), deserialized_req.taker_pays); + } +} diff --git a/src/models/requests/channel_authorize.rs b/src/models/requests/channel_authorize.rs new file mode 100644 index 00000000..348eb7b1 --- /dev/null +++ b/src/models/requests/channel_authorize.rs @@ -0,0 +1,178 @@ +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::requests::XRPLChannelAuthorizeException; +use crate::{ + constants::CryptoAlgorithm, + models::{requests::RequestMethod, Model}, + Err, +}; + +/// The channel_authorize method creates a signature that can be +/// used to redeem a specific amount of XRP from a payment channel. +/// +/// Warning: Do not send secret keys to untrusted servers or +/// through unsecured network connections. (This includes the +/// secret, seed, seed_hex, or passphrase fields of this request.) +/// You should only use this method on a secure, encrypted network +/// connection to a server you run or fully trust with your funds. +/// Otherwise, eavesdroppers could use your secret key to sign +/// claims and take all the money from this payment channel and +/// anything else using the same key pair. +/// +/// See Set Up Secure Signing: +/// `` +/// +/// See Channel Authorize: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct ChannelAuthorize<'a> { + /// The unique ID of the payment channel to use. + pub channel_id: &'a str, + /// Cumulative amount of XRP, in drops, to authorize. + /// If the destination has already received a lesser amount + /// of XRP from this channel, the signature created by this + /// method can be redeemed for the difference. + pub amount: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// The secret key to use to sign the claim. This must be + /// the same key pair as the public key specified in the + /// channel. Cannot be used with seed, seed_hex, or passphrase. + pub secret: Option<&'a str>, + /// The secret seed to use to sign the claim. This must be + /// the same key pair as the public key specified in the channel. + /// Must be in the XRP Ledger's base58 format. If provided, + /// you must also specify the key_type. Cannot be used with + /// secret, seed_hex, or passphrase. + pub seed: Option<&'a str>, + /// The secret seed to use to sign the claim. This must be the + /// same key pair as the public key specified in the channel. + /// Must be in hexadecimal format. If provided, you must also + /// specify the key_type. Cannot be used with secret, seed, + /// or passphrase. + pub seed_hex: Option<&'a str>, + /// A string passphrase to use to sign the claim. This must be + /// the same key pair as the public key specified in the channel. + /// The key derived from this passphrase must match the public + /// key specified in the channel. If provided, you must also + /// specify the key_type. Cannot be used with secret, seed, + /// or seed_hex. + pub passphrase: Option<&'a str>, + /// The signing algorithm of the cryptographic key pair provided. + /// Valid types are secp256k1 or ed25519. The default is secp256k1. + pub key_type: Option, + /// The request method. + #[serde(default = "RequestMethod::channel_authorize")] + pub command: RequestMethod, +} + +impl<'a> Default for ChannelAuthorize<'a> { + fn default() -> Self { + ChannelAuthorize { + channel_id: "", + amount: "", + id: None, + secret: None, + seed: None, + seed_hex: None, + passphrase: None, + key_type: None, + command: RequestMethod::ChannelAuthorize, + } + } +} + +impl<'a> Model for ChannelAuthorize<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_field_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + } + } +} + +impl<'a> ChannelAuthorizeError for ChannelAuthorize<'a> { + fn _get_field_error(&self) -> Result<(), XRPLChannelAuthorizeException> { + let mut signing_methods = Vec::new(); + for method in [self.secret, self.seed, self.seed_hex, self.passphrase] { + if method.is_some() { + signing_methods.push(method) + } + } + if signing_methods.len() != 1 { + Err(XRPLChannelAuthorizeException::DefineExactlyOneOf { + field1: "secret", + field2: "seed", + field3: "seed_hex", + field4: "passphrase", + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> ChannelAuthorize<'a> { + fn new( + channel_id: &'a str, + amount: &'a str, + id: Option<&'a str>, + secret: Option<&'a str>, + seed: Option<&'a str>, + seed_hex: Option<&'a str>, + passphrase: Option<&'a str>, + key_type: Option, + ) -> Self { + Self { + channel_id, + amount, + id, + secret, + seed, + seed_hex, + passphrase, + key_type, + command: RequestMethod::ChannelAuthorize, + } + } +} + +pub trait ChannelAuthorizeError { + fn _get_field_error(&self) -> Result<(), XRPLChannelAuthorizeException>; +} + +#[cfg(test)] +mod test_channel_authorize_errors { + + use crate::{constants::CryptoAlgorithm, models::Model}; + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_fields_error() { + let channel_authorize = ChannelAuthorize { + command: RequestMethod::ChannelAuthorize, + channel_id: "5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3", + amount: "1000000", + id: None, + secret: None, + seed: Some(""), + seed_hex: Some(""), + passphrase: None, + key_type: Some(CryptoAlgorithm::SECP256K1), + }; + + assert_eq!( + channel_authorize.validate().unwrap_err().to_string().as_str(), + "The field `secret` can not be defined with `seed`, `seed_hex`, `passphrase`. Define exactly one of them. For more information see: " + ); + } +} diff --git a/src/models/requests/channel_verify.rs b/src/models/requests/channel_verify.rs new file mode 100644 index 00000000..4355e07f --- /dev/null +++ b/src/models/requests/channel_verify.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The channel_verify method checks the validity of a signature +/// that can be used to redeem a specific amount of XRP from a +/// payment channel. +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct ChannelVerify<'a> { + /// The Channel ID of the channel that provides the XRP. + /// This is a 64-character hexadecimal string. + pub channel_id: &'a str, + /// The amount of XRP, in drops, the provided signature authorizes. + pub amount: &'a str, + /// The public key of the channel and the key pair that was used to + /// create the signature, in hexadecimal or the XRP Ledger's + /// base58 format. + pub public_key: &'a str, + /// The signature to verify, in hexadecimal. + pub signature: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::channel_verify")] + pub command: RequestMethod, +} + +impl<'a> Default for ChannelVerify<'a> { + fn default() -> Self { + ChannelVerify { + channel_id: "", + amount: "", + public_key: "", + signature: "", + id: None, + command: RequestMethod::ChannelVerify, + } + } +} + +impl<'a> Model for ChannelVerify<'a> {} + +impl<'a> ChannelVerify<'a> { + fn new( + channel_id: &'a str, + amount: &'a str, + public_key: &'a str, + signature: &'a str, + id: Option<&'a str>, + ) -> Self { + Self { + channel_id, + amount, + public_key, + signature, + id, + command: RequestMethod::ChannelVerify, + } + } +} diff --git a/src/models/requests/deposit_authorize.rs b/src/models/requests/deposit_authorize.rs new file mode 100644 index 00000000..21a4af44 --- /dev/null +++ b/src/models/requests/deposit_authorize.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The deposit_authorized command indicates whether one account +/// is authorized to send payments directly to another. +/// +/// See Deposit Authorization: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct DepositAuthorized<'a> { + /// The sender of a possible payment. + pub source_account: &'a str, + /// The recipient of a possible payment. + pub destination_account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::deposit_authorization")] + pub command: RequestMethod, +} + +impl<'a> Default for DepositAuthorized<'a> { + fn default() -> Self { + DepositAuthorized { + source_account: "", + destination_account: "", + id: None, + ledger_hash: None, + ledger_index: None, + command: RequestMethod::DepositAuthorized, + } + } +} + +impl<'a> Model for DepositAuthorized<'a> {} + +impl<'a> DepositAuthorized<'a> { + fn new( + source_account: &'a str, + destination_account: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + ) -> Self { + Self { + source_account, + destination_account, + id, + ledger_hash, + ledger_index, + command: RequestMethod::DepositAuthorized, + } + } +} diff --git a/src/models/requests/exceptions.rs b/src/models/requests/exceptions.rs new file mode 100644 index 00000000..2e43eecc --- /dev/null +++ b/src/models/requests/exceptions.rs @@ -0,0 +1,76 @@ +use strum_macros::Display; +use thiserror_no_std::Error; + +#[derive(Debug, Clone, PartialEq, Eq, Display)] +pub enum XRPLRequestException<'a> { + XRPLChannelAuthorizeError(XRPLChannelAuthorizeException<'a>), + XRPLLedgerEntryError(XRPLLedgerEntryException<'a>), + /*SignAndSubmitError(SignAndSubmitException), + SignForError(SignForException), + SignError(SignException),*/ +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLRequestException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLChannelAuthorizeException<'a> { + /// A field cannot be defined with other fields. + #[error("The field `{field1:?}` can not be defined with `{field2:?}`, `{field3:?}`, `{field4:?}`. Define exactly one of them. For more information see: {resource:?}")] + DefineExactlyOneOf { + field1: &'a str, + field2: &'a str, + field3: &'a str, + field4: &'a str, + resource: &'a str, + }, +} + +/*impl<'a> From> for anyhow::Error { + fn from(value: XRPLChannelAuthorizeException<'a>) -> Self { + anyhow::anyhow!("{:?}", value) + } +}*/ + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLChannelAuthorizeException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLLedgerEntryException<'a> { + /// A field cannot be defined with other fields. + #[error("Define one of: `{field1:?}`, `{field2:?}`, `{field3:?}`, `{field4:?}`, `{field5:?}`, `{field6:?}`, `{field7:?}`, `{field8:?}`, `{field9:?}`, `{field10:?}`. Define exactly one of them. For more information see: {resource:?}")] + DefineExactlyOneOf { + field1: &'a str, + field2: &'a str, + field3: &'a str, + field4: &'a str, + field5: &'a str, + field6: &'a str, + field7: &'a str, + field8: &'a str, + field9: &'a str, + field10: &'a str, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLLedgerEntryException<'a> {} + +/*#[derive(Debug, Clone, PartialEq, Display)] +pub enum SignAndSubmitException { + InvalidMustSetExactlyOneOf { fields: String }, + InvalidMustOmitKeyTypeIfSecretProvided, +} + +#[derive(Debug, Clone, PartialEq, Display)] +pub enum SignForException { + InvalidMustSetExactlyOneOf { fields: String }, + InvalidMustOmitKeyTypeIfSecretProvided, +} + +#[derive(Debug, Clone, PartialEq, Display)] +pub enum SignException { + InvalidMustSetExactlyOneOf { fields: String }, + InvalidMustOmitKeyTypeIfSecretProvided, +}*/ diff --git a/src/models/requests/fee.rs b/src/models/requests/fee.rs new file mode 100644 index 00000000..a5507b32 --- /dev/null +++ b/src/models/requests/fee.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The fee command reports the current state of the open-ledger +/// requirements for the transaction cost. This requires the +/// FeeEscalation amendment to be enabled. This is a public +/// command available to unprivileged users. +/// +/// See Fee: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Fee<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::fee")] + pub command: RequestMethod, +} + +impl<'a> Default for Fee<'a> { + fn default() -> Self { + Fee { + id: None, + command: RequestMethod::Fee, + } + } +} + +impl<'a> Model for Fee<'a> {} + +impl<'a> Fee<'a> { + fn new(id: Option<&'a str>) -> Self { + Self { + id, + command: RequestMethod::Fee, + } + } +} diff --git a/src/models/requests/gateway_balances.rs b/src/models/requests/gateway_balances.rs new file mode 100644 index 00000000..122ff42c --- /dev/null +++ b/src/models/requests/gateway_balances.rs @@ -0,0 +1,71 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This request calculates the total balances issued by a +/// given account, optionally excluding amounts held by +/// operational addresses. +/// +/// See Gateway Balances: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct GatewayBalances<'a> { + /// The Address to check. This should be the issuing address. + pub account: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// If true, only accept an address or public key for the + /// account parameter. Defaults to false. + pub strict: Option, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger version to use, or a + /// shortcut string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// An operational address to exclude from the balances + /// issued, or an array of such addresses. + pub hotwallet: Option>, + /// The request method. + #[serde(default = "RequestMethod::deposit_authorization")] + pub command: RequestMethod, +} + +impl<'a> Default for GatewayBalances<'a> { + fn default() -> Self { + GatewayBalances { + account: "", + id: None, + strict: None, + ledger_hash: None, + ledger_index: None, + hotwallet: None, + command: RequestMethod::GatewayBalances, + } + } +} + +impl<'a> Model for GatewayBalances<'a> {} + +impl<'a> GatewayBalances<'a> { + fn new( + account: &'a str, + id: Option<&'a str>, + strict: Option, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + hotwallet: Option>, + ) -> Self { + Self { + account, + id, + strict, + ledger_hash, + ledger_index, + hotwallet, + command: RequestMethod::GatewayBalances, + } + } +} diff --git a/src/models/requests/ledger.rs b/src/models/requests/ledger.rs new file mode 100644 index 00000000..76d22253 --- /dev/null +++ b/src/models/requests/ledger.rs @@ -0,0 +1,105 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// Retrieve information about the public ledger. +/// +/// See Ledger Data: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Ledger<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// Admin required. If true, return full information on + /// the entire ledger. Ignored if you did not specify a + /// ledger version. Defaults to false. (Equivalent to + /// enabling transactions, accounts, and expand.) + /// Caution: This is a very large amount of data -- on + /// the order of several hundred megabytes! + pub full: Option, + /// Admin required. If true, return information on accounts + /// in the ledger. Ignored if you did not specify a ledger + /// version. Defaults to false. Caution: This returns a very + /// large amount of data! + pub accounts: Option, + /// If true, return information on transactions in the + /// specified ledger version. Defaults to false. Ignored if + /// you did not specify a ledger version. + pub transactions: Option, + /// Provide full JSON-formatted information for + /// transaction/account information instead of only hashes. + /// Defaults to false. Ignored unless you request transactions, + /// accounts, or both. + pub expand: Option, + /// If true, include owner_funds field in the metadata of + /// OfferCreate transactions in the response. Defaults to + /// false. Ignored unless transactions are included and + /// expand is true. + pub owner_funds: Option, + /// If true, and transactions and expand are both also true, + /// return transaction information in binary format + /// (hexadecimal string) instead of JSON format. + pub binary: Option, + /// If true, and the command is requesting the current ledger, + /// includes an array of queued transactions in the results. + pub queue: Option, + /// The request method. + #[serde(default = "RequestMethod::ledger")] + pub command: RequestMethod, +} + +impl<'a> Default for Ledger<'a> { + fn default() -> Self { + Ledger { + id: None, + ledger_hash: None, + ledger_index: None, + full: None, + accounts: None, + transactions: None, + expand: None, + owner_funds: None, + binary: None, + queue: None, + command: RequestMethod::Ledger, + } + } +} + +impl<'a> Model for Ledger<'a> {} + +impl<'a> Ledger<'a> { + fn new( + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + full: Option, + accounts: Option, + transactions: Option, + expand: Option, + owner_funds: Option, + binary: Option, + queue: Option, + ) -> Self { + Self { + id, + ledger_hash, + ledger_index, + full, + accounts, + transactions, + expand, + owner_funds, + binary, + queue, + command: RequestMethod::Ledger, + } + } +} diff --git a/src/models/requests/ledger_closed.rs b/src/models/requests/ledger_closed.rs new file mode 100644 index 00000000..1b76d328 --- /dev/null +++ b/src/models/requests/ledger_closed.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The ledger_closed method returns the unique identifiers of +/// the most recently closed ledger. (This ledger is not +/// necessarily validated and immutable yet.) +/// +/// See Ledger Closed: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct LedgerClosed<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::ledger_closed")] + pub command: RequestMethod, +} + +impl<'a> Default for LedgerClosed<'a> { + fn default() -> Self { + LedgerClosed { + id: None, + command: RequestMethod::LedgerClosed, + } + } +} + +impl<'a> Model for LedgerClosed<'a> {} + +impl<'a> LedgerClosed<'a> { + fn new(id: Option<&'a str>) -> Self { + Self { + id, + command: RequestMethod::LedgerClosed, + } + } +} diff --git a/src/models/requests/ledger_current.rs b/src/models/requests/ledger_current.rs new file mode 100644 index 00000000..93e6bfac --- /dev/null +++ b/src/models/requests/ledger_current.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The ledger_closed method returns the unique identifiers of +/// the most recently closed ledger. (This ledger is not +/// necessarily validated and immutable yet.) +/// +/// See Ledger Closed: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct LedgerCurrent<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::ledger_current")] + pub command: RequestMethod, +} + +impl<'a> Default for LedgerCurrent<'a> { + fn default() -> Self { + LedgerCurrent { + id: None, + command: RequestMethod::LedgerCurrent, + } + } +} + +impl<'a> Model for LedgerCurrent<'a> {} + +impl<'a> LedgerCurrent<'a> { + fn new(id: Option<&'a str>) -> Self { + Self { + id, + command: RequestMethod::LedgerCurrent, + } + } +} diff --git a/src/models/requests/ledger_data.rs b/src/models/requests/ledger_data.rs new file mode 100644 index 00000000..e5197671 --- /dev/null +++ b/src/models/requests/ledger_data.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The ledger_data method retrieves contents of the specified +/// ledger. You can iterate through several calls to retrieve +/// the entire contents of a single ledger version. +/// +/// See Ledger Data: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct LedgerData<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// If set to true, return ledger objects as hashed hex + /// strings instead of JSON. + pub binary: Option, + /// Limit the number of ledger objects to retrieve. + /// The server is not required to honor this value. + pub limit: Option, + /// Value from a previous paginated response. + /// Resume retrieving data where that response left off. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::ledger_data")] + pub command: RequestMethod, +} + +impl<'a> Default for LedgerData<'a> { + fn default() -> Self { + LedgerData { + id: None, + ledger_hash: None, + ledger_index: None, + binary: None, + limit: None, + marker: None, + command: RequestMethod::LedgerData, + } + } +} + +impl<'a> Model for LedgerData<'a> {} + +impl<'a> LedgerData<'a> { + fn new( + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + binary: Option, + limit: Option, + marker: Option, + ) -> Self { + Self { + id, + ledger_hash, + ledger_index, + binary, + limit, + marker, + command: RequestMethod::LedgerData, + } + } +} diff --git a/src/models/requests/ledger_entry.rs b/src/models/requests/ledger_entry.rs new file mode 100644 index 00000000..a21bd5a6 --- /dev/null +++ b/src/models/requests/ledger_entry.rs @@ -0,0 +1,270 @@ +use crate::Err; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::requests::XRPLLedgerEntryException; +use crate::models::{requests::RequestMethod, Model}; + +/// Required fields for requesting a DepositPreauth if not +/// querying by object ID. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct DepositPreauth<'a> { + pub owner: &'a str, + pub authorized: &'a str, +} + +/// Required fields for requesting a DirectoryNode if not +/// querying by object ID. +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Directory<'a> { + pub owner: &'a str, + pub dir_root: &'a str, + pub sub_index: Option, +} + +/// Required fields for requesting a Escrow if not querying +/// by object ID. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Escrow<'a> { + pub owner: &'a str, + pub seq: u64, +} + +/// Required fields for requesting a Escrow if not querying +/// by object ID. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Offer<'a> { + pub account: &'a str, + pub seq: u64, +} + +/// Required fields for requesting a Ticket, if not +/// querying by object ID. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Ticket<'a> { + pub owner: &'a str, + pub ticket_sequence: u64, +} + +/// Required fields for requesting a RippleState. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct RippleState<'a> { + pub account: &'a str, + pub currency: &'a str, +} + +/// The ledger_entry method returns a single ledger object +/// from the XRP Ledger in its raw format. See ledger formats +/// for information on the different types of objects you can +/// retrieve. +/// +/// See Ledger Formats: +/// `` +/// +/// See Ledger Entry: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct LedgerEntry<'a> { + /// The unique request id. + pub id: Option<&'a str>, + pub index: Option<&'a str>, + pub account_root: Option<&'a str>, + pub check: Option<&'a str>, + pub payment_channel: Option<&'a str>, + pub deposit_preauth: Option>, + pub directory: Option>, + pub escrow: Option>, + pub offer: Option>, + pub ripple_state: Option>, + pub ticket: Option>, + /// If true, return the requested ledger object's contents as a + /// hex string in the XRP Ledger's binary format. Otherwise, return + /// data in JSON format. The default is false. + pub binary: Option, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut string + /// (e.g. "validated" or "closed" or "current") to choose a ledger + /// automatically. + pub ledger_index: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::ledger_entry")] + pub command: RequestMethod, +} + +impl<'a> Default for LedgerEntry<'a> { + fn default() -> Self { + LedgerEntry { + id: None, + index: None, + account_root: None, + check: None, + payment_channel: None, + deposit_preauth: None, + directory: None, + escrow: None, + offer: None, + ripple_state: None, + ticket: None, + binary: None, + ledger_hash: None, + ledger_index: None, + command: RequestMethod::LedgerEntry, + } + } +} + +impl<'a: 'static> Model for LedgerEntry<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_field_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + } + } +} + +impl<'a> LedgerEntryError for LedgerEntry<'a> { + fn _get_field_error(&self) -> Result<(), XRPLLedgerEntryException> { + let mut signing_methods: u32 = 0; + for method in [self.index, self.account_root, self.check] { + if method.is_some() { + signing_methods += 1 + } + } + if self.directory.is_some() { + signing_methods += 1 + } + if self.offer.is_some() { + signing_methods += 1 + } + if self.ripple_state.is_some() { + signing_methods += 1 + } + if self.escrow.is_some() { + signing_methods += 1 + } + if self.payment_channel.is_some() { + signing_methods += 1 + } + if self.deposit_preauth.is_some() { + signing_methods += 1 + } + if self.ticket.is_some() { + signing_methods += 1 + } + if signing_methods != 1 { + Err(XRPLLedgerEntryException::DefineExactlyOneOf { + field1: "index", + field2: "account_root", + field3: "check", + field4: "directory", + field5: "offer", + field6: "ripple_state", + field7: "escrow", + field8: "payment_channel", + field9: "deposit_preauth", + field10: "ticket", + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> LedgerEntry<'a> { + fn new( + id: Option<&'a str>, + index: Option<&'a str>, + account_root: Option<&'a str>, + check: Option<&'a str>, + payment_channel: Option<&'a str>, + deposit_preauth: Option>, + directory: Option>, + escrow: Option>, + offer: Option>, + ripple_state: Option>, + ticket: Option>, + binary: Option, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + ) -> Self { + Self { + id, + index, + account_root, + check, + payment_channel, + deposit_preauth, + directory, + escrow, + offer, + ripple_state, + ticket, + binary, + ledger_hash, + ledger_index, + command: RequestMethod::LedgerData, + } + } +} + +pub trait LedgerEntryError { + fn _get_field_error(&self) -> Result<(), XRPLLedgerEntryException>; +} + +#[cfg(test)] +mod test_ledger_entry_errors { + use super::Offer; + use crate::models::requests::XRPLLedgerEntryException; + use crate::models::Model; + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_fields_error() { + let ledger_entry = LedgerEntry { + command: RequestMethod::LedgerEntry, + id: None, + index: None, + account_root: Some("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"), + check: None, + payment_channel: None, + deposit_preauth: None, + directory: None, + escrow: None, + offer: Some(Offer { + account: "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + seq: 359, + }), + ripple_state: None, + ticket: None, + binary: None, + ledger_hash: None, + ledger_index: None, + }; + let _expected = XRPLLedgerEntryException::DefineExactlyOneOf { + field1: "index", + field2: "account_root", + field3: "check", + field4: "directory", + field5: "offer", + field6: "ripple_state", + field7: "escrow", + field8: "payment_channel", + field9: "deposit_preauth", + field10: "ticket", + resource: "", + }; + assert_eq!( + ledger_entry.validate().unwrap_err().to_string().as_str(), + "Define one of: `index`, `account_root`, `check`, `directory`, `offer`, `ripple_state`, `escrow`, `payment_channel`, `deposit_preauth`, `ticket`. Define exactly one of them. For more information see: " + ); + } +} diff --git a/src/models/requests/manifest.rs b/src/models/requests/manifest.rs new file mode 100644 index 00000000..9705a6f3 --- /dev/null +++ b/src/models/requests/manifest.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The manifest method reports the current "manifest" +/// information for a given validator public key. The +/// "manifest" is the public portion of that validator's +/// configured token. +/// +/// See Manifest: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Manifest<'a> { + /// The base58-encoded public key of the validator + /// to look up. This can be the master public key or + /// ephemeral public key. + pub public_key: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::manifest")] + pub command: RequestMethod, +} + +impl<'a> Default for Manifest<'a> { + fn default() -> Self { + Manifest { + public_key: "", + id: None, + command: RequestMethod::Manifest, + } + } +} + +impl<'a> Model for Manifest<'a> {} + +impl<'a> Manifest<'a> { + fn new(public_key: &'a str, id: Option<&'a str>) -> Self { + Self { + public_key, + id, + command: RequestMethod::Manifest, + } + } +} diff --git a/src/models/requests/mod.rs b/src/models/requests/mod.rs new file mode 100644 index 00000000..9473949d --- /dev/null +++ b/src/models/requests/mod.rs @@ -0,0 +1,249 @@ +pub mod account_channels; +pub mod account_currencies; +pub mod account_info; +pub mod account_lines; +pub mod account_nfts; +pub mod account_objects; +pub mod account_offers; +pub mod account_tx; +pub mod book_offers; +pub mod channel_authorize; +pub mod channel_verify; +pub mod deposit_authorize; +pub mod exceptions; +pub mod fee; +pub mod gateway_balances; +pub mod ledger; +pub mod ledger_closed; +pub mod ledger_current; +pub mod ledger_data; +pub mod ledger_entry; +pub mod manifest; +pub mod nft_buy_offers; +pub mod nft_sell_offers; +pub mod no_ripple_check; +pub mod path_find; +pub mod ping; +pub mod random; +pub mod ripple_path_find; +pub mod server_info; +pub mod server_state; +pub mod submit; +pub mod submit_multisigned; +pub mod subscribe; +pub mod transaction_entry; +pub mod tx; +pub mod unsubscribe; + +pub use account_channels::*; +pub use account_currencies::*; +pub use account_info::*; +pub use account_lines::*; +pub use account_nfts::*; +pub use account_objects::*; +pub use account_offers::*; +pub use account_tx::*; +pub use book_offers::*; +pub use channel_authorize::*; +pub use channel_verify::*; +pub use deposit_authorize::*; +pub use exceptions::*; +pub use fee::*; +pub use gateway_balances::*; +pub use ledger::*; +pub use ledger_closed::*; +pub use ledger_current::*; +pub use ledger_data::*; +pub use ledger_entry::*; +pub use manifest::*; +pub use nft_buy_offers::*; +pub use nft_sell_offers::*; +pub use no_ripple_check::*; +pub use path_find::*; +pub use ping::*; +pub use random::*; +pub use ripple_path_find::*; +pub use server_info::*; +pub use server_state::*; +pub use submit::*; +pub use submit_multisigned::*; +pub use subscribe::*; +pub use transaction_entry::*; +pub use tx::*; +pub use unsubscribe::*; + +use serde::{Deserialize, Serialize}; +use strum_macros::Display; + +/// Represents the different options for the `method` +/// field in a request. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Display)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RequestMethod { + // Account methods + AccountChannels, + AccountCurrencies, + AccountInfo, + AccountLines, + AccountNfts, + AccountObjects, + AccountOffers, + AccountTx, + GatewayBalances, + NoRippleCheck, + + // Transaction methods + Sign, + SignFor, + Submit, + SubmitMultisigned, + TransactionEntry, + Tx, + + // Channel methods + ChannelAuthorize, + ChannelVerify, + + // Path methods + BookOffers, + DepositAuthorized, + NftBuyOffers, + NftSellOffers, + PathFind, + RipplePathFind, + + // Ledger methods + Ledger, + LedgerClosed, + LedgerCurrent, + LedgerData, + LedgerEntry, + + // Subscription methods + Subscribe, + Unsubscribe, + + // Server info methods + Fee, + Manifest, + ServerInfo, + ServerState, + + // Utility methods + Ping, + Random, +} + +/// For use with serde defaults. +/// TODO Find a better way +impl RequestMethod { + fn account_channels() -> Self { + RequestMethod::AccountChannels + } + fn account_currencies() -> Self { + RequestMethod::AccountCurrencies + } + fn account_info() -> Self { + RequestMethod::AccountInfo + } + fn account_lines() -> Self { + RequestMethod::AccountLines + } + fn account_nfts() -> Self { + RequestMethod::AccountNfts + } + fn account_objects() -> Self { + RequestMethod::AccountObjects + } + fn account_offers() -> Self { + RequestMethod::AccountOffers + } + fn account_tx() -> Self { + RequestMethod::AccountTx + } + fn book_offers() -> Self { + RequestMethod::BookOffers + } + fn channel_authorize() -> Self { + RequestMethod::ChannelAuthorize + } + fn channel_verify() -> Self { + RequestMethod::ChannelVerify + } + fn deposit_authorization() -> Self { + RequestMethod::DepositAuthorized + } + fn fee() -> Self { + RequestMethod::Fee + } + fn ledger_closed() -> Self { + RequestMethod::LedgerClosed + } + fn ledger_current() -> Self { + RequestMethod::LedgerCurrent + } + fn ledger_data() -> Self { + RequestMethod::LedgerData + } + fn ledger_entry() -> Self { + RequestMethod::LedgerEntry + } + fn ledger() -> Self { + RequestMethod::Ledger + } + fn manifest() -> Self { + RequestMethod::Manifest + } + fn nft_buy_offers() -> Self { + RequestMethod::NftBuyOffers + } + fn nft_sell_offers() -> Self { + RequestMethod::NftSellOffers + } + fn no_ripple_check() -> Self { + RequestMethod::NoRippleCheck + } + fn path_find() -> Self { + RequestMethod::PathFind + } + fn ripple_path_find() -> Self { + RequestMethod::RipplePathFind + } + fn ping() -> Self { + RequestMethod::Ping + } + fn random() -> Self { + RequestMethod::Random + } + fn server_info() -> Self { + RequestMethod::ServerInfo + } + fn server_state() -> Self { + RequestMethod::ServerState + } + fn submit() -> Self { + RequestMethod::Submit + } + fn sign_for() -> Self { + RequestMethod::SignFor + } + fn sign() -> Self { + RequestMethod::Sign + } + fn submit_multisigned() -> Self { + RequestMethod::SubmitMultisigned + } + fn subscribe() -> Self { + RequestMethod::Subscribe + } + fn unsubscribe() -> Self { + RequestMethod::Unsubscribe + } + fn transaction_entry() -> Self { + RequestMethod::TransactionEntry + } + fn tx() -> Self { + RequestMethod::Tx + } +} diff --git a/src/models/requests/nft_buy_offers.rs b/src/models/requests/nft_buy_offers.rs new file mode 100644 index 00000000..b34308d3 --- /dev/null +++ b/src/models/requests/nft_buy_offers.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This method retrieves all of buy offers for the specified NFToken. +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct NftBuyOffers<'a> { + /// The unique identifier of a NFToken object. + pub nft_id: &'a str, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// Limit the number of NFT buy offers to retrieve. + /// This value cannot be lower than 50 or more than 500. + /// The default is 250. + pub limit: Option, + /// Value from a previous paginated response. + /// Resume retrieving data where that response left off. + pub marker: Option, + /// The request method. + #[serde(default = "RequestMethod::nft_buy_offers")] + pub command: RequestMethod, +} + +impl<'a> Default for NftBuyOffers<'a> { + fn default() -> Self { + NftBuyOffers { + nft_id: "", + ledger_hash: None, + ledger_index: None, + limit: None, + marker: None, + command: RequestMethod::NftBuyOffers, + } + } +} + +impl<'a> Model for NftBuyOffers<'a> {} + +impl<'a> NftBuyOffers<'a> { + fn new( + nft_id: &'a str, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + limit: Option, + marker: Option, + ) -> Self { + Self { + nft_id, + ledger_hash, + ledger_index, + limit, + marker, + command: RequestMethod::NftBuyOffers, + } + } +} diff --git a/src/models/requests/nft_sell_offers.rs b/src/models/requests/nft_sell_offers.rs new file mode 100644 index 00000000..35273506 --- /dev/null +++ b/src/models/requests/nft_sell_offers.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// This method retrieves all of sell offers for the specified NFToken. +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct NftSellOffers<'a> { + /// The unique identifier of a NFToken object. + pub nft_id: &'a str, + /// The request method. + #[serde(default = "RequestMethod::nft_sell_offers")] + pub command: RequestMethod, +} + +impl<'a> Default for NftSellOffers<'a> { + fn default() -> Self { + NftSellOffers { + nft_id: "", + command: RequestMethod::NftSellOffers, + } + } +} + +impl<'a> Model for NftSellOffers<'a> {} + +impl<'a> NftSellOffers<'a> { + fn new(nft_id: &'a str) -> Self { + Self { + nft_id, + command: RequestMethod::NftSellOffers, + } + } +} diff --git a/src/models/requests/no_ripple_check.rs b/src/models/requests/no_ripple_check.rs new file mode 100644 index 00000000..5c52ef61 --- /dev/null +++ b/src/models/requests/no_ripple_check.rs @@ -0,0 +1,96 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use strum_macros::Display; + +use crate::models::{requests::RequestMethod, Model}; + +/// Enum representing the options for the address role in +/// a NoRippleCheckRequest. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +#[serde(tag = "role")] +#[derive(Default)] +pub enum NoRippleCheckRole { + #[default] + User, + Gateway, +} + +/// This request provides a quick way to check the status of +/// the Default Ripple field for an account and the No Ripple +/// flag of its trust lines, compared with the recommended +/// settings. +/// +/// See No Ripple Check: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct NoRippleCheck<'a> { + /// A unique identifier for the account, most commonly the + /// account's address. + pub account: &'a str, + /// Whether the address refers to a gateway or user. + /// Recommendations depend on the role of the account. + /// Issuers must have Default Ripple enabled and must disable + /// No Ripple on all trust lines. Users should have Default Ripple + /// disabled, and should enable No Ripple on all trust lines. + pub role: NoRippleCheckRole, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut string + /// to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// If true, include an array of suggested transactions, as JSON + /// objects, that you can sign and submit to fix the problems. + /// Defaults to false. + pub transactions: Option, + /// The maximum number of trust line problems to include in the + /// results. Defaults to 300. + pub limit: Option, + /// The request method. + #[serde(default = "RequestMethod::no_ripple_check")] + pub command: RequestMethod, +} + +impl<'a> Default for NoRippleCheck<'a> { + fn default() -> Self { + NoRippleCheck { + account: "", + role: Default::default(), + id: None, + ledger_hash: None, + ledger_index: None, + transactions: None, + limit: None, + command: RequestMethod::NoRippleCheck, + } + } +} + +impl<'a> Model for NoRippleCheck<'a> {} + +impl<'a> NoRippleCheck<'a> { + fn new( + account: &'a str, + role: NoRippleCheckRole, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + transactions: Option, + limit: Option, + ) -> Self { + Self { + account, + role, + id, + ledger_hash, + ledger_index, + transactions, + limit, + command: RequestMethod::NoRippleCheck, + } + } +} diff --git a/src/models/requests/path_find.rs b/src/models/requests/path_find.rs new file mode 100644 index 00000000..57bc751b --- /dev/null +++ b/src/models/requests/path_find.rs @@ -0,0 +1,128 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::currency::{Currency, XRP}; +use crate::models::{requests::RequestMethod, Model, PathStep}; + +/// A path is an array. Each member of a path is an object that specifies a step on that path. +pub type Path<'a> = Vec>; + +/// There are three different modes, or sub-commands, of +/// the path_find command. Specify which one you want with +/// the subcommand parameter: +/// * create - Start sending pathfinding information +/// * close - Stop sending pathfinding information +/// * status - Info on the currently-open pathfinding request +/// +/// See Path Find: +/// `` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum PathFindSubcommand { + #[default] + Create, + Close, + Status, +} + +/// WebSocket API only! The path_find method searches for +/// a path along which a transaction can possibly be made, +/// and periodically sends updates when the path changes +/// over time. For a simpl<'a>er version that is supported by +/// JSON-RPC, see the ripple_path_find method. For payments +/// occurring strictly in XRP, it is not necessary to find +/// a path, because XRP can be sent directly to any account. +/// +/// Although the rippled server tries to find the cheapest +/// path or combination of paths for making a payment, it is +/// not guaranteed that the paths returned by this method +/// are, in fact, the best paths. Due to server load, +/// pathfinding may not find the best results. Additionally, +/// you should be careful with the pathfinding results from +/// untrusted servers. A server could be modified to return +/// less-than-optimal paths to earn money for its operators. +/// If you do not have your own server that you can trust +/// with pathfinding, you should compare the results of +/// pathfinding from multiple servers run by different +/// parties, to minimize the risk of a single server +/// returning poor results. (Note: A server returning +/// less-than-optimal results is not necessarily proof of +/// malicious behavior; it could also be a symptom of heavy +/// server load.) +/// +/// See Path Find: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct PathFind<'a> { + /// Use "create" to send the create sub-command. + pub subcommand: PathFindSubcommand, + /// Unique address of the account to find a path + /// from. (In other words, the account that would + /// be sending a payment.) + pub source_account: &'a str, + /// Unique address of the account to find a path to. + /// (In other words, the account that would receive a payment.) + pub destination_account: &'a str, + /// Currency Amount that the destination account would + /// receive in a transaction. Special case: New in: rippled 0.30.0 + /// You can specify "-1" (for XRP) or provide -1 as the contents of + /// the value field (for non-XRP currencies). This requests a path + /// to deliver as much as possible, while spending no more than + /// the amount specified in send_max (if provided). + pub destination_amount: Currency<'a>, + /// The unique request id. + pub id: Option<&'a str>, + /// Currency Amount that would be spent in the transaction. + /// Not compatible with source_currencies. + pub send_max: Option>, + /// Array of arrays of objects, representing payment paths to check. + /// You can use this to keep updated on changes to particular paths + /// you already know about, or to check the overall cost to make a + /// payment along a certain path. + pub paths: Option>>, + /// The request method. + #[serde(default = "RequestMethod::path_find")] + pub command: RequestMethod, +} + +impl<'a> Default for PathFind<'a> { + fn default() -> Self { + PathFind { + subcommand: Default::default(), + source_account: "", + destination_account: "", + destination_amount: Currency::XRP(XRP::new()), + id: None, + send_max: None, + paths: None, + command: RequestMethod::PathFind, + } + } +} + +impl<'a> Model for PathFind<'a> {} + +impl<'a> PathFind<'a> { + fn new( + subcommand: PathFindSubcommand, + source_account: &'a str, + destination_account: &'a str, + destination_amount: Currency<'a>, + id: Option<&'a str>, + send_max: Option>, + paths: Option>>>, + ) -> Self { + Self { + subcommand, + source_account, + destination_account, + destination_amount, + id, + send_max, + paths, + command: RequestMethod::PathFind, + } + } +} diff --git a/src/models/requests/ping.rs b/src/models/requests/ping.rs new file mode 100644 index 00000000..24e1c68b --- /dev/null +++ b/src/models/requests/ping.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The ping command returns an acknowledgement, so that +/// clients can test the connection status and latency. +/// +/// See Ping: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Ping<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::ping")] + pub command: RequestMethod, +} + +impl<'a> Default for Ping<'a> { + fn default() -> Self { + Ping { + id: None, + command: RequestMethod::Ping, + } + } +} + +impl<'a> Model for Ping<'a> {} + +impl<'a> Ping<'a> { + fn new(id: Option<&'a str>) -> Self { + Self { + id, + command: RequestMethod::Ping, + } + } +} diff --git a/src/models/requests/random.rs b/src/models/requests/random.rs new file mode 100644 index 00000000..8cf99192 --- /dev/null +++ b/src/models/requests/random.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The random command provides a random number to be used +/// as a source of entropy for random number generation +/// by clients. +/// +/// See Random: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Random<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::random")] + pub command: RequestMethod, +} + +impl<'a> Default for Random<'a> { + fn default() -> Self { + Random { + id: None, + command: RequestMethod::Random, + } + } +} + +impl<'a> Model for Random<'a> {} + +impl<'a> Random<'a> { + fn new(id: Option<&'a str>) -> Self { + Self { + id, + command: RequestMethod::Random, + } + } +} diff --git a/src/models/requests/ripple_path_find.rs b/src/models/requests/ripple_path_find.rs new file mode 100644 index 00000000..cbafd6ed --- /dev/null +++ b/src/models/requests/ripple_path_find.rs @@ -0,0 +1,104 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::currency::XRP; +use crate::models::{currency::Currency, requests::RequestMethod, Model}; + +/// The ripple_path_find method is a simpl<'a>ified version of +/// the path_find method that provides a single response with +/// a payment path you can use right away. It is available in +/// both the WebSocket and JSON-RPC APIs. However, the +/// results tend to become outdated as time passes. Instead of +/// making multiple calls to stay updated, you should instead +/// use the path_find method to subscribe to continued updates +/// where possible. +/// +/// Although the rippled server tries to find the cheapest path +/// or combination of paths for making a payment, it is not +/// guaranteed that the paths returned by this method are, in +/// fact, the best paths. +/// +/// See Ripple Path Find: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct RipplePathFind<'a> { + /// Unique address of the account that would send funds + /// in a transaction. + pub source_account: &'a str, + /// Unique address of the account that would receive funds + /// in a transaction. + pub destination_account: &'a str, + /// Currency Amount that the destination account would + /// receive in a transaction. Special case: New in: rippled 0.30.0 + /// You can specify "-1" (for XRP) or provide -1 as the contents + /// of the value field (for non-XRP currencies). This requests a + /// path to deliver as much as possible, while spending no more + /// than the amount specified in send_max (if provided). + pub destination_amount: Currency<'a>, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// Currency Amount that would be spent in the transaction. + /// Cannot be used with source_currencies. + pub send_max: Option>, + /// Array of currencies that the source account might want + /// to spend. Each entry in the array should be a JSON object + /// with a mandatory currency field and optional issuer field, + /// like how currency amounts are specified. Cannot contain + /// more than 18 source currencies. By default, uses all source + /// currencies available up to a maximum of 88 different + /// currency/issuer pairs. + pub source_currencies: Option>>, + /// The request method. + #[serde(default = "RequestMethod::ripple_path_find")] + pub command: RequestMethod, +} + +impl<'a> Default for RipplePathFind<'a> { + fn default() -> Self { + RipplePathFind { + source_account: "", + destination_account: "", + destination_amount: Currency::XRP(XRP::new()), + id: None, + ledger_hash: None, + ledger_index: None, + send_max: None, + source_currencies: None, + command: RequestMethod::RipplePathFind, + } + } +} + +impl<'a> Model for RipplePathFind<'a> {} + +impl<'a> RipplePathFind<'a> { + fn new( + source_account: &'a str, + destination_account: &'a str, + destination_amount: Currency<'a>, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + send_max: Option>, + source_currencies: Option>>, + ) -> Self { + Self { + source_account, + destination_account, + destination_amount, + id, + ledger_hash, + ledger_index, + send_max, + source_currencies, + command: RequestMethod::RipplePathFind, + } + } +} diff --git a/src/models/requests/server_info.rs b/src/models/requests/server_info.rs new file mode 100644 index 00000000..0308fbd5 --- /dev/null +++ b/src/models/requests/server_info.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The server_info command asks the server for a +/// human-readable version of various information about the +/// rippled server being queried. +/// +/// See Server Info: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct ServerInfo<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// The request info. + #[serde(default = "RequestMethod::server_info")] + pub command: RequestMethod, +} + +impl<'a> Default for ServerInfo<'a> { + fn default() -> Self { + ServerInfo { + id: None, + command: RequestMethod::ServerInfo, + } + } +} + +impl<'a> Model for ServerInfo<'a> {} + +impl<'a> ServerInfo<'a> { + fn new(id: Option<&'a str>) -> Self { + Self { + id, + command: RequestMethod::ServerInfo, + } + } +} diff --git a/src/models/requests/server_state.rs b/src/models/requests/server_state.rs new file mode 100644 index 00000000..2b7942c6 --- /dev/null +++ b/src/models/requests/server_state.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The server_state command asks the server for various +/// machine-readable information about the rippled server's +/// current state. The response is almost the same as the +/// server_info method, but uses units that are easier to +/// process instead of easier to read. (For example, XRP +/// values are given in integer drops instead of scientific +/// notation or decimal values, and time is given in +/// milliseconds instead of seconds.) +/// +/// See Server State: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct ServerState<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::server_state")] + pub command: RequestMethod, +} + +impl<'a> Default for ServerState<'a> { + fn default() -> Self { + ServerState { + id: None, + command: RequestMethod::ServerState, + } + } +} + +impl<'a> Model for ServerState<'a> {} + +impl<'a> ServerState<'a> { + fn new(id: Option<&'a str>) -> Self { + Self { + id, + command: RequestMethod::ServerState, + } + } +} diff --git a/src/models/requests/submit.rs b/src/models/requests/submit.rs new file mode 100644 index 00000000..2fabd316 --- /dev/null +++ b/src/models/requests/submit.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The submit method applies a transaction and sends it to +/// the network to be confirmed and included in future ledgers. +/// +/// This command has two modes: +/// * Submit-only mode takes a signed, serialized transaction +/// as a binary blob, and submits it to the network as-is. +/// Since signed transaction objects are immutable, no part +/// of the transaction can be modified or automatically +/// filled in after submission. +/// * Sign-and-submit mode takes a JSON-formatted Transaction +/// object, completes and signs the transaction in the same +/// manner as the sign method, and then submits the signed +/// transaction. We recommend only using this mode for +/// testing and development. +/// +/// To send a transaction as robustly as possible, you should +/// construct and sign it in advance, persist it somewhere that +/// you can access even after a power outage, then submit it as +/// a tx_blob. After submission, monitor the network with the +/// tx method command to see if the transaction was successfully +/// applied; if a restart or other problem occurs, you can +/// safely re-submit the tx_blob transaction: it won't be +/// applied twice since it has the same sequence number as the +/// old transaction. +/// +/// See Submit: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Submit<'a> { + /// Hex representation of the signed transaction to submit. + /// This can also be a multi-signed transaction. + pub tx_blob: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// If true, and the transaction fails locally, do not retry + /// or relay the transaction to other servers + pub fail_hard: Option, + /// The request method. + #[serde(default = "RequestMethod::submit")] + pub command: RequestMethod, +} + +impl<'a> Default for Submit<'a> { + fn default() -> Self { + Submit { + tx_blob: "", + id: None, + fail_hard: None, + command: RequestMethod::Submit, + } + } +} + +impl<'a> Model for Submit<'a> {} + +impl<'a> Submit<'a> { + fn new(tx_blob: &'a str, id: Option<&'a str>, fail_hard: Option) -> Self { + Self { + tx_blob, + id, + fail_hard, + command: RequestMethod::Submit, + } + } +} diff --git a/src/models/requests/submit_multisigned.rs b/src/models/requests/submit_multisigned.rs new file mode 100644 index 00000000..368207a3 --- /dev/null +++ b/src/models/requests/submit_multisigned.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The server_state command asks the server for various +/// machine-readable information about the rippled server's +/// current state. The response is almost the same as the +/// server_info method, but uses units that are easier to +/// process instead of easier to read. (For example, XRP +/// values are given in integer drops instead of scientific +/// notation or decimal values, and time is given in +/// milliseconds instead of seconds.) +/// +/// See Submit Multisigned: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct SubmitMultisigned<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// If true, and the transaction fails locally, do not + /// retry or relay the transaction to other servers. + pub fail_hard: Option, + /// The request method. + #[serde(default = "RequestMethod::submit_multisigned")] + pub command: RequestMethod, +} + +impl<'a> Default for SubmitMultisigned<'a> { + fn default() -> Self { + SubmitMultisigned { + id: None, + fail_hard: None, + command: RequestMethod::SubmitMultisigned, + } + } +} + +impl<'a> Model for SubmitMultisigned<'a> {} + +impl<'a> SubmitMultisigned<'a> { + fn new(id: Option<&'a str>, fail_hard: Option) -> Self { + Self { + id, + fail_hard, + command: RequestMethod::SubmitMultisigned, + } + } +} diff --git a/src/models/requests/subscribe.rs b/src/models/requests/subscribe.rs new file mode 100644 index 00000000..5c1efd05 --- /dev/null +++ b/src/models/requests/subscribe.rs @@ -0,0 +1,118 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use strum_macros::Display; + +use crate::models::{currency::Currency, default_false, requests::RequestMethod, Model}; + +/// Format for elements in the `books` array for Subscribe only. +/// +/// See Subscribe: +/// `` +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all(serialize = "PascalCase", deserialize = "snake_case"))] +pub struct SubscribeBook<'a> { + pub taker_gets: Currency<'a>, + pub taker_pays: Currency<'a>, + pub taker: &'a str, + #[serde(default = "default_false")] + pub snapshot: Option, + #[serde(default = "default_false")] + pub both: Option, +} + +/// Represents possible values of the streams query param +/// for subscribe. +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Display)] +#[serde(rename_all = "snake_case")] +pub enum StreamParameter { + Consensus, + Ledger, + Manifests, + PeerStatus, + Transactions, + TransactionsProposed, + Server, + Validations, +} + +/// The subscribe method requests periodic notifications +/// from the server when certain events happen. +/// +/// Note: WebSocket API only. +/// +/// See Subscribe: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Subscribe<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// Array of objects defining order books to monitor for + /// updates, as detailed below. + pub books: Option>>, + /// Array of string names of generic streams to subscribe to. + pub streams: Option>, + /// Array with the unique addresses of accounts to monitor + /// for validated transactions. The addresses must be in the + /// XRP Ledger's base58 format. The server sends a notification + /// for any transaction that affects at least one of these accounts. + pub accounts: Option>, + /// Like accounts, but include transactions that are not + /// yet finalized. + pub accounts_proposed: Option>, + /// (Optional for Websocket; Required otherwise) URL where the server + /// sends a JSON-RPC callbacks for each event. Admin-only. + pub url: Option<&'a str>, + /// Username to provide for basic authentication at the callback URL. + pub url_username: Option<&'a str>, + /// Password to provide for basic authentication at the callback URL. + pub url_password: Option<&'a str>, + /// The request method. + // #[serde(skip_serializing)] + #[serde(default = "RequestMethod::subscribe")] + pub command: RequestMethod, +} + +impl<'a> Default for Subscribe<'a> { + fn default() -> Self { + Subscribe { + id: None, + books: None, + streams: None, + accounts: None, + accounts_proposed: None, + url: None, + url_username: None, + url_password: None, + command: RequestMethod::Subscribe, + } + } +} + +impl<'a> Model for Subscribe<'a> {} + +impl<'a> Subscribe<'a> { + fn new( + id: Option<&'a str>, + books: Option>>, + streams: Option>, + accounts: Option>, + accounts_proposed: Option>, + url: Option<&'a str>, + url_username: Option<&'a str>, + url_password: Option<&'a str>, + ) -> Self { + Self { + id, + books, + streams, + accounts, + accounts_proposed, + url, + url_username, + url_password, + command: RequestMethod::Subscribe, + } + } +} diff --git a/src/models/requests/transaction_entry.rs b/src/models/requests/transaction_entry.rs new file mode 100644 index 00000000..cf0cf33c --- /dev/null +++ b/src/models/requests/transaction_entry.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The transaction_entry method retrieves information on a +/// single transaction from a specific ledger version. +/// (The tx method, by contrast, searches all ledgers for +/// the specified transaction. We recommend using that +/// method instead.) +/// +/// See Transaction Entry: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct TransactionEntry<'a> { + /// Unique hash of the transaction you are looking up. + pub tx_hash: &'a str, + /// The unique request id. + pub id: Option<&'a str>, + /// A 20-byte hex string for the ledger version to use. + pub ledger_hash: Option<&'a str>, + /// The ledger index of the ledger to use, or a shortcut + /// string to choose a ledger automatically. + pub ledger_index: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::transaction_entry")] + pub command: RequestMethod, +} + +impl<'a> Default for TransactionEntry<'a> { + fn default() -> Self { + TransactionEntry { + tx_hash: "", + id: None, + ledger_hash: None, + ledger_index: None, + command: RequestMethod::TransactionEntry, + } + } +} + +impl<'a> Model for TransactionEntry<'a> {} + +impl<'a> TransactionEntry<'a> { + fn new( + tx_hash: &'a str, + id: Option<&'a str>, + ledger_hash: Option<&'a str>, + ledger_index: Option<&'a str>, + ) -> Self { + Self { + tx_hash, + id, + ledger_hash, + ledger_index, + command: RequestMethod::TransactionEntry, + } + } +} diff --git a/src/models/requests/tx.rs b/src/models/requests/tx.rs new file mode 100644 index 00000000..9d3056f9 --- /dev/null +++ b/src/models/requests/tx.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{requests::RequestMethod, Model}; + +/// The tx method retrieves information on a single transaction. +/// +/// See Tx: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Tx<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// If true, return transaction data and metadata as binary + /// serialized to hexadecimal strings. If false, return + /// transaction data and metadata as JSON. The default is false. + pub binary: Option, + /// Use this with max_ledger to specify a range of up to 1000 + /// ledger indexes, starting with this ledger (inclusive). If + /// the server cannot find the transaction, it confirms whether + /// it was able to search all the ledgers in this range. + pub min_ledger: Option, + /// Use this with min_ledger to specify a range of up to 1000 + /// ledger indexes, ending with this ledger (inclusive). If the + /// server cannot find the transaction, it confirms whether it + /// was able to search all the ledgers in the requested range. + pub max_ledger: Option, + /// The request method. + #[serde(default = "RequestMethod::tx")] + pub command: RequestMethod, +} + +impl<'a> Default for Tx<'a> { + fn default() -> Self { + Tx { + id: None, + binary: None, + min_ledger: None, + max_ledger: None, + command: RequestMethod::Tx, + } + } +} + +impl<'a> Model for Tx<'a> {} + +impl<'a> Tx<'a> { + fn new( + id: Option<&'a str>, + binary: Option, + min_ledger: Option, + max_ledger: Option, + ) -> Self { + Self { + id, + binary, + min_ledger, + max_ledger, + command: RequestMethod::Tx, + } + } +} diff --git a/src/models/requests/unsubscribe.rs b/src/models/requests/unsubscribe.rs new file mode 100644 index 00000000..d8ac3d99 --- /dev/null +++ b/src/models/requests/unsubscribe.rs @@ -0,0 +1,96 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{ + currency::Currency, + default_false, + requests::{RequestMethod, StreamParameter}, + Model, +}; + +/// Format for elements in the `books` array for Unsubscribe only. +/// +/// See Unsubscribe: +/// `` +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all(serialize = "PascalCase", deserialize = "snake_case"))] +pub struct UnsubscribeBook<'a> { + pub taker_gets: Currency<'a>, + pub taker_pays: Currency<'a>, + #[serde(default = "default_false")] + pub both: Option, +} + +/// The unsubscribe command tells the server to stop +/// sending messages for a particular subscription or set +/// of subscriptions. +/// +/// Note: WebSocket API only. +/// +/// See Unsubscribe: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Unsubscribe<'a> { + /// The unique request id. + pub id: Option<&'a str>, + /// Array of objects defining order books to unsubscribe + /// from, as explained below. + pub books: Option>>, + /// Array of string names of generic streams to unsubscribe + /// from, including ledger, server, transactions, + /// and transactions_proposed. + pub streams: Option>, + /// Array of unique account addresses to stop receiving updates + /// for, in the XRP Ledger's base58 format. (This only stops + /// those messages if you previously subscribed to those accounts + /// specifically. You cannot use this to filter accounts out of + /// the general transactions stream.) + pub accounts: Option>, + /// Like accounts, but for accounts_proposed subscriptions that + /// included not-yet-validated transactions. + pub accounts_proposed: Option>, + #[serde(skip_serializing)] + pub broken: Option<&'a str>, + /// The request method. + #[serde(default = "RequestMethod::unsubscribe")] + pub command: RequestMethod, +} + +impl<'a> Default for Unsubscribe<'a> { + fn default() -> Self { + Unsubscribe { + id: None, + books: None, + streams: None, + accounts: None, + accounts_proposed: None, + broken: None, + command: RequestMethod::Unsubscribe, + } + } +} + +impl<'a> Model for Unsubscribe<'a> {} + +impl<'a> Unsubscribe<'a> { + fn new( + id: Option<&'a str>, + books: Option>>, + streams: Option>, + accounts: Option>, + accounts_proposed: Option>, + broken: Option<&'a str>, + ) -> Self { + Self { + id, + books, + streams, + accounts, + accounts_proposed, + broken, + command: RequestMethod::Unsubscribe, + } + } +} diff --git a/src/models/response.rs b/src/models/response.rs new file mode 100644 index 00000000..a6ef23c5 --- /dev/null +++ b/src/models/response.rs @@ -0,0 +1 @@ +// TODO: Add `Response` model diff --git a/src/models/transactions/account_delete.rs b/src/models/transactions/account_delete.rs new file mode 100644 index 00000000..6623b401 --- /dev/null +++ b/src/models/transactions/account_delete.rs @@ -0,0 +1,211 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// An AccountDelete transaction deletes an account and any objects it +/// owns in the XRP Ledger, if possible, sending the account's remaining +/// XRP to a specified destination account. See Deletion of Accounts for +/// the requirements to delete an account. +/// +/// See AccountDelete: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct AccountDelete<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::account_set")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + // The custom fields for the AccountDelete model. + // + // See AccountDelete fields: + // `` + /// The address of an account to receive any leftover XRP after + /// deleting the sending account. Must be a funded account in + /// the ledger, and must not be the sending account. + pub destination: &'a str, + /// Arbitrary destination tag that identifies a hosted + /// recipient or other information for the recipient + /// of the deleted account's leftover XRP. + pub destination_tag: Option, +} + +impl<'a> Default for AccountDelete<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::AccountDelete, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + destination: Default::default(), + destination_tag: Default::default(), + } + } +} + +impl<'a> Model for AccountDelete<'a> {} + +impl<'a> Transaction for AccountDelete<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> AccountDelete<'a> { + fn new( + account: &'a str, + destination: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + destination_tag: Option, + ) -> Self { + Self { + transaction_type: TransactionType::AccountDelete, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + destination, + destination_tag, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = AccountDelete::new( + "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm", + "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", + Some("2000000".into()), + Some(2470665), + None, + None, + None, + None, + None, + None, + None, + None, + Some(13), + ); + let default_json = r#"{"TransactionType":"AccountDelete","Account":"rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm","Fee":"2000000","Sequence":2470665,"Destination":"rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe","DestinationTag":13}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = AccountDelete::new( + "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm", + "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", + Some("2000000".into()), + Some(2470665), + None, + None, + None, + None, + None, + None, + None, + None, + Some(13), + ); + let default_json = r#"{"TransactionType":"AccountDelete","Account":"rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm","Destination":"rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe","DestinationTag":13,"Fee":"2000000","Sequence":2470665}"#; + + let txn_as_obj: AccountDelete = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/account_set.rs b/src/models/transactions/account_set.rs new file mode 100644 index 00000000..8b37c14a --- /dev/null +++ b/src/models/transactions/account_set.rs @@ -0,0 +1,729 @@ +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use alloc::string::ToString; + +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLAccountSetException; +use crate::{ + _serde::txn_flags, + constants::{ + DISABLE_TICK_SIZE, MAX_DOMAIN_LENGTH, MAX_TICK_SIZE, MAX_TRANSFER_RATE, MIN_TICK_SIZE, + MIN_TRANSFER_RATE, SPECIAL_CASE_TRANFER_RATE, + }, + models::{ + model::Model, + transactions::{Flag, Memo, Signer, Transaction, TransactionType}, + }, + Err, +}; + +/// Transactions of the AccountSet type support additional values +/// in the Flags field. This enum represents those options. +/// +/// See AccountSet flags: +/// `` +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum AccountSetFlag { + /// Track the ID of this account's most recent transaction + /// Required for AccountTxnID + AsfAccountTxnID = 5, + /// Enable to allow another account to mint non-fungible tokens (NFTokens) + /// on this account's behalf. Specify the authorized account in the + /// NFTokenMinter field of the AccountRoot object. This is an experimental + /// field to enable behavior for NFToken support. + AsfAuthorizedNFTokenMinter = 10, + /// Enable rippling on this account's trust lines by default. + AsfDefaultRipple = 8, + /// Enable Deposit Authorization on this account. + /// (Added by the DepositAuth amendment.) + AsfDepositAuth = 9, + /// Disallow use of the master key pair. Can only be enabled if the + /// account has configured another way to sign transactions, such as + /// a Regular Key or a Signer List. + AsfDisableMaster = 4, + /// XRP should not be sent to this account. + /// (Enforced by client applications, not by rippled) + AsfDisallowXRP = 3, + /// Freeze all assets issued by this account. + AsfGlobalFreeze = 7, + /// Permanently give up the ability to freeze individual + /// trust lines or disable Global Freeze. This flag can never + /// be disabled after being enabled. + AsfNoFreeze = 6, + /// Require authorization for users to hold balances issued by + /// this address. Can only be enabled if the address has no + /// trust lines connected to it. + AsfRequireAuth = 2, + /// Require a destination tag to send transactions to this account. + AsfRequireDest = 1, +} + +/// An AccountSet transaction modifies the properties of an +/// account in the XRP Ledger. +/// +/// See AccountSet: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct AccountSet<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::account_set")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + // The custom fields for the AccountSet model. + // + // See AccountSet fields: + // `` + /// Unique identifier of a flag to disable for this account. + pub clear_flag: Option, + /// The domain that owns this account, as a string of hex + /// representing the ASCII for the domain in lowercase. + /// Cannot be more than 256 bytes in length. + pub domain: Option<&'a str>, + /// Hash of an email address to be used for generating an + /// avatar image. Conventionally, clients use Gravatar + /// to display this image. + pub email_hash: Option<&'a str>, + /// Public key for sending encrypted messages to this account. + /// To set the key, it must be exactly 33 bytes, with the + /// first byte indicating the key type: 0x02 or 0x03 for + /// secp256k1 keys, 0xED for Ed25519 keys. To remove the + /// key, use an empty value. + pub message_key: Option<&'a str>, + /// Sets an alternate account that is allowed to mint NFTokens + /// on this account's behalf using NFTokenMint's Issuer field. + /// This field is part of the experimental XLS-20 standard + /// for non-fungible tokens. + pub nftoken_minter: Option<&'a str>, + /// Flag to enable for this account. + pub set_flag: Option, + /// The fee to charge when users transfer this account's tokens, + /// represented as billionths of a unit. Cannot be more than + /// 2000000000 or less than 1000000000, except for the special + /// case 0 meaning no fee. + pub transfer_rate: Option, + /// Tick size to use for offers involving a currency issued by + /// this address. The exchange rates of those offers is rounded + /// to this many significant digits. Valid values are 3 to 15 + /// inclusive, or 0 to disable. + pub tick_size: Option, +} + +impl<'a> Default for AccountSet<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::AccountSet, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + clear_flag: Default::default(), + domain: Default::default(), + email_hash: Default::default(), + message_key: Default::default(), + nftoken_minter: Default::default(), + set_flag: Default::default(), + transfer_rate: Default::default(), + tick_size: Default::default(), + } + } +} + +impl<'a: 'static> Model for AccountSet<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_tick_size_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_transfer_rate_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_domain_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_clear_flag_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_nftoken_minter_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + }, + }, + }, + }, + } + } +} + +impl<'a> Transaction for AccountSet<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + let mut flags = &Vec::new(); + + if let Some(flag_set) = self.flags.as_ref() { + flags = flag_set; + } + + match flag { + Flag::AccountSet(account_set_flag) => match account_set_flag { + AccountSetFlag::AsfAccountTxnID => flags.contains(&AccountSetFlag::AsfAccountTxnID), + AccountSetFlag::AsfAuthorizedNFTokenMinter => { + flags.contains(&AccountSetFlag::AsfAuthorizedNFTokenMinter) + } + AccountSetFlag::AsfDefaultRipple => { + flags.contains(&AccountSetFlag::AsfDefaultRipple) + } + AccountSetFlag::AsfDepositAuth => flags.contains(&AccountSetFlag::AsfDepositAuth), + AccountSetFlag::AsfDisableMaster => { + flags.contains(&AccountSetFlag::AsfDisableMaster) + } + AccountSetFlag::AsfDisallowXRP => flags.contains(&AccountSetFlag::AsfDisallowXRP), + AccountSetFlag::AsfGlobalFreeze => flags.contains(&AccountSetFlag::AsfGlobalFreeze), + AccountSetFlag::AsfNoFreeze => flags.contains(&AccountSetFlag::AsfNoFreeze), + AccountSetFlag::AsfRequireAuth => flags.contains(&AccountSetFlag::AsfRequireAuth), + AccountSetFlag::AsfRequireDest => flags.contains(&AccountSetFlag::AsfRequireDest), + }, + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> AccountSetError for AccountSet<'a> { + fn _get_tick_size_error(&self) -> Result<(), XRPLAccountSetException> { + if let Some(tick_size) = self.tick_size { + if tick_size > MAX_TICK_SIZE { + Err(XRPLAccountSetException::ValueTooHigh { + field: "tick_size", + max: MAX_TICK_SIZE, + found: tick_size, + resource: "", + }) + } else if tick_size < MIN_TICK_SIZE && tick_size != DISABLE_TICK_SIZE { + Err(XRPLAccountSetException::ValueTooLow { + field: "tick_size", + min: MIN_TICK_SIZE, + found: tick_size, + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_transfer_rate_error(&self) -> Result<(), XRPLAccountSetException> { + if let Some(transfer_rate) = self.transfer_rate { + if transfer_rate > MAX_TRANSFER_RATE { + Err(XRPLAccountSetException::ValueTooHigh { + field: "transfer_rate", + max: MAX_TRANSFER_RATE, + found: transfer_rate, + resource: "", + }) + } else if transfer_rate < MIN_TRANSFER_RATE + && transfer_rate != SPECIAL_CASE_TRANFER_RATE + { + Err(XRPLAccountSetException::ValueTooLow { + field: "transfer_rate", + min: MIN_TRANSFER_RATE, + found: transfer_rate, + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_domain_error(&self) -> Result<(), XRPLAccountSetException> { + if let Some(domain) = self.domain { + if domain.to_lowercase().as_str() != domain { + Err(XRPLAccountSetException::InvalidValueFormat { + field: "domain", + found: domain, + format: "lowercase", + resource: "", + }) + } else if domain.len() > MAX_DOMAIN_LENGTH { + Err(XRPLAccountSetException::ValueTooLong { + field: "domain", + max: MAX_DOMAIN_LENGTH, + found: domain.len(), + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_clear_flag_error(&self) -> Result<(), XRPLAccountSetException> { + if self.clear_flag.is_some() && self.set_flag.is_some() && self.clear_flag == self.set_flag + { + Err(XRPLAccountSetException::SetAndUnsetSameFlag { + found: self.clear_flag.clone().unwrap(), + resource: "", + }) + } else { + Ok(()) + } + } + + fn _get_nftoken_minter_error(&self) -> Result<(), XRPLAccountSetException> { + if let Some(_nftoken_minter) = self.nftoken_minter { + if self.set_flag.is_none() { + if let Some(clear_flag) = &self.clear_flag { + match clear_flag { + AccountSetFlag::AsfAuthorizedNFTokenMinter => { + Err(XRPLAccountSetException::SetFieldWhenUnsetRequiredFlag { + field: "nftoken_minter", + flag: AccountSetFlag::AsfAuthorizedNFTokenMinter, + resource: "", + }) + } + _ => Ok(()), + } + } else { + Err(XRPLAccountSetException::FieldRequiresFlag { + field: "set_flag", + flag: AccountSetFlag::AsfAuthorizedNFTokenMinter, + resource: "", + }) + } + } else { + Ok(()) + } + } else if let Some(set_flag) = &self.set_flag { + match set_flag { + AccountSetFlag::AsfAuthorizedNFTokenMinter => { + Err(XRPLAccountSetException::FlagRequiresField { + flag: AccountSetFlag::AsfAuthorizedNFTokenMinter, + field: "nftoken_minter", + resource: "", + }) + } + _ => Ok(()), + } + } else { + Ok(()) + } + } +} + +impl<'a> AccountSet<'a> { + fn new( + account: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + flags: Option>, + memos: Option>>, + signers: Option>>, + clear_flag: Option, + domain: Option<&'a str>, + email_hash: Option<&'a str>, + message_key: Option<&'a str>, + set_flag: Option, + transfer_rate: Option, + tick_size: Option, + nftoken_minter: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::AccountSet, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags, + memos, + signers, + clear_flag, + domain, + email_hash, + message_key, + nftoken_minter, + set_flag, + transfer_rate, + tick_size, + } + } +} + +pub trait AccountSetError { + fn _get_tick_size_error(&self) -> Result<(), XRPLAccountSetException>; + fn _get_transfer_rate_error(&self) -> Result<(), XRPLAccountSetException>; + fn _get_domain_error(&self) -> Result<(), XRPLAccountSetException>; + fn _get_clear_flag_error(&self) -> Result<(), XRPLAccountSetException>; + fn _get_nftoken_minter_error(&self) -> Result<(), XRPLAccountSetException>; +} + +#[cfg(test)] +mod test_account_set_errors { + + use crate::models::Model; + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_tick_size_error() { + let mut account_set = AccountSet { + transaction_type: TransactionType::AccountSet, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + clear_flag: None, + domain: None, + email_hash: None, + message_key: None, + set_flag: None, + transfer_rate: None, + tick_size: None, + nftoken_minter: None, + }; + let tick_size_too_low = Some(2); + account_set.tick_size = tick_size_too_low; + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `tick_size` is defined below its minimum (min 3, found 2). For more information see: " + ); + + let tick_size_too_high = Some(16); + account_set.tick_size = tick_size_too_high; + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `tick_size` is defined above its maximum (max 15, found 16). For more information see: " + ); + } + + #[test] + fn test_transfer_rate_error() { + let mut account_set = AccountSet { + transaction_type: TransactionType::AccountSet, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + clear_flag: None, + domain: None, + email_hash: None, + message_key: None, + set_flag: None, + transfer_rate: None, + tick_size: None, + nftoken_minter: None, + }; + let tick_size_too_low = Some(999999999); + account_set.transfer_rate = tick_size_too_low; + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `transfer_rate` is defined below its minimum (min 1000000000, found 999999999). For more information see: " + ); + + let tick_size_too_high = Some(2000000001); + account_set.transfer_rate = tick_size_too_high; + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `transfer_rate` is defined above its maximum (max 2000000000, found 2000000001). For more information see: " + ); + } + + #[test] + fn test_domain_error() { + let mut account_set = AccountSet { + transaction_type: TransactionType::AccountSet, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + clear_flag: None, + domain: None, + email_hash: None, + message_key: None, + set_flag: None, + transfer_rate: None, + tick_size: None, + nftoken_minter: None, + }; + let domain_not_lowercase = Some("https://Example.com/"); + account_set.domain = domain_not_lowercase; + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `domain` does not have the correct format (expected lowercase, found https://Example.com/). For more information see: " + ); + + let domain_too_long = Some("https://example.com/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + account_set.domain = domain_too_long; + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `domain` exceeds its maximum length of characters (max 256, found 270). For more information see: " + ); + } + + #[test] + fn test_flag_error() { + let account_set = AccountSet { + transaction_type: TransactionType::AccountSet, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + clear_flag: Some(AccountSetFlag::AsfDisallowXRP), + domain: None, + email_hash: None, + message_key: None, + set_flag: Some(AccountSetFlag::AsfDisallowXRP), + transfer_rate: None, + tick_size: None, + nftoken_minter: None, + }; + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "A flag cannot be set and unset at the same time (found AsfDisallowXRP). For more information see: " + ); + } + + #[test] + fn test_asf_authorized_nftoken_minter_error() { + let mut account_set = AccountSet { + transaction_type: TransactionType::AccountSet, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + clear_flag: None, + domain: None, + email_hash: None, + message_key: None, + set_flag: None, + transfer_rate: None, + tick_size: None, + nftoken_minter: None, + }; + account_set.nftoken_minter = Some("rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK"); + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "For the field `set_flag` to be defined it is required to set the flag `AsfAuthorizedNFTokenMinter`. For more information see: " + ); + + account_set.nftoken_minter = None; + account_set.set_flag = Some(AccountSetFlag::AsfAuthorizedNFTokenMinter); + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "For the flag `AsfAuthorizedNFTokenMinter` to be set it is required to define the field `nftoken_minter`. For more information see: " + ); + + account_set.set_flag = None; + account_set.nftoken_minter = Some("rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK"); + account_set.clear_flag = Some(AccountSetFlag::AsfAuthorizedNFTokenMinter); + + assert_eq!( + account_set.validate().unwrap_err().to_string().as_str(), + "The field `nftoken_minter` cannot be defined if its required flag `AsfAuthorizedNFTokenMinter` is being unset. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = AccountSet::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Some("12".into()), + Some(5), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("6578616D706C652E636F6D"), + None, + Some("03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB"), + Some(AccountSetFlag::AsfAccountTxnID), + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"AccountSet","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"12","Sequence":5,"Domain":"6578616D706C652E636F6D","MessageKey":"03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB","SetFlag":5}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = AccountSet::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Some("12".into()), + Some(5), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("6578616D706C652E636F6D"), + None, + Some("03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB"), + Some(AccountSetFlag::AsfAccountTxnID), + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"AccountSet","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"12","Sequence":5,"Domain":"6578616D706C652E636F6D","MessageKey":"03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB","SetFlag":5}"#; + + let txn_as_obj: AccountSet = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/check_cancel.rs b/src/models/transactions/check_cancel.rs new file mode 100644 index 00000000..26a81743 --- /dev/null +++ b/src/models/transactions/check_cancel.rs @@ -0,0 +1,200 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Cancels an unredeemed Check, removing it from the ledger without +/// sending any money. The source or the destination of the check can +/// cancel a Check at any time using this transaction type. If the Check +/// has expired, any address can cancel it. +/// +/// See CheckCancel: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct CheckCancel<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::check_cancel")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + // The custom fields for the CheckCancel model. + // + // See CheckCancel fields: + // `` + #[serde(rename = "CheckID")] + pub check_id: &'a str, +} + +impl<'a> Default for CheckCancel<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::CheckCancel, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + check_id: Default::default(), + } + } +} + +impl<'a> Model for CheckCancel<'a> {} + +impl<'a> Transaction for CheckCancel<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> CheckCancel<'a> { + fn new( + account: &'a str, + check_id: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + ) -> Self { + Self { + transaction_type: TransactionType::CheckCancel, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + check_id, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = CheckCancel::new( + "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", + "49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0", + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"CheckCancel","Account":"rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo","Fee":"12","CheckID":"49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = CheckCancel::new( + "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", + "49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0", + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"CheckCancel","Account":"rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo","CheckID":"49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0","Fee":"12"}"#; + + let txn_as_obj: CheckCancel = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/check_cash.rs b/src/models/transactions/check_cash.rs new file mode 100644 index 00000000..58648d39 --- /dev/null +++ b/src/models/transactions/check_cash.rs @@ -0,0 +1,282 @@ +use crate::Err; +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLCheckCashException; +use crate::models::{ + amount::Amount, + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Cancels an unredeemed Check, removing it from the ledger without +/// sending any money. The source or the destination of the check can +/// cancel a Check at any time using this transaction type. If the Check +/// has expired, any address can cancel it. +/// +/// See CheckCash: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct CheckCash<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::check_cash")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the CheckCash model. + /// + /// See CheckCash fields: + /// `` + #[serde(rename = "CheckID")] + pub check_id: &'a str, + pub amount: Option>, + pub deliver_min: Option>, +} + +impl<'a> Default for CheckCash<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::CheckCash, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + check_id: Default::default(), + amount: Default::default(), + deliver_min: Default::default(), + } + } +} + +impl<'a: 'static> Model for CheckCash<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_amount_and_deliver_min_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + } + } +} + +impl<'a> Transaction for CheckCash<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> CheckCashError for CheckCash<'a> { + fn _get_amount_and_deliver_min_error(&self) -> Result<(), XRPLCheckCashException> { + if (self.amount.is_none() && self.deliver_min.is_none()) + || (self.amount.is_some() && self.deliver_min.is_some()) + { + Err(XRPLCheckCashException::DefineExactlyOneOf { + field1: "amount", + field2: "deliver_min", + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> CheckCash<'a> { + fn new( + account: &'a str, + check_id: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + amount: Option>, + deliver_min: Option>, + ) -> Self { + Self { + transaction_type: TransactionType::CheckCash, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + check_id, + amount, + deliver_min, + } + } +} + +pub trait CheckCashError { + fn _get_amount_and_deliver_min_error(&self) -> Result<(), XRPLCheckCashException>; +} + +#[cfg(test)] +mod test_check_cash_error { + use crate::models::Model; + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_amount_and_deliver_min_error() { + let check_cash = CheckCash { + transaction_type: TransactionType::CheckCash, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + check_id: "", + amount: None, + deliver_min: None, + }; + + assert_eq!( + check_cash.validate().unwrap_err().to_string().as_str(), + "The field `amount` can not be defined with `deliver_min`. Define exactly one of them. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use crate::models::amount::XRPAmount; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = CheckCash::new( + "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", + "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334", + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(Amount::XRPAmount(XRPAmount::from("100000000"))), + None, + ); + let default_json = r#"{"TransactionType":"CheckCash","Account":"rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy","Fee":"12","CheckID":"838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334","Amount":"100000000"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = CheckCash::new( + "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", + "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334", + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(Amount::XRPAmount(XRPAmount::from("100000000"))), + None, + ); + let default_json = r#"{"Account":"rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy","TransactionType":"CheckCash","Amount":"100000000","CheckID":"838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334","Fee":"12"}"#; + + let txn_as_obj: CheckCash = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/check_create.rs b/src/models/transactions/check_create.rs new file mode 100644 index 00000000..772bbd11 --- /dev/null +++ b/src/models/transactions/check_create.rs @@ -0,0 +1,225 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + amount::Amount, + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Create a Check object in the ledger, which is a deferred +/// payment that can be cashed by its intended destination. +/// +/// See CheckCreate: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct CheckCreate<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::check_create")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the CheckCreate model. + /// + /// See CheckCreate fields: + /// `` + pub destination: &'a str, + pub send_max: Amount<'a>, + pub destination_tag: Option, + pub expiration: Option, + #[serde(rename = "InvoiceID")] + pub invoice_id: Option<&'a str>, +} + +impl<'a> Default for CheckCreate<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::CheckCreate, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + destination: Default::default(), + send_max: Default::default(), + destination_tag: Default::default(), + expiration: Default::default(), + invoice_id: Default::default(), + } + } +} + +impl<'a> Model for CheckCreate<'a> {} + +impl<'a> Transaction for CheckCreate<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> CheckCreate<'a> { + fn new( + account: &'a str, + destination: &'a str, + send_max: Amount<'a>, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + destination_tag: Option, + expiration: Option, + invoice_id: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::CheckCreate, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + destination, + send_max, + destination_tag, + expiration, + invoice_id, + } + } +} + +#[cfg(test)] +mod test_serde { + use crate::models::amount::XRPAmount; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = CheckCreate::new( + "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", + "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", + Amount::XRPAmount(XRPAmount::from("100000000")), + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(1), + Some(570113521), + Some("6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B"), + ); + let default_json = r#"{"TransactionType":"CheckCreate","Account":"rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo","Fee":"12","Destination":"rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy","SendMax":"100000000","DestinationTag":1,"Expiration":570113521,"InvoiceID":"6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = CheckCreate::new( + "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", + "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", + Amount::XRPAmount(XRPAmount::from("100000000")), + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(1), + Some(570113521), + Some("6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B"), + ); + let default_json = r#"{"TransactionType":"CheckCreate","Account":"rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo","Destination":"rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy","SendMax":"100000000","Expiration":570113521,"InvoiceID":"6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B","DestinationTag":1,"Fee":"12"}"#; + + let txn_as_obj: CheckCreate = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/deposit_preauth.rs b/src/models/transactions/deposit_preauth.rs new file mode 100644 index 00000000..ceb42a5e --- /dev/null +++ b/src/models/transactions/deposit_preauth.rs @@ -0,0 +1,270 @@ +use crate::Err; +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLDepositPreauthException; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// A DepositPreauth transaction gives another account pre-approval +/// to deliver payments to the sender of this transaction. +/// +/// See DepositPreauth: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct DepositPreauth<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::deposit_preauth")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the DepositPreauth model. + /// + /// See DepositPreauth fields: + /// `` + pub authorize: Option<&'a str>, + pub unauthorize: Option<&'a str>, +} + +impl<'a> Default for DepositPreauth<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::DepositPreauth, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + authorize: Default::default(), + unauthorize: Default::default(), + } + } +} + +impl<'a: 'static> Model for DepositPreauth<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_authorize_and_unauthorize_error() { + Ok(_no_error) => Ok(()), + Err(error) => Err!(error), + } + } +} + +impl<'a> Transaction for DepositPreauth<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> DepositPreauthError for DepositPreauth<'a> { + fn _get_authorize_and_unauthorize_error(&self) -> Result<(), XRPLDepositPreauthException> { + if (self.authorize.is_none() && self.unauthorize.is_none()) + || (self.authorize.is_some() && self.unauthorize.is_some()) + { + Err(XRPLDepositPreauthException::DefineExactlyOneOf { + field1: "authorize", + field2: "unauthorize", + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> DepositPreauth<'a> { + fn new( + account: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + authorize: Option<&'a str>, + unauthorize: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::DepositPreauth, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + authorize, + unauthorize, + } + } +} + +pub trait DepositPreauthError { + fn _get_authorize_and_unauthorize_error(&self) -> Result<(), XRPLDepositPreauthException>; +} + +#[cfg(test)] +mod test_deposit_preauth_exception { + + use crate::models::Model; + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_authorize_and_unauthorize_error() { + let deposit_preauth = DepositPreauth { + transaction_type: TransactionType::DepositPreauth, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + authorize: None, + unauthorize: None, + }; + + assert_eq!( + deposit_preauth.validate().unwrap_err().to_string().as_str(), + "The field `authorize` can not be defined with `unauthorize`. Define exactly one of them. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = DepositPreauth::new( + "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8", + Some("10".into()), + Some(2), + None, + None, + None, + None, + None, + None, + None, + None, + Some("rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de"), + None, + ); + let default_json = r#"{"TransactionType":"DepositPreauth","Account":"rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8","Fee":"10","Sequence":2,"Authorize":"rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = DepositPreauth::new( + "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8", + Some("10".into()), + Some(2), + None, + None, + None, + None, + None, + None, + None, + None, + Some("rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de"), + None, + ); + let default_json = r#"{"TransactionType":"DepositPreauth","Account":"rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8","Authorize":"rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de","Fee":"10","Sequence":2}"#; + + let txn_as_obj: DepositPreauth = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/escrow_cancel.rs b/src/models/transactions/escrow_cancel.rs new file mode 100644 index 00000000..5bef7a5b --- /dev/null +++ b/src/models/transactions/escrow_cancel.rs @@ -0,0 +1,202 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Cancels an Escrow and returns escrowed XRP to the sender. +/// +/// See EscrowCancel: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct EscrowCancel<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::escrow_cancel")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the EscrowCancel model. + /// + /// See EscrowCancel fields: + /// `` + pub owner: &'a str, + pub offer_sequence: u32, +} + +impl<'a> Default for EscrowCancel<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::EscrowCancel, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + owner: Default::default(), + offer_sequence: Default::default(), + } + } +} + +impl<'a> Model for EscrowCancel<'a> {} + +impl<'a> Transaction for EscrowCancel<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> EscrowCancel<'a> { + fn new( + account: &'a str, + owner: &'a str, + offer_sequence: u32, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + ) -> Self { + Self { + transaction_type: TransactionType::EscrowCancel, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + owner, + offer_sequence, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = EscrowCancel::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 7, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"EscrowCancel","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Owner":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","OfferSequence":7}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = EscrowCancel::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 7, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"EscrowCancel","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Owner":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","OfferSequence":7}"#; + + let txn_as_obj: EscrowCancel = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/escrow_create.rs b/src/models/transactions/escrow_create.rs new file mode 100644 index 00000000..f5e3b102 --- /dev/null +++ b/src/models/transactions/escrow_create.rs @@ -0,0 +1,303 @@ +use crate::Err; +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLEscrowCreateException; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Creates an Escrow, which sequests XRP until the escrow process either finishes or is canceled. +/// +/// See EscrowCreate: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct EscrowCreate<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::escrow_create")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the EscrowCreate model. + /// + /// See EscrowCreate fields: + /// `` + pub amount: XRPAmount<'a>, + pub destination: &'a str, + pub destination_tag: Option, + pub cancel_after: Option, + pub finish_after: Option, + pub condition: Option<&'a str>, +} + +impl<'a> Default for EscrowCreate<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::EscrowCreate, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + amount: Default::default(), + destination: Default::default(), + destination_tag: Default::default(), + cancel_after: Default::default(), + finish_after: Default::default(), + condition: Default::default(), + } + } +} + +impl<'a: 'static> Model for EscrowCreate<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_finish_after_error() { + Ok(_) => Ok(()), + Err(error) => Err!(error), + } + } +} + +impl<'a> Transaction for EscrowCreate<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> EscrowCreateError for EscrowCreate<'a> { + fn _get_finish_after_error(&self) -> Result<(), XRPLEscrowCreateException> { + if let (Some(finish_after), Some(cancel_after)) = (self.finish_after, self.cancel_after) { + if finish_after >= cancel_after { + Err(XRPLEscrowCreateException::ValueBelowValue { + field1: "cancel_after", + field2: "finish_after", + field1_val: cancel_after, + field2_val: finish_after, + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} + +impl<'a> EscrowCreate<'a> { + fn new( + account: &'a str, + amount: XRPAmount<'a>, + destination: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + destination_tag: Option, + cancel_after: Option, + finish_after: Option, + condition: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::EscrowCreate, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + amount, + destination, + destination_tag, + cancel_after, + finish_after, + condition, + } + } +} + +pub trait EscrowCreateError { + fn _get_finish_after_error(&self) -> Result<(), XRPLEscrowCreateException>; +} + +#[cfg(test)] +mod test_escrow_create_errors { + use crate::models::Model; + + use crate::models::amount::XRPAmount; + + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_cancel_after_error() { + let escrow_create = EscrowCreate { + transaction_type: TransactionType::EscrowCreate, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + amount: XRPAmount::from("100000000"), + destination: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + destination_tag: None, + cancel_after: Some(13298498), + finish_after: Some(14359039), + condition: None, + }; + + assert_eq!( + escrow_create.validate().unwrap_err().to_string().as_str(), + "The value of the field `cancel_after` is not allowed to be below the value of the field `finish_after` (max 14359039, found 13298498). For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = EscrowCreate::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + XRPAmount::from("10000"), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + None, + None, + None, + None, + None, + Some(11747), + None, + None, + None, + None, + Some(23480), + Some(533257958), + Some(533171558), + Some("A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100"), + ); + let default_json = r#"{"TransactionType":"EscrowCreate","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","SourceTag":11747,"Amount":"10000","Destination":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","DestinationTag":23480,"CancelAfter":533257958,"FinishAfter":533171558,"Condition":"A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = EscrowCreate::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + XRPAmount::from("10000"), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + None, + None, + None, + None, + None, + Some(11747), + None, + None, + None, + None, + Some(23480), + Some(533257958), + Some(533171558), + Some("A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100"), + ); + let default_json = r#"{"TransactionType":"EscrowCreate","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Amount":"10000","Destination":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","CancelAfter":533257958,"FinishAfter":533171558,"Condition":"A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100","DestinationTag":23480,"SourceTag":11747}"#; + + let txn_as_obj: EscrowCreate = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/escrow_finish.rs b/src/models/transactions/escrow_finish.rs new file mode 100644 index 00000000..7f5f75e2 --- /dev/null +++ b/src/models/transactions/escrow_finish.rs @@ -0,0 +1,285 @@ +use crate::Err; +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::transactions::XRPLEscrowFinishException; +use crate::models::{ + amount::XRPAmount, + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Finishes an Escrow and delivers XRP from a held payment to the recipient. +/// +/// See EscrowFinish: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct EscrowFinish<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::escrow_finish")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the EscrowFinish model. + /// + /// See EscrowFinish fields: + /// `` + pub owner: &'a str, + pub offer_sequence: u32, + pub condition: Option<&'a str>, + pub fulfillment: Option<&'a str>, +} + +impl<'a> Default for EscrowFinish<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::EscrowFinish, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + owner: Default::default(), + offer_sequence: Default::default(), + condition: Default::default(), + fulfillment: Default::default(), + } + } +} + +impl<'a: 'static> Model for EscrowFinish<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_condition_and_fulfillment_error() { + Ok(_) => Ok(()), + Err(error) => Err!(error), + } + } +} + +impl<'a> Transaction for EscrowFinish<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> EscrowFinishError for EscrowFinish<'a> { + fn _get_condition_and_fulfillment_error(&self) -> Result<(), XRPLEscrowFinishException> { + if (self.condition.is_some() && self.fulfillment.is_none()) + || (self.condition.is_none() && self.condition.is_some()) + { + Err(XRPLEscrowFinishException::FieldRequiresField { + field1: "condition", + field2: "fulfillment", + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> EscrowFinish<'a> { + fn new( + account: &'a str, + owner: &'a str, + offer_sequence: u32, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + condition: Option<&'a str>, + fulfillment: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::EscrowFinish, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + owner, + offer_sequence, + condition, + fulfillment, + } + } +} + +pub trait EscrowFinishError { + fn _get_condition_and_fulfillment_error(&self) -> Result<(), XRPLEscrowFinishException>; +} + +#[cfg(test)] +mod test_escrow_finish_errors { + + use crate::models::Model; + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_condition_and_fulfillment_error() { + let escrow_finish = EscrowFinish { + transaction_type: TransactionType::EscrowCancel, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + owner: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + offer_sequence: 10, + condition: Some( + "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", + ), + fulfillment: None, + }; + + assert_eq!( + escrow_finish.validate().unwrap_err().to_string().as_str(), + "For the field `condition` to be defined it is required to also define the field `fulfillment`. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = EscrowFinish::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 7, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100"), + Some("A0028000"), + ); + let default_json = r#"{"TransactionType":"EscrowFinish","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Owner":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","OfferSequence":7,"Condition":"A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100","Fulfillment":"A0028000"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = EscrowFinish::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 7, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100"), + Some("A0028000"), + ); + let default_json = r#"{"TransactionType":"EscrowFinish","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Owner":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","OfferSequence":7,"Condition":"A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100","Fulfillment":"A0028000"}"#; + + let txn_as_obj: EscrowFinish = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/exceptions.rs b/src/models/transactions/exceptions.rs new file mode 100644 index 00000000..8fef9623 --- /dev/null +++ b/src/models/transactions/exceptions.rs @@ -0,0 +1,328 @@ +use crate::models::transactions::{AccountSetFlag, PaymentFlag}; +use strum_macros::Display; +use thiserror_no_std::Error; + +#[derive(Debug, Clone, PartialEq, Eq, Display)] +pub enum XRPLTransactionException<'a> { + XRPLAccountSetError(XRPLAccountSetException<'a>), + XRPLCheckCashError(XRPLCheckCashException<'a>), + XRPLDepositPreauthError(XRPLDepositPreauthException<'a>), + XRPLEscrowCreateError(XRPLEscrowCreateException<'a>), + XRPLEscrowFinishError(XRPLEscrowFinishException<'a>), + XRPLNFTokenAcceptOfferError(XRPLNFTokenAcceptOfferException<'a>), + XRPLNFTokenCancelOfferError(XRPLNFTokenCancelOfferException<'a>), + XRPLNFTokenCreateOfferError(XRPLNFTokenCreateOfferException<'a>), + XRPLNFTokenMintError(XRPLNFTokenMintException<'a>), + XRPLPaymentError(XRPLPaymentException<'a>), + XRPLSignerListSetError(XRPLSignerListSetException<'a>), +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLTransactionException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLAccountSetException<'a> { + /// A fields value exceeds its maximum value. + #[error("The value of the field `{field:?}` is defined above its maximum (max {max:?}, found {found:?}). For more information see: {resource:?}")] + ValueTooHigh { + field: &'a str, + max: u32, + found: u32, + resource: &'a str, + }, + /// A fields value exceeds its minimum value. + #[error("The value of the field `{field:?}` is defined below its minimum (min {min:?}, found {found:?}). For more information see: {resource:?}")] + ValueTooLow { + field: &'a str, + min: u32, + found: u32, + resource: &'a str, + }, + /// A fields value exceeds its maximum character length. + #[error("The value of the field `{field:?}` exceeds its maximum length of characters (max {max:?}, found {found:?}). For more information see: {resource:?}")] + ValueTooLong { + field: &'a str, + max: usize, + found: usize, + resource: &'a str, + }, + /// A fields value doesn't match its required format. + #[error("The value of the field `{field:?}` does not have the correct format (expected {format:?}, found {found:?}). For more information see: {resource:?}")] + InvalidValueFormat { + field: &'a str, + format: &'a str, + found: &'a str, + resource: &'a str, + }, + /// A field can only be defined if a transaction flag is set. + #[error("For the field `{field:?}` to be defined it is required to set the flag `{flag:?}`. For more information see: {resource:?}")] + FieldRequiresFlag { + field: &'a str, + flag: AccountSetFlag, + resource: &'a str, + }, + /// An account set flag can only be set if a field is defined. + #[error("For the flag `{flag:?}` to be set it is required to define the field `{field:?}`. For more information see: {resource:?}")] + FlagRequiresField { + flag: AccountSetFlag, + field: &'a str, + resource: &'a str, + }, + /// Am account set flag can not be set and unset at the same time. + #[error("A flag cannot be set and unset at the same time (found {found:?}). For more information see: {resource:?}")] + SetAndUnsetSameFlag { + found: AccountSetFlag, + resource: &'a str, + }, + /// A field was defined and an account set flag that is required for that field was unset. + #[error("The field `{field:?}` cannot be defined if its required flag `{flag:?}` is being unset. For more information see: {resource:?}")] + SetFieldWhenUnsetRequiredFlag { + field: &'a str, + flag: AccountSetFlag, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLAccountSetException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLCheckCashException<'a> { + /// A field cannot be defined with other fields. + #[error("The field `{field1:?}` can not be defined with `{field2:?}`. Define exactly one of them. For more information see: {resource:?}")] + DefineExactlyOneOf { + field1: &'a str, + field2: &'a str, + resource: &'a str, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLDepositPreauthException<'a> { + /// A field cannot be defined with other fields. + #[error("The field `{field1:?}` can not be defined with `{field2:?}`. Define exactly one of them. For more information see: {resource:?}")] + DefineExactlyOneOf { + field1: &'a str, + field2: &'a str, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLCheckCashException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLEscrowCreateException<'a> { + /// A fields value cannot be below another fields value. + #[error("The value of the field `{field1:?}` is not allowed to be below the value of the field `{field2:?}` (max {field2_val:?}, found {field1_val:?}). For more information see: {resource:?}")] + ValueBelowValue { + field1: &'a str, + field2: &'a str, + field1_val: u32, + field2_val: u32, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLEscrowCreateException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLEscrowFinishException<'a> { + /// For a field to be defined it also needs another field to be defined. + #[error("For the field `{field1:?}` to be defined it is required to also define the field `{field2:?}`. For more information see: {resource:?}")] + FieldRequiresField { + field1: &'a str, + field2: &'a str, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLEscrowFinishException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLNFTokenAcceptOfferException<'a> { + /// Define at least one of the fields. + #[error("Define at least one of the fields `{field1:?}` and `{field2:?}`. For more information see: {resource:?}")] + DefineOneOf { + field1: &'a str, + field2: &'a str, + resource: &'a str, + }, + /// The value can not be zero. + #[error("The value of the field `{field:?}` is not allowed to be zero. For more information see: {resource:?}")] + ValueZero { field: &'a str, resource: &'a str }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLNFTokenAcceptOfferException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLNFTokenCancelOfferException<'a> { + /// A collection was defined to be empty. + #[error("The value of the field `{field:?}` is not allowed to be empty (type `{r#type:?}`). If the field is optional, define it to be `None`. For more information see: {resource:?}")] + CollectionEmpty { + field: &'a str, + r#type: &'a str, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLNFTokenCancelOfferException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLNFTokenCreateOfferException<'a> { + /// The value can not be zero. + #[error("The value of the field `{field:?}` is not allowed to be zero. For more information see: {resource:?}")] + ValueZero { field: &'a str, resource: &'a str }, + /// A fields value is not allowed to be the same as another fields value. + #[error("The value of the field `{field1:?}` is not allowed to be the same as the value of the field `{field2:?}`. For more information see: {resource:?}")] + ValueEqualsValue { + field1: &'a str, + field2: &'a str, + resource: &'a str, + }, + /// An optional value must be defined in a certain context. + #[error("The optional field `{field:?}` is required to be defined for {context:?}. For more information see: {resource:?}")] + OptionRequired { + field: &'a str, + context: &'a str, + resource: &'a str, + }, + /// An optional value is not allowed to be defined in a certain context. + #[error("The optional field `{field:?}` is not allowed to be defined for {context:?}. For more information see: {resource:?}")] + IllegalOption { + field: &'a str, + context: &'a str, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLNFTokenCreateOfferException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLNFTokenMintException<'a> { + /// A fields value is not allowed to be the same as another fields value. + #[error("The value of the field `{field1:?}` is not allowed to be the same as the value of the field `{field2:?}`. For more information see: {resource:?}")] + ValueEqualsValue { + field1: &'a str, + field2: &'a str, + resource: &'a str, + }, + /// A fields value exceeds its maximum value. + #[error("The field `{field:?}` exceeds its maximum value (max {max:?}, found {found:?}). For more information see: {resource:?}")] + ValueTooHigh { + field: &'a str, + max: u32, + found: u32, + resource: &'a str, + }, + /// A fields value exceeds its maximum character length. + #[error("The value of the field `{field:?}` exceeds its maximum length of characters (max {max:?}, found {found:?}). For more information see: {resource:?}")] + ValueTooLong { + field: &'a str, + max: usize, + found: usize, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLNFTokenMintException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLPaymentException<'a> { + /// An optional value must be defined in a certain context. + #[error("The optional field `{field:?}` is required to be defined for {context:?}. For more information see: {resource:?}")] + OptionRequired { + field: &'a str, + context: &'a str, + resource: &'a str, + }, + /// An optional value is not allowed to be defined in a certain context. + #[error("The optional field `{field:?}` is not allowed to be defined for {context:?}.For more information see: {resource:?}")] + IllegalOption { + field: &'a str, + context: &'a str, + resource: &'a str, + }, + /// A fields value is not allowed to be the same as another fields value, in a certain context. + #[error("The value of the field `{field1:?}` is not allowed to be the same as the value of the field `{field2:?}`, for {context:?}. For more information see: {resource:?}")] + ValueEqualsValueInContext { + field1: &'a str, + field2: &'a str, + context: &'a str, + resource: &'a str, + }, + /// An account set flag can only be set if a field is defined. + #[error("For the flag `{flag:?}` to be set it is required to define the field `{field:?}`. For more information see: {resource:?}")] + FlagRequiresField { + flag: PaymentFlag, + field: &'a str, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLPaymentException<'a> {} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum XRPLSignerListSetException<'a> { + /// A field was defined that another field definition would delete. + #[error("The value of the field `{field1:?}` can not be defined with the field `{field2:?}` because it would cause the deletion of `{field1:?}`. For more information see: {resource:?}")] + ValueCausesValueDeletion { + field1: &'a str, + field2: &'a str, + resource: &'a str, + }, + /// A field is expected to have a certain value to be deleted. + #[error("The field `{field:?}` has the wrong value to be deleted (expected {expected:?}, found {found:?}). For more information see: {resource:?}")] + InvalidValueForValueDeletion { + field: &'a str, + expected: u32, + found: u32, + resource: &'a str, + }, + /// A collection has too few items in it. + #[error("The value of the field `{field:?}` has too few items in it (min {min:?}, found {found:?}). For more information see: {resource:?}")] + CollectionTooFewItems { + field: &'a str, + min: usize, + found: usize, + resource: &'a str, + }, + /// A collection has too many items in it. + #[error("The value of the field `{field:?}` has too many items in it (max {max:?}, found {found:?}). For more information see: {resource:?}")] + CollectionTooManyItems { + field: &'a str, + max: usize, + found: usize, + resource: &'a str, + }, + /// A collection is not allowed to have duplicates in it. + #[error("The value of the field `{field:?}` has a duplicate in it (found {found:?}). For more information see: {resource:?}")] + CollectionItemDuplicate { + field: &'a str, + found: &'a str, + resource: &'a str, + }, + /// A collection contains an invalid value. + #[error("The field `{field:?}` contains an invalid value (found {found:?}). For more information see: {resource:?}")] + CollectionInvalidItem { + field: &'a str, + found: &'a str, + resource: &'a str, + }, + #[error("The field `signer_quorum` must be below or equal to the sum of `signer_weight` in `signer_entries`. For more information see: {resource:?}")] + SignerQuorumExceedsSignerWeight { + max: u32, + found: u32, + resource: &'a str, + }, +} + +#[cfg(feature = "std")] +impl<'a> alloc::error::Error for XRPLSignerListSetException<'a> {} diff --git a/src/models/transactions/mod.rs b/src/models/transactions/mod.rs new file mode 100644 index 00000000..1add1bb1 --- /dev/null +++ b/src/models/transactions/mod.rs @@ -0,0 +1,235 @@ +pub mod account_delete; +pub mod account_set; +pub mod check_cancel; +pub mod check_cash; +pub mod check_create; +pub mod deposit_preauth; +pub mod escrow_cancel; +pub mod escrow_create; +pub mod escrow_finish; +pub mod exceptions; +pub mod nftoken_accept_offer; +pub mod nftoken_burn; +pub mod nftoken_cancel_offer; +pub mod nftoken_create_offer; +pub mod nftoken_mint; +pub mod offer_cancel; +pub mod offer_create; +pub mod payment; +pub mod payment_channel_claim; +pub mod payment_channel_create; +pub mod payment_channel_fund; +pub mod pseudo_transactions; +pub mod set_regular_key; +pub mod signer_list_set; +pub mod ticket_create; +pub mod trust_set; + +pub use account_delete::*; +pub use account_set::*; +pub use check_cancel::*; +pub use check_cash::*; +pub use check_create::*; +pub use deposit_preauth::*; +pub use escrow_cancel::*; +pub use escrow_create::*; +pub use escrow_finish::*; +pub use exceptions::*; +pub use nftoken_accept_offer::*; +pub use nftoken_burn::*; +pub use nftoken_cancel_offer::*; +pub use nftoken_create_offer::*; +pub use nftoken_mint::*; +pub use offer_cancel::*; +pub use offer_create::*; +pub use payment::*; +pub use payment_channel_claim::*; +pub use payment_channel_create::*; +pub use payment_channel_fund::*; +pub use pseudo_transactions::*; +pub use set_regular_key::*; +pub use signer_list_set::*; +pub use ticket_create::*; +pub use trust_set::*; + +use crate::serde_with_tag; +use derive_new::new; +use serde::ser::SerializeMap; +use serde::{Deserialize, Serialize}; +use strum_macros::{AsRefStr, Display}; + +/// Enum containing the different Transaction types. +#[derive(Debug, Clone, Serialize, Deserialize, Display, PartialEq, Eq)] +pub enum TransactionType { + AccountDelete, + AccountSet, + CheckCancel, + CheckCash, + CheckCreate, + DepositPreauth, + EscrowCancel, + EscrowCreate, + EscrowFinish, + NFTokenAcceptOffer, + NFTokenBurn, + NFTokenCancelOffer, + NFTokenCreateOffer, + NFTokenMint, + OfferCancel, + OfferCreate, + Payment, + PaymentChannelClaim, + PaymentChannelCreate, + PaymentChannelFund, + SetRegularKey, + SignerListSet, + TicketCreate, + TrustSet, + + // Psuedo-Transaction types, + EnableAmendment, + SetFee, + UNLModify, +} + +/// For use with serde defaults. +/// TODO Find a better way +impl TransactionType { + fn account_delete() -> Self { + TransactionType::AccountDelete + } + fn account_set() -> Self { + TransactionType::AccountSet + } + fn check_cancel() -> Self { + TransactionType::CheckCancel + } + fn check_cash() -> Self { + TransactionType::CheckCash + } + fn check_create() -> Self { + TransactionType::CheckCreate + } + fn deposit_preauth() -> Self { + TransactionType::DepositPreauth + } + fn escrow_cancel() -> Self { + TransactionType::EscrowCancel + } + fn escrow_create() -> Self { + TransactionType::EscrowCreate + } + fn escrow_finish() -> Self { + TransactionType::EscrowFinish + } + fn nftoken_accept_offer() -> Self { + TransactionType::NFTokenAcceptOffer + } + fn nftoken_burn() -> Self { + TransactionType::NFTokenBurn + } + fn nftoken_cancel_offer() -> Self { + TransactionType::NFTokenCancelOffer + } + fn nftoken_create_offer() -> Self { + TransactionType::NFTokenCreateOffer + } + fn nftoken_mint() -> Self { + TransactionType::NFTokenMint + } + fn offer_cancel() -> Self { + TransactionType::OfferCancel + } + fn offer_create() -> Self { + TransactionType::OfferCreate + } + fn payment() -> Self { + TransactionType::Payment + } + fn payment_channel_claim() -> Self { + TransactionType::PaymentChannelClaim + } + fn payment_channel_create() -> Self { + TransactionType::PaymentChannelCreate + } + fn payment_channel_fund() -> Self { + TransactionType::PaymentChannelFund + } + fn set_regular_key() -> Self { + TransactionType::SetRegularKey + } + fn signer_list_set() -> Self { + TransactionType::SignerListSet + } + fn ticket_create() -> Self { + TransactionType::TicketCreate + } + fn trust_set() -> Self { + TransactionType::TrustSet + } + fn enable_amendment() -> Self { + TransactionType::EnableAmendment + } + fn set_fee() -> Self { + TransactionType::SetFee + } + fn unl_modify() -> Self { + TransactionType::UNLModify + } +} + +serde_with_tag! { +/// An arbitrary piece of data attached to a transaction. A +/// transaction can have multiple Memo objects as an array +/// in the Memos field. +/// +/// Must contain one or more of `memo_data`, `memo_format`, +/// and `memo_type`. +/// +/// See Memos Field: +/// `` +// `#[derive(Serialize)]` is defined in the macro +#[derive(Debug, PartialEq, Eq, Default, Clone, new)] +pub struct Memo<'a> { + pub memo_data: Option<&'a str>, + pub memo_format: Option<&'a str>, + pub memo_type: Option<&'a str>, +} +} + +/// One Signer in a multi-signature. A multi-signed transaction +/// can have an array of up to 8 Signers, each contributing a +/// signature, in the Signers field. +/// +/// See Signers Field: +/// `` +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone, new)] +#[serde(rename_all = "PascalCase")] +pub struct Signer<'a> { + account: &'a str, + txn_signature: &'a str, + signing_pub_key: &'a str, +} + +/// Standard functions for transactions. +pub trait Transaction { + // TODO: use generic type + fn has_flag(&self, flag: &Flag) -> bool { + let _txn_flag = flag; + false + } + + fn get_transaction_type(&self) -> TransactionType; +} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Display, AsRefStr)] +pub enum Flag { + AccountSet(AccountSetFlag), + NFTokenCreateOffer(NFTokenCreateOfferFlag), + NFTokenMint(NFTokenMintFlag), + OfferCreate(OfferCreateFlag), + Payment(PaymentFlag), + PaymentChannelClaim(PaymentChannelClaimFlag), + TrustSet(TrustSetFlag), + EnableAmendment(EnableAmendmentFlag), +} diff --git a/src/models/transactions/nftoken_accept_offer.rs b/src/models/transactions/nftoken_accept_offer.rs new file mode 100644 index 00000000..0b8f785c --- /dev/null +++ b/src/models/transactions/nftoken_accept_offer.rs @@ -0,0 +1,350 @@ +use crate::Err; +use alloc::vec::Vec; +use anyhow::Result; +use core::convert::TryInto; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::amount::exceptions::XRPLAmountException; +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLNFTokenAcceptOfferException; +use crate::models::{ + amount::Amount, + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Accept offers to buy or sell an NFToken. +/// +/// See NFTokenAcceptOffer: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NFTokenAcceptOffer<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::nftoken_accept_offer")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the NFTokenAcceptOffer model. + /// + /// See NFTokenAcceptOffer fields: + /// `` + #[serde(rename = "NFTokenSellOffer")] + pub nftoken_sell_offer: Option<&'a str>, + #[serde(rename = "NFTokenBuyOffer")] + pub nftoken_buy_offer: Option<&'a str>, + #[serde(rename = "NFTokenBrokerFee")] + pub nftoken_broker_fee: Option>, +} + +impl<'a> Default for NFTokenAcceptOffer<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::NFTokenAcceptOffer, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + nftoken_sell_offer: Default::default(), + nftoken_buy_offer: Default::default(), + nftoken_broker_fee: Default::default(), + } + } +} + +impl<'a: 'static> Model for NFTokenAcceptOffer<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_brokered_mode_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_nftoken_broker_fee_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + }, + } + } +} + +impl<'a> Transaction for NFTokenAcceptOffer<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> NFTokenAcceptOfferError for NFTokenAcceptOffer<'a> { + fn _get_brokered_mode_error(&self) -> Result<(), XRPLNFTokenAcceptOfferException> { + if self.nftoken_broker_fee.is_some() + && self.nftoken_sell_offer.is_none() + && self.nftoken_buy_offer.is_none() + { + Err(XRPLNFTokenAcceptOfferException::DefineOneOf { + field1: "nftoken_sell_offer", + field2: "nftoken_buy_offer", + resource: "", + }) + } else { + Ok(()) + } + } + fn _get_nftoken_broker_fee_error(&self) -> Result<()> { + if let Some(nftoken_broker_fee) = &self.nftoken_broker_fee { + let nftoken_broker_fee_decimal: Result = + nftoken_broker_fee.clone().try_into(); + match nftoken_broker_fee_decimal { + Ok(nftoken_broker_fee_dec) => { + if nftoken_broker_fee_dec.is_zero() { + Err!(XRPLNFTokenAcceptOfferException::ValueZero { + field: "nftoken_broker_fee", + resource: "", + }) + } else { + Ok(()) + } + } + Err(decimal_error) => Err!(decimal_error), + } + } else { + Ok(()) + } + } +} + +impl<'a> NFTokenAcceptOffer<'a> { + fn new( + account: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + nftoken_sell_offer: Option<&'a str>, + nftoken_buy_offer: Option<&'a str>, + nftoken_broker_fee: Option>, + ) -> Self { + Self { + transaction_type: TransactionType::NFTokenAcceptOffer, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + nftoken_sell_offer, + nftoken_buy_offer, + nftoken_broker_fee, + } + } +} + +pub trait NFTokenAcceptOfferError { + fn _get_brokered_mode_error(&self) -> Result<(), XRPLNFTokenAcceptOfferException>; + fn _get_nftoken_broker_fee_error(&self) -> Result<()>; +} + +#[cfg(test)] +mod test_nftoken_accept_offer_error { + + use alloc::string::ToString; + + use crate::models::{ + amount::{Amount, XRPAmount}, + Model, + }; + + use super::*; + + #[test] + fn test_brokered_mode_error() { + let nftoken_accept_offer = NFTokenAcceptOffer { + transaction_type: TransactionType::NFTokenAcceptOffer, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_sell_offer: None, + nftoken_buy_offer: None, + nftoken_broker_fee: Some(Amount::XRPAmount(XRPAmount::from("100"))), + }; + + assert_eq!( + nftoken_accept_offer.validate().unwrap_err().to_string().as_str(), + "Define at least one of the fields `nftoken_sell_offer` and `nftoken_buy_offer`. For more information see: " + ); + } + + #[test] + fn test_broker_fee_error() { + let nftoken_accept_offer = NFTokenAcceptOffer { + transaction_type: TransactionType::NFTokenAcceptOffer, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_sell_offer: Some(""), + nftoken_buy_offer: None, + nftoken_broker_fee: Some(Amount::XRPAmount(XRPAmount::from("0"))), + }; + + assert_eq!( + nftoken_accept_offer.validate().unwrap_err().to_string().as_str(), + "The value of the field `nftoken_broker_fee` is not allowed to be zero. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use alloc::vec; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = NFTokenAcceptOffer::new( + "r9spUPhPBfB6kQeF6vPhwmtFwRhBh2JUCG", + Some("12".into()), + Some(68549302), + Some(75447550), + None, + None, + None, + None, + None, + Some(vec![Memo::new( + Some("61356534373538372D633134322D346663382D616466362D393666383562356435386437"), + None, + None, + )]), + None, + Some("68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"), + None, + None, + ); + let default_json = r#"{"TransactionType":"NFTokenAcceptOffer","Account":"r9spUPhPBfB6kQeF6vPhwmtFwRhBh2JUCG","Fee":"12","Sequence":68549302,"LastLedgerSequence":75447550,"Memos":[{"Memo":{"MemoData":"61356534373538372D633134322D346663382D616466362D393666383562356435386437","MemoFormat":null,"MemoType":null}}],"NFTokenSellOffer":"68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = NFTokenAcceptOffer::new( + "r9spUPhPBfB6kQeF6vPhwmtFwRhBh2JUCG", + Some("12".into()), + Some(68549302), + Some(75447550), + None, + None, + None, + None, + None, + Some(vec![Memo::new( + Some("61356534373538372D633134322D346663382D616466362D393666383562356435386437"), + None, + None, + )]), + None, + Some("68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77"), + None, + None, + ); + let default_json = r#"{"TransactionType":"NFTokenAcceptOffer","Account":"r9spUPhPBfB6kQeF6vPhwmtFwRhBh2JUCG","Fee":"12","LastLedgerSequence":75447550,"Memos":[{"Memo":{"MemoData":"61356534373538372D633134322D346663382D616466362D393666383562356435386437","MemoFormat":null,"MemoType":null}}],"NFTokenSellOffer":"68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C3B77","Sequence":68549302}"#; + + let txn_as_obj: NFTokenAcceptOffer = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/nftoken_burn.rs b/src/models/transactions/nftoken_burn.rs new file mode 100644 index 00000000..457d2382 --- /dev/null +++ b/src/models/transactions/nftoken_burn.rs @@ -0,0 +1,204 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Removes a NFToken object from the NFTokenPage in which it is being held, +/// effectively removing the token from the ledger (burning it). +/// +/// See NFTokenBurn: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NFTokenBurn<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::nftoken_burn")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the NFTokenBurn model. + /// + /// See NFTokenBurn fields: + /// `` + #[serde(rename = "NFTokenID")] + pub nftoken_id: &'a str, + pub owner: Option<&'a str>, +} + +impl<'a> Default for NFTokenBurn<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::NFTokenBurn, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + nftoken_id: Default::default(), + owner: Default::default(), + } + } +} + +impl<'a> Model for NFTokenBurn<'a> {} + +impl<'a> Transaction for NFTokenBurn<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> NFTokenBurn<'a> { + fn new( + account: &'a str, + nftoken_id: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + owner: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::NFTokenBurn, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + nftoken_id, + owner, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = NFTokenBurn::new( + "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", + "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65", + Some("10".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), + ); + let default_json = r#"{"TransactionType":"NFTokenBurn","Account":"rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2","Fee":"10","NFTokenID":"000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65","Owner":"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = NFTokenBurn::new( + "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", + "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65", + Some("10".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), + ); + let default_json = r#"{"TransactionType":"NFTokenBurn","Account":"rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2","Owner":"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B","Fee":"10","NFTokenID":"000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"}"#; + + let txn_as_obj: NFTokenBurn = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/nftoken_cancel_offer.rs b/src/models/transactions/nftoken_cancel_offer.rs new file mode 100644 index 00000000..5c850ccd --- /dev/null +++ b/src/models/transactions/nftoken_cancel_offer.rs @@ -0,0 +1,266 @@ +use crate::Err; +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLNFTokenCancelOfferException; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Cancels existing token offers created using NFTokenCreateOffer. +/// +/// See NFTokenCancelOffer: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NFTokenCancelOffer<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::nftoken_cancel_offer")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the NFTokenCancelOffer model. + /// + /// See NFTokenCancelOffer fields: + /// `` + /// Lifetime issue + #[serde(borrow)] + #[serde(rename = "NFTokenOffers")] + pub nftoken_offers: Vec<&'a str>, +} + +impl<'a> Default for NFTokenCancelOffer<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::NFTokenCancelOffer, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + nftoken_offers: Default::default(), + } + } +} + +impl<'a: 'static> Model for NFTokenCancelOffer<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_nftoken_offers_error() { + Ok(_) => Ok(()), + Err(error) => Err!(error), + } + } +} + +impl<'a> Transaction for NFTokenCancelOffer<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> NFTokenCancelOfferError for NFTokenCancelOffer<'a> { + fn _get_nftoken_offers_error(&self) -> Result<(), XRPLNFTokenCancelOfferException> { + if self.nftoken_offers.is_empty() { + Err(XRPLNFTokenCancelOfferException::CollectionEmpty { + field: "nftoken_offers", + r#type: stringify!(Vec), + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> NFTokenCancelOffer<'a> { + fn new( + account: &'a str, + nftoken_offers: Vec<&'a str>, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + ) -> Self { + Self { + transaction_type: TransactionType::NFTokenCancelOffer, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + nftoken_offers, + } + } +} + +pub trait NFTokenCancelOfferError { + fn _get_nftoken_offers_error(&self) -> Result<(), XRPLNFTokenCancelOfferException>; +} + +#[cfg(test)] +mod test_nftoken_cancel_offer_error { + use alloc::string::ToString; + use alloc::vec::Vec; + + use crate::models::Model; + + use super::*; + + #[test] + fn test_nftoken_offer_error() { + let nftoken_cancel_offer = NFTokenCancelOffer { + transaction_type: TransactionType::NFTokenCancelOffer, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_offers: Vec::new(), + }; + + assert_eq!( + nftoken_cancel_offer.validate().unwrap_err().to_string().as_str(), + "The value of the field `nftoken_offers` is not allowed to be empty (type `Vec`). If the field is optional, define it to be `None`. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use alloc::vec; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = NFTokenCancelOffer::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + vec!["9C92E061381C1EF37A8CDE0E8FC35188BFC30B1883825042A64309AC09F4C36D"], + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"NFTokenCancelOffer","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","NFTokenOffers":["9C92E061381C1EF37A8CDE0E8FC35188BFC30B1883825042A64309AC09F4C36D"]}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = NFTokenCancelOffer::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + vec!["9C92E061381C1EF37A8CDE0E8FC35188BFC30B1883825042A64309AC09F4C36D"], + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"NFTokenCancelOffer","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","NFTokenOffers":["9C92E061381C1EF37A8CDE0E8FC35188BFC30B1883825042A64309AC09F4C36D"]}"#; + + let txn_as_obj: NFTokenCancelOffer = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/nftoken_create_offer.rs b/src/models/transactions/nftoken_create_offer.rs new file mode 100644 index 00000000..658a061a --- /dev/null +++ b/src/models/transactions/nftoken_create_offer.rs @@ -0,0 +1,490 @@ +use alloc::vec::Vec; +use anyhow::Result; +use core::convert::TryInto; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use alloc::string::ToString; + +use crate::models::{ + model::Model, + transactions::{Flag, Memo, Signer, Transaction, TransactionType}, +}; + +use crate::Err; +use crate::_serde::txn_flags; +use crate::models::amount::exceptions::XRPLAmountException; +use crate::models::amount::{Amount, XRPAmount}; +use crate::models::transactions::XRPLNFTokenCreateOfferException; + +/// Transactions of the NFTokenCreateOffer type support additional values +/// in the Flags field. This enum represents those options. +/// +/// See NFTokenCreateOffer flags: +/// `` +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum NFTokenCreateOfferFlag { + /// If enabled, indicates that the offer is a sell offer. + /// Otherwise, it is a buy offer. + TfSellOffer = 0x00000001, +} + +/// Creates either a new Sell offer for an NFToken owned by +/// the account executing the transaction, or a new Buy +/// offer for an NFToken owned by another account. +/// +/// See NFTokenCreateOffer: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NFTokenCreateOffer<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::nftoken_create_offer")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the NFTokenCreateOffer model. + /// + /// See NFTokenCreateOffer fields: + /// `` + #[serde(rename = "NFTokenID")] + pub nftoken_id: &'a str, + pub amount: Amount<'a>, + pub owner: Option<&'a str>, + pub expiration: Option, + pub destination: Option<&'a str>, +} + +impl<'a> Default for NFTokenCreateOffer<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::NFTokenCreateOffer, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + nftoken_id: Default::default(), + amount: Default::default(), + owner: Default::default(), + expiration: Default::default(), + destination: Default::default(), + } + } +} + +impl<'a: 'static> Model for NFTokenCreateOffer<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_amount_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_destination_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_owner_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + }, + }, + } + } +} + +impl<'a> Transaction for NFTokenCreateOffer<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + let mut flags = &Vec::new(); + + if let Some(flag_set) = self.flags.as_ref() { + flags = flag_set; + } + + match flag { + Flag::NFTokenCreateOffer(nftoken_create_offer_flag) => { + match nftoken_create_offer_flag { + NFTokenCreateOfferFlag::TfSellOffer => { + flags.contains(&NFTokenCreateOfferFlag::TfSellOffer) + } + } + } + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> NFTokenCreateOfferError for NFTokenCreateOffer<'a> { + fn _get_amount_error(&self) -> Result<()> { + let amount_into_decimal: Result = + self.amount.clone().try_into(); + match amount_into_decimal { + Ok(amount) => { + if !self.has_flag(&Flag::NFTokenCreateOffer( + NFTokenCreateOfferFlag::TfSellOffer, + )) && amount.is_zero() + { + Err!(XRPLNFTokenCreateOfferException::ValueZero { + field: "amount", + resource: "", + }) + } else { + Ok(()) + } + } + Err(decimal_error) => { + Err!(decimal_error) + } + } + } + + fn _get_destination_error(&self) -> Result<(), XRPLNFTokenCreateOfferException> { + if let Some(destination) = self.destination { + if destination == self.account { + Err(XRPLNFTokenCreateOfferException::ValueEqualsValue { + field1: "destination", + field2: "account", + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_owner_error(&self) -> Result<(), XRPLNFTokenCreateOfferException> { + if let Some(owner) = self.owner { + if self.has_flag(&Flag::NFTokenCreateOffer( + NFTokenCreateOfferFlag::TfSellOffer, + )) { + Err(XRPLNFTokenCreateOfferException::IllegalOption { + field: "owner", + context: "NFToken sell offers", + resource: "", + }) + } else if owner == self.account { + Err(XRPLNFTokenCreateOfferException::ValueEqualsValue { + field1: "owner", + field2: "account", + resource: "", + }) + } else { + Ok(()) + } + } else if !self.has_flag(&Flag::NFTokenCreateOffer( + NFTokenCreateOfferFlag::TfSellOffer, + )) { + Err(XRPLNFTokenCreateOfferException::OptionRequired { + field: "owner", + context: "NFToken buy offers", + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> NFTokenCreateOffer<'a> { + fn new( + account: &'a str, + nftoken_id: &'a str, + amount: Amount<'a>, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + flags: Option>, + memos: Option>>, + signers: Option>>, + owner: Option<&'a str>, + expiration: Option, + destination: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::NFTokenCreateOffer, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags, + memos, + signers, + nftoken_id, + amount, + owner, + expiration, + destination, + } + } +} + +pub trait NFTokenCreateOfferError { + fn _get_amount_error(&self) -> Result<()>; + fn _get_destination_error(&self) -> Result<(), XRPLNFTokenCreateOfferException>; + fn _get_owner_error(&self) -> Result<(), XRPLNFTokenCreateOfferException>; +} + +#[cfg(test)] +mod test_nftoken_create_offer_error { + use alloc::string::ToString; + use alloc::vec; + + use crate::models::{ + amount::{Amount, XRPAmount}, + Model, + }; + + use super::*; + + #[test] + fn test_amount_error() { + let nftoken_create_offer = NFTokenCreateOffer { + transaction_type: TransactionType::NFTokenCreateOffer, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_id: "", + amount: Amount::XRPAmount(XRPAmount::from("0")), + owner: None, + expiration: None, + destination: None, + }; + + assert_eq!( + nftoken_create_offer + .validate() + .unwrap_err() + .to_string() + .as_str(), + "The value of the field `amount` is not allowed to be zero. For more information see: " + ); + } + + #[test] + fn test_destination_error() { + let nftoken_create_offer = NFTokenCreateOffer { + transaction_type: TransactionType::NFTokenCreateOffer, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_id: "", + amount: Amount::XRPAmount(XRPAmount::from("1")), + owner: None, + expiration: None, + destination: Some("rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb"), + }; + + assert_eq!( + nftoken_create_offer.validate().unwrap_err().to_string().as_str(), + "The value of the field `destination` is not allowed to be the same as the value of the field `account`. For more information see: " + ); + } + + #[test] + fn test_owner_error() { + let mut nftoken_create_offer = NFTokenCreateOffer { + transaction_type: TransactionType::NFTokenCreateOffer, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_id: "", + amount: Amount::XRPAmount(XRPAmount::from("1")), + owner: Some("rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK"), + expiration: None, + destination: None, + }; + let sell_flag = vec![NFTokenCreateOfferFlag::TfSellOffer]; + nftoken_create_offer.flags = Some(sell_flag); + + assert_eq!( + nftoken_create_offer.validate().unwrap_err().to_string().as_str(), + "The optional field `owner` is not allowed to be defined for NFToken sell offers. For more information see: " + ); + + nftoken_create_offer.flags = None; + nftoken_create_offer.owner = None; + + assert_eq!( + nftoken_create_offer.validate().unwrap_err().to_string().as_str(), + "The optional field `owner` is required to be defined for NFToken buy offers. For more information see: " + ); + + nftoken_create_offer.owner = Some("rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb"); + + assert_eq!( + nftoken_create_offer.validate().unwrap_err().to_string().as_str(), + "The value of the field `owner` is not allowed to be the same as the value of the field `account`. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use crate::models::amount::XRPAmount; + use alloc::vec; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = NFTokenCreateOffer::new( + "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX", + "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", + Amount::XRPAmount(XRPAmount::from("1000000")), + None, + None, + None, + None, + None, + None, + None, + None, + Some(vec![NFTokenCreateOfferFlag::TfSellOffer]), + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"NFTokenCreateOffer","Account":"rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX","Flags":1,"NFTokenID":"000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007","Amount":"1000000"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = NFTokenCreateOffer::new( + "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX", + "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", + Amount::XRPAmount(XRPAmount::from("1000000")), + None, + None, + None, + None, + None, + None, + None, + None, + Some(vec![NFTokenCreateOfferFlag::TfSellOffer]), + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"NFTokenCreateOffer","Account":"rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX","NFTokenID":"000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007","Amount":"1000000","Flags":1}"#; + + let txn_as_obj: NFTokenCreateOffer = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/nftoken_mint.rs b/src/models/transactions/nftoken_mint.rs new file mode 100644 index 00000000..e61dbc08 --- /dev/null +++ b/src/models/transactions/nftoken_mint.rs @@ -0,0 +1,439 @@ +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use alloc::string::ToString; + +use crate::{ + constants::{MAX_TRANSFER_FEE, MAX_URI_LENGTH}, + models::{ + model::Model, + transactions::{Flag, Memo, Signer, Transaction, TransactionType}, + }, + Err, +}; + +use crate::_serde::txn_flags; +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLNFTokenMintException; + +/// Transactions of the NFTokenMint type support additional values +/// in the Flags field. This enum represents those options. +/// +/// See NFTokenMint flags: +/// `` +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum NFTokenMintFlag { + /// Allow the issuer (or an entity authorized by the issuer) to + /// destroy the minted NFToken. (The NFToken's owner can always do so.) + TfBurnable = 0x00000001, + /// The minted NFToken can only be bought or sold for XRP. + /// This can be desirable if the token has a transfer fee and the issuer + /// does not want to receive fees in non-XRP currencies. + TfOnlyXRP = 0x00000002, + /// The minted NFToken can be transferred to others. If this flag is not + /// enabled, the token can still be transferred from or to the issuer. + TfTransferable = 0x00000008, +} + +/// The NFTokenMint transaction creates a non-fungible token and adds it to +/// the relevant NFTokenPage object of the NFTokenMinter as an NFToken object. +/// +/// See NFTokenMint: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct NFTokenMint<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::nftoken_mint")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the NFTokenMint model. + /// + /// See NFTokenMint fields: + /// `` + #[serde(rename = "NFTokenTaxon")] + pub nftoken_taxon: u32, + pub issuer: Option<&'a str>, + pub transfer_fee: Option, + #[serde(rename = "URI")] + pub uri: Option<&'a str>, +} + +impl<'a> Default for NFTokenMint<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::NFTokenMint, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + nftoken_taxon: Default::default(), + issuer: Default::default(), + transfer_fee: Default::default(), + uri: Default::default(), + } + } +} + +impl<'a: 'static> Model for NFTokenMint<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_issuer_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_transfer_fee_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_uri_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + }, + }, + } + } +} + +impl<'a> Transaction for NFTokenMint<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + let mut flags = &Vec::new(); + + if let Some(flag_set) = self.flags.as_ref() { + flags = flag_set; + } + + match flag { + Flag::NFTokenMint(nftoken_mint_flag) => match nftoken_mint_flag { + NFTokenMintFlag::TfBurnable => flags.contains(&NFTokenMintFlag::TfBurnable), + NFTokenMintFlag::TfOnlyXRP => flags.contains(&NFTokenMintFlag::TfOnlyXRP), + NFTokenMintFlag::TfTransferable => flags.contains(&NFTokenMintFlag::TfTransferable), + }, + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> NFTokenMintError for NFTokenMint<'a> { + fn _get_issuer_error(&self) -> Result<(), XRPLNFTokenMintException> { + if let Some(issuer) = self.issuer { + if issuer == self.account { + Err(XRPLNFTokenMintException::ValueEqualsValue { + field1: "issuer", + field2: "account", + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_transfer_fee_error(&self) -> Result<(), XRPLNFTokenMintException> { + if let Some(transfer_fee) = self.transfer_fee { + if transfer_fee > MAX_TRANSFER_FEE { + Err(XRPLNFTokenMintException::ValueTooHigh { + field: "transfer_fee", + max: MAX_TRANSFER_FEE, + found: transfer_fee, + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_uri_error(&self) -> Result<(), XRPLNFTokenMintException> { + if let Some(uri) = self.uri { + if uri.len() > MAX_URI_LENGTH { + Err(XRPLNFTokenMintException::ValueTooLong { + field: "uri", + max: MAX_URI_LENGTH, + found: uri.len(), + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } +} + +impl<'a> NFTokenMint<'a> { + fn new( + account: &'a str, + nftoken_taxon: u32, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + flags: Option>, + memos: Option>>, + signers: Option>>, + issuer: Option<&'a str>, + transfer_fee: Option, + uri: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::NFTokenMint, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags, + memos, + signers, + nftoken_taxon, + issuer, + transfer_fee, + uri, + } + } +} + +pub trait NFTokenMintError { + fn _get_issuer_error(&self) -> Result<(), XRPLNFTokenMintException>; + fn _get_transfer_fee_error(&self) -> Result<(), XRPLNFTokenMintException>; + fn _get_uri_error(&self) -> Result<(), XRPLNFTokenMintException>; +} + +#[cfg(test)] +mod test_nftoken_mint_error { + + use crate::models::Model; + use alloc::string::ToString; + + use super::*; + + #[test] + fn test_issuer_error() { + let nftoken_mint = NFTokenMint { + transaction_type: TransactionType::NFTokenMint, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_taxon: 0, + issuer: Some("rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb"), + transfer_fee: None, + uri: None, + }; + + assert_eq!( + nftoken_mint.validate().unwrap_err().to_string().as_str(), + "The value of the field `issuer` is not allowed to be the same as the value of the field `account`. For more information see: " + ); + } + + #[test] + fn test_transfer_fee_error() { + let nftoken_mint = NFTokenMint { + transaction_type: TransactionType::NFTokenMint, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_taxon: 0, + issuer: None, + transfer_fee: Some(50001), + uri: None, + }; + + assert_eq!( + nftoken_mint.validate().unwrap_err().to_string().as_str(), + "The field `transfer_fee` exceeds its maximum value (max 50000, found 50001). For more information see: " + ); + } + + #[test] + fn test_uri_error() { + let nftoken_mint = NFTokenMint { + transaction_type: TransactionType::NFTokenMint, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + nftoken_taxon: 0, + issuer: None, + transfer_fee: None, + uri: Some("wss://xrplcluster.com/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + }; + + assert_eq!( + nftoken_mint.validate().unwrap_err().to_string().as_str(), + "The value of the field `uri` exceeds its maximum length of characters (max 512, found 513). For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use alloc::vec; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = NFTokenMint::new( + "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + 0, + Some("10".into()), + None, + None, + None, + None, + None, + None, + None, + Some(vec![NFTokenMintFlag::TfTransferable]), + Some(vec![Memo::new(Some("72656E74"), None, Some("687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E65726963"))]), + None, + None, + Some(314), + Some("697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469"), + ); + let default_json = r#"{"TransactionType":"NFTokenMint","Account":"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B","Fee":"10","Flags":8,"Memos":[{"Memo":{"MemoData":"72656E74","MemoFormat":null,"MemoType":"687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E65726963"}}],"NFTokenTaxon":0,"TransferFee":314,"URI":"697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = NFTokenMint::new( + "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + 0, + Some("10".into()), + None, + None, + None, + None, + None, + None, + None, + Some(vec![NFTokenMintFlag::TfTransferable]), + Some(vec![Memo::new(Some("72656E74"), None, Some("687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E65726963"))]), + None, + None, + Some(314), + Some("697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469"), + ); + let default_json = r#"{"TransactionType":"NFTokenMint","Account":"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B","TransferFee":314,"NFTokenTaxon":0,"Flags":8,"Fee":"10","URI":"697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469","Memos":[{"Memo":{"MemoType":"687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E65726963","MemoFormat":null,"MemoData":"72656E74"}}]}"#; + + let txn_as_obj: NFTokenMint = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/offer_cancel.rs b/src/models/transactions/offer_cancel.rs new file mode 100644 index 00000000..643252a2 --- /dev/null +++ b/src/models/transactions/offer_cancel.rs @@ -0,0 +1,196 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Removes an Offer object from the XRP Ledger. +/// +/// See OfferCancel: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct OfferCancel<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::offer_cancel")] + transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the OfferCancel model. + /// + /// See OfferCancel fields: + /// `` + pub offer_sequence: u32, +} + +impl<'a> Default for OfferCancel<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::OfferCancel, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + offer_sequence: Default::default(), + } + } +} + +impl<'a> Model for OfferCancel<'a> {} + +impl<'a> Transaction for OfferCancel<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> OfferCancel<'a> { + fn new( + account: &'a str, + offer_sequence: u32, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + ) -> Self { + Self { + transaction_type: TransactionType::OfferCancel, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + offer_sequence, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = OfferCancel::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + 6, + Some("12".into()), + Some(7), + Some(7108629), + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"OfferCancel","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Fee":"12","Sequence":7,"LastLedgerSequence":7108629,"OfferSequence":6}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = OfferCancel::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + 6, + Some("12".into()), + Some(7), + Some(7108629), + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"OfferCancel","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Fee":"12","LastLedgerSequence":7108629,"OfferSequence":6,"Sequence":7}"#; + + let txn_as_obj: OfferCancel = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/offer_create.rs b/src/models/transactions/offer_create.rs new file mode 100644 index 00000000..1acd33de --- /dev/null +++ b/src/models/transactions/offer_create.rs @@ -0,0 +1,352 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use crate::models::{ + amount::Amount, + model::Model, + transactions::{Flag, Memo, Signer, Transaction, TransactionType}, +}; + +use crate::_serde::txn_flags; +use crate::models::amount::XRPAmount; + +/// Transactions of the OfferCreate type support additional values +/// in the Flags field. This enum represents those options. +/// +/// See OfferCreate flags: +/// `` +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum OfferCreateFlag { + /// If enabled, the Offer does not consume Offers that exactly match it, + /// and instead becomes an Offer object in the ledger. + /// It still consumes Offers that cross it. + TfPassive = 0x00010000, + /// Treat the Offer as an Immediate or Cancel order. The Offer never creates + /// an Offer object in the ledger: it only trades as much as it can by + /// consuming existing Offers at the time the transaction is processed. If no + /// Offers match, it executes "successfully" without trading anything. + /// In this case, the transaction still uses the result code tesSUCCESS. + TfImmediateOrCancel = 0x00020000, + /// Treat the offer as a Fill or Kill order . The Offer never creates an Offer + /// object in the ledger, and is canceled if it cannot be fully filled at the + /// time of execution. By default, this means that the owner must receive the + /// full TakerPays amount; if the tfSell flag is enabled, the owner must be + /// able to spend the entire TakerGets amount instead. + TfFillOrKill = 0x00040000, + /// Exchange the entire TakerGets amount, even if it means obtaining more than + /// the TakerPays amount in exchange. + TfSell = 0x00080000, +} + +/// Places an Offer in the decentralized exchange. +/// +/// See OfferCreate: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct OfferCreate<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::offer_create")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the OfferCreate model. + /// + /// See OfferCreate fields: + /// `` + pub taker_gets: Amount<'a>, + pub taker_pays: Amount<'a>, + pub expiration: Option, + pub offer_sequence: Option, +} + +impl<'a> Default for OfferCreate<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::OfferCreate, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + taker_gets: Default::default(), + taker_pays: Default::default(), + expiration: Default::default(), + offer_sequence: Default::default(), + } + } +} + +impl<'a> Model for OfferCreate<'a> {} + +impl<'a> Transaction for OfferCreate<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + let mut flags = &Vec::new(); + + if let Some(flag_set) = self.flags.as_ref() { + flags = flag_set; + } + + match flag { + Flag::OfferCreate(offer_create_flag) => match offer_create_flag { + OfferCreateFlag::TfFillOrKill => flags.contains(&OfferCreateFlag::TfFillOrKill), + OfferCreateFlag::TfImmediateOrCancel => { + flags.contains(&OfferCreateFlag::TfImmediateOrCancel) + } + OfferCreateFlag::TfPassive => flags.contains(&OfferCreateFlag::TfPassive), + OfferCreateFlag::TfSell => flags.contains(&OfferCreateFlag::TfSell), + }, + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> OfferCreate<'a> { + fn new( + account: &'a str, + taker_gets: Amount<'a>, + taker_pays: Amount<'a>, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + flags: Option>, + memos: Option>>, + signers: Option>>, + expiration: Option, + offer_sequence: Option, + ) -> Self { + Self { + transaction_type: TransactionType::OfferCreate, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags, + memos, + signers, + taker_gets, + taker_pays, + expiration, + offer_sequence, + } + } +} + +#[cfg(test)] +mod test { + use crate::models::amount::{IssuedCurrencyAmount, XRPAmount}; + use alloc::vec; + + use super::*; + + #[test] + fn test_has_flag() { + let txn: OfferCreate = OfferCreate { + transaction_type: TransactionType::OfferCreate, + account: "rpXhhWmCvDwkzNtRbm7mmD1vZqdfatQNEe", + fee: Some("10".into()), + sequence: Some(1), + last_ledger_sequence: Some(72779837), + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: Some(vec![OfferCreateFlag::TfImmediateOrCancel]), + memos: None, + signers: None, + taker_gets: Amount::XRPAmount(XRPAmount::from("1000000")), + taker_pays: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq".into(), + "0.3".into(), + )), + expiration: None, + offer_sequence: None, + }; + assert!(txn.has_flag(&Flag::OfferCreate(OfferCreateFlag::TfImmediateOrCancel))); + assert!(!txn.has_flag(&Flag::OfferCreate(OfferCreateFlag::TfPassive))); + } + + #[test] + fn test_get_transaction_type() { + let txn: OfferCreate = OfferCreate { + transaction_type: TransactionType::OfferCreate, + account: "rpXhhWmCvDwkzNtRbm7mmD1vZqdfatQNEe", + fee: Some("10".into()), + sequence: Some(1), + last_ledger_sequence: Some(72779837), + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: Some(vec![OfferCreateFlag::TfImmediateOrCancel]), + memos: None, + signers: None, + taker_gets: Amount::XRPAmount(XRPAmount::from("1000000")), + taker_pays: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq".into(), + "0.3".into(), + )), + expiration: None, + offer_sequence: None, + }; + let actual = txn.get_transaction_type(); + let expect = TransactionType::OfferCreate; + assert_eq!(actual, expect) + } +} + +#[cfg(test)] +mod test_serde { + use crate::models::amount::{IssuedCurrencyAmount, XRPAmount}; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = OfferCreate::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + Amount::XRPAmount(XRPAmount::from("6000000")), + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "GKO".into(), + "ruazs5h1qEsqpke88pcqnaseXdm6od2xc".into(), + "2".into(), + )), + Some("12".into()), + Some(8), + Some(7108682), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"OfferCreate","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Fee":"12","Sequence":8,"LastLedgerSequence":7108682,"TakerGets":"6000000","TakerPays":{"currency":"GKO","issuer":"ruazs5h1qEsqpke88pcqnaseXdm6od2xc","value":"2"}}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = OfferCreate::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + Amount::XRPAmount(XRPAmount::from("6000000")), + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "GKO".into(), + "ruazs5h1qEsqpke88pcqnaseXdm6od2xc".into(), + "2".into(), + )), + Some("12".into()), + Some(8), + Some(7108682), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"OfferCreate","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Fee":"12","Sequence":8,"LastLedgerSequence":7108682,"TakerGets":"6000000","TakerPays":{"value":"2","currency":"GKO","issuer":"ruazs5h1qEsqpke88pcqnaseXdm6od2xc"}}"#; + + let txn_as_obj: OfferCreate = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/payment.rs b/src/models/transactions/payment.rs new file mode 100644 index 00000000..41c757a0 --- /dev/null +++ b/src/models/transactions/payment.rs @@ -0,0 +1,532 @@ +use alloc::vec::Vec; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use crate::models::{ + amount::Amount, + model::Model, + transactions::{Flag, Memo, Signer, Transaction, TransactionType}, + PathStep, +}; +use alloc::string::ToString; + +use crate::Err; +use crate::_serde::txn_flags; +use crate::models::amount::XRPAmount; +use crate::models::transactions::XRPLPaymentException; + +/// Transactions of the Payment type support additional values +/// in the Flags field. This enum represents those options. +/// +/// See Payment flags: +/// `` +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum PaymentFlag { + /// Do not use the default path; only use paths included in the Paths field. + /// This is intended to force the transaction to take arbitrage opportunities. + /// Most clients do not need this. + TfNoDirectRipple = 0x00010000, + /// If the specified Amount cannot be sent without spending more than SendMax, + /// reduce the received amount instead of failing outright. + /// See Partial Payments for more details. + TfPartialPayment = 0x00020000, + /// Only take paths where all the conversions have an input:output ratio that + /// is equal or better than the ratio of Amount:SendMax. + /// See Limit Quality for details. + TfLimitQuality = 0x00040000, +} + +/// Transfers value from one account to another. +/// +/// See Payment: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Payment<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::payment")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the Payment model. + /// + /// See Payment fields: + /// `` + pub amount: Amount<'a>, + pub destination: &'a str, + pub destination_tag: Option, + pub invoice_id: Option, + pub paths: Option>>>, + pub send_max: Option>, + pub deliver_min: Option>, +} + +impl<'a> Default for Payment<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::Payment, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + amount: Default::default(), + destination: Default::default(), + destination_tag: Default::default(), + invoice_id: Default::default(), + paths: Default::default(), + send_max: Default::default(), + deliver_min: Default::default(), + } + } +} + +impl<'a: 'static> Model for Payment<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_xrp_transaction_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_partial_payment_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_exchange_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + }, + }, + } + } +} + +impl<'a> Transaction for Payment<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + let mut flags = &Vec::new(); + + if let Some(flag_set) = self.flags.as_ref() { + flags = flag_set; + } + + match flag { + Flag::Payment(payment_flag) => match payment_flag { + PaymentFlag::TfLimitQuality => flags.contains(&PaymentFlag::TfLimitQuality), + PaymentFlag::TfNoDirectRipple => flags.contains(&PaymentFlag::TfNoDirectRipple), + PaymentFlag::TfPartialPayment => flags.contains(&PaymentFlag::TfPartialPayment), + }, + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> PaymentError for Payment<'a> { + fn _get_xrp_transaction_error(&self) -> Result<(), XRPLPaymentException> { + if self.amount.is_xrp() && self.send_max.is_none() { + if self.paths.is_some() { + Err(XRPLPaymentException::IllegalOption { + field: "paths", + context: "XRP to XRP payments", + resource: "", + }) + } else if self.account == self.destination { + Err(XRPLPaymentException::ValueEqualsValueInContext { + field1: "account", + field2: "destination", + context: "XRP to XRP Payments", + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_partial_payment_error(&self) -> Result<(), XRPLPaymentException> { + if let Some(send_max) = &self.send_max { + if !self.has_flag(&Flag::Payment(PaymentFlag::TfPartialPayment)) + && send_max.is_xrp() + && self.amount.is_xrp() + { + Err(XRPLPaymentException::IllegalOption { + field: "send_max", + context: "XRP to XRP non-partial payments", + resource: "", + }) + } else { + Ok(()) + } + } else if self.has_flag(&Flag::Payment(PaymentFlag::TfPartialPayment)) { + Err(XRPLPaymentException::FlagRequiresField { + flag: PaymentFlag::TfPartialPayment, + field: "send_max", + resource: "", + }) + } else if !self.has_flag(&Flag::Payment(PaymentFlag::TfPartialPayment)) { + if let Some(_deliver_min) = &self.deliver_min { + Err(XRPLPaymentException::IllegalOption { + field: "deliver_min", + context: "XRP to XRP non-partial payments", + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_exchange_error(&self) -> Result<(), XRPLPaymentException> { + if self.account == self.destination && self.send_max.is_none() { + return Err(XRPLPaymentException::OptionRequired { + field: "send_max", + context: "exchanges", + resource: "", + }); + } + + Ok(()) + } +} + +impl<'a> Payment<'a> { + fn new( + account: &'a str, + amount: Amount<'a>, + destination: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + flags: Option>, + memos: Option>>, + signers: Option>>, + destination_tag: Option, + invoice_id: Option, + paths: Option>>>, + send_max: Option>, + deliver_min: Option>, + ) -> Self { + Self { + transaction_type: TransactionType::Payment, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags, + memos, + signers, + amount, + destination, + destination_tag, + invoice_id, + paths, + send_max, + deliver_min, + } + } +} + +pub trait PaymentError { + fn _get_xrp_transaction_error(&self) -> Result<(), XRPLPaymentException>; + fn _get_partial_payment_error(&self) -> Result<(), XRPLPaymentException>; + fn _get_exchange_error(&self) -> Result<(), XRPLPaymentException>; +} + +#[cfg(test)] +mod test_payment_error { + use alloc::string::ToString; + use alloc::vec; + + use crate::models::{ + amount::{Amount, IssuedCurrencyAmount, XRPAmount}, + Model, PathStep, + }; + + use super::*; + + #[test] + fn test_xrp_to_xrp_error() { + let mut payment = Payment { + transaction_type: TransactionType::Payment, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + amount: Amount::XRPAmount(XRPAmount::from("1000000")), + destination: "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK", + destination_tag: None, + invoice_id: None, + paths: Some(vec![vec![PathStep { + account: Some("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), + currency: None, + issuer: None, + r#type: None, + type_hex: None, + }]]), + send_max: None, + deliver_min: None, + }; + + assert_eq!( + payment.validate().unwrap_err().to_string().as_str(), + "The optional field `paths` is not allowed to be defined for XRP to XRP payments.For more information see: " + ); + + payment.paths = None; + payment.send_max = Some(Amount::XRPAmount(XRPAmount::from("99999"))); + + assert_eq!( + payment.validate().unwrap_err().to_string().as_str(), + "The optional field `send_max` is not allowed to be defined for XRP to XRP non-partial payments.For more information see: " + ); + + payment.send_max = None; + payment.destination = "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb"; + + assert_eq!( + payment.validate().unwrap_err().to_string().as_str(), + "The value of the field `account` is not allowed to be the same as the value of the field `destination`, for XRP to XRP Payments. For more information see: " + ); + } + + #[test] + fn test_partial_payments_eror() { + let mut payment = Payment { + transaction_type: TransactionType::Payment, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + amount: Amount::XRPAmount("1000000".into()), + destination: "rLSn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK", + destination_tag: None, + invoice_id: None, + paths: None, + send_max: None, + deliver_min: None, + }; + payment.flags = Some(vec![PaymentFlag::TfPartialPayment]); + + assert_eq!( + payment.validate().unwrap_err().to_string().as_str(), + "For the flag `TfPartialPayment` to be set it is required to define the field `send_max`. For more information see: " + ); + + payment.flags = None; + payment.deliver_min = Some(Amount::XRPAmount("99999".into())); + + assert_eq!( + payment.validate().unwrap_err().to_string().as_str(), + "The optional field `deliver_min` is not allowed to be defined for XRP to XRP non-partial payments.For more information see: " + ); + } + + #[test] + fn test_exchange_error() { + let payment = Payment { + transaction_type: TransactionType::Payment, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + amount: Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(), + "10".into(), + )), + destination: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + destination_tag: None, + invoice_id: None, + paths: None, + send_max: None, + deliver_min: None, + }; + + assert_eq!( + payment.validate().unwrap_err().to_string().as_str(), + "The optional field `send_max` is required to be defined for exchanges. For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use alloc::vec; + + use crate::models::amount::{Amount, IssuedCurrencyAmount}; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = Payment::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(), + "1".into(), + )), + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + Some("12".into()), + Some(2), + None, + None, + None, + None, + None, + None, + Some(vec![PaymentFlag::TfPartialPayment]), + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"Payment","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"12","Sequence":2,"Flags":131072,"Amount":{"currency":"USD","issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","value":"1"},"Destination":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = Payment::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new( + "USD".into(), + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn".into(), + "1".into(), + )), + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + Some("12".into()), + Some(2), + None, + None, + None, + None, + None, + None, + Some(vec![PaymentFlag::TfPartialPayment]), + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"Payment","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Destination":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Amount":{"currency":"USD","value":"1","issuer":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"},"Fee":"12","Flags":131072,"Sequence":2}"#; + + let txn_as_obj: Payment = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/payment_channel_claim.rs b/src/models/transactions/payment_channel_claim.rs new file mode 100644 index 00000000..61ccbd34 --- /dev/null +++ b/src/models/transactions/payment_channel_claim.rs @@ -0,0 +1,279 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use crate::models::{ + model::Model, + transactions::{Flag, Memo, Signer, Transaction, TransactionType}, +}; + +use crate::_serde::txn_flags; +use crate::models::amount::XRPAmount; + +/// Transactions of the PaymentChannelClaim type support additional values +/// in the Flags field. This enum represents those options. +/// +/// See PaymentChannelClaim flags: +/// `` +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum PaymentChannelClaimFlag { + /// Clear the channel's Expiration time. (Expiration is different from the + /// channel's immutable CancelAfter time.) Only the source address of the + /// payment channel can use this flag. + TfRenew = 0x00010000, + /// Request to close the channel. Only the channel source and destination + /// addresses can use this flag. This flag closes the channel immediately if + /// it has no more XRP allocated to it after processing the current claim, + /// or if the destination address uses it. If the source address uses this + /// flag when the channel still holds XRP, this schedules the channel to close + /// after SettleDelay seconds have passed. (Specifically, this sets the Expiration + /// of the channel to the close time of the previous ledger plus the channel's + /// SettleDelay time, unless the channel already has an earlier Expiration time.) + /// If the destination address uses this flag when the channel still holds XRP, + /// any XRP that remains after processing the claim is returned to the source address. + TfClose = 0x00020000, +} + +/// Claim XRP from a payment channel, adjust +/// the payment channel's expiration, or both. +/// +/// See PaymentChannelClaim: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct PaymentChannelClaim<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::payment_channel_claim")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the PaymentChannelClaim model. + /// + /// See PaymentChannelClaim fields: + /// `` + pub channel: &'a str, + pub balance: Option<&'a str>, + pub amount: Option<&'a str>, + pub signature: Option<&'a str>, + pub public_key: Option<&'a str>, +} + +impl<'a> Default for PaymentChannelClaim<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::PaymentChannelClaim, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + channel: Default::default(), + balance: Default::default(), + amount: Default::default(), + signature: Default::default(), + public_key: Default::default(), + } + } +} + +impl<'a> Model for PaymentChannelClaim<'a> {} + +impl<'a> Transaction for PaymentChannelClaim<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + let mut flags = &Vec::new(); + + if let Some(flag_set) = self.flags.as_ref() { + flags = flag_set; + } + + match flag { + Flag::PaymentChannelClaim(payment_channel_claim_flag) => { + match payment_channel_claim_flag { + PaymentChannelClaimFlag::TfClose => { + flags.contains(&PaymentChannelClaimFlag::TfClose) + } + PaymentChannelClaimFlag::TfRenew => { + flags.contains(&PaymentChannelClaimFlag::TfRenew) + } + } + } + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> PaymentChannelClaim<'a> { + fn new( + account: &'a str, + channel: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + flags: Option>, + memos: Option>>, + signers: Option>>, + balance: Option<&'a str>, + amount: Option<&'a str>, + signature: Option<&'a str>, + public_key: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::PaymentChannelClaim, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags, + memos, + signers, + channel, + balance, + amount, + signature, + public_key, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = PaymentChannelClaim::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("1000000"), + Some("1000000"), + Some("30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B"), + Some("32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A"), + ); + let default_json = r#"{"TransactionType":"PaymentChannelClaim","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Channel":"C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198","Balance":"1000000","Amount":"1000000","Signature":"30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B","PublicKey":"32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = PaymentChannelClaim::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("1000000"), + Some("1000000"), + Some("30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B"), + Some("32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A"), + ); + let default_json = r#"{"TransactionType":"PaymentChannelClaim","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Channel":"C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198","Balance":"1000000","Amount":"1000000","Signature":"30440220718D264EF05CAED7C781FF6DE298DCAC68D002562C9BF3A07C1E721B420C0DAB02203A5A4779EF4D2CCC7BC3EF886676D803A9981B928D3B8ACA483B80ECA3CD7B9B","PublicKey":"32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A"}"#; + + let txn_as_obj: PaymentChannelClaim = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/payment_channel_create.rs b/src/models/transactions/payment_channel_create.rs new file mode 100644 index 00000000..9188e8a2 --- /dev/null +++ b/src/models/transactions/payment_channel_create.rs @@ -0,0 +1,226 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Create a unidirectional channel and fund it with XRP. +/// +/// See PaymentChannelCreate fields: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct PaymentChannelCreate<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::payment_channel_create")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the PaymentChannelCreate model. + /// + /// See PaymentChannelCreate fields: + /// `` + pub amount: XRPAmount<'a>, + pub destination: &'a str, + pub settle_delay: u32, + pub public_key: &'a str, + pub cancel_after: Option, + pub destination_tag: Option, +} + +impl<'a> Default for PaymentChannelCreate<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::PaymentChannelCreate, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + amount: Default::default(), + destination: Default::default(), + settle_delay: Default::default(), + public_key: Default::default(), + cancel_after: Default::default(), + destination_tag: Default::default(), + } + } +} + +impl<'a> Model for PaymentChannelCreate<'a> {} + +impl<'a> Transaction for PaymentChannelCreate<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> PaymentChannelCreate<'a> { + fn new( + account: &'a str, + amount: XRPAmount<'a>, + destination: &'a str, + settle_delay: u32, + public_key: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + cancel_after: Option, + destination_tag: Option, + ) -> Self { + Self { + transaction_type: TransactionType::PaymentChannelCreate, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + amount, + destination, + settle_delay, + public_key, + cancel_after, + destination_tag, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = PaymentChannelCreate::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + XRPAmount::from("10000"), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + 86400, + "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", + None, + None, + None, + None, + None, + Some(11747), + None, + None, + None, + None, + Some(533171558), + Some(23480), + ); + let default_json = r#"{"TransactionType":"PaymentChannelCreate","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","SourceTag":11747,"Amount":"10000","Destination":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","SettleDelay":86400,"PublicKey":"32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A","CancelAfter":533171558,"DestinationTag":23480}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = PaymentChannelCreate::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + XRPAmount::from("10000"), + "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + 86400, + "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", + None, + None, + None, + None, + None, + Some(11747), + None, + None, + None, + None, + Some(533171558), + Some(23480), + ); + let default_json = r#"{"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","TransactionType":"PaymentChannelCreate","Amount":"10000","Destination":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","SettleDelay":86400,"PublicKey":"32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A","CancelAfter":533171558,"DestinationTag":23480,"SourceTag":11747}"#; + + let txn_as_obj: PaymentChannelCreate = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/payment_channel_fund.rs b/src/models/transactions/payment_channel_fund.rs new file mode 100644 index 00000000..4f9c3a34 --- /dev/null +++ b/src/models/transactions/payment_channel_fund.rs @@ -0,0 +1,211 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::{ + amount::XRPAmount, + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Add additional XRP to an open payment channel, +/// and optionally update the expiration time of the channel. +/// +/// See PaymentChannelFund: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct PaymentChannelFund<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::payment_channel_fund")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the PaymentChannelFund model. + /// + /// See PaymentChannelFund fields: + /// `` + pub amount: XRPAmount<'a>, + pub channel: &'a str, + pub expiration: Option, +} + +impl<'a> Default for PaymentChannelFund<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::PaymentChannelFund, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + amount: Default::default(), + channel: Default::default(), + expiration: Default::default(), + } + } +} + +impl<'a> Model for PaymentChannelFund<'a> {} + +impl<'a> Transaction for PaymentChannelFund<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> PaymentChannelFund<'a> { + fn new( + account: &'a str, + channel: &'a str, + amount: XRPAmount<'a>, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + expiration: Option, + ) -> Self { + Self { + transaction_type: TransactionType::PaymentChannelFund, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + amount, + channel, + expiration, + } + } +} + +#[cfg(test)] +mod test_serde { + use crate::models::amount::XRPAmount; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = PaymentChannelFund::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", + XRPAmount::from("200000"), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(543171558), + ); + let default_json = r#"{"TransactionType":"PaymentChannelFund","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Amount":"200000","Channel":"C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198","Expiration":543171558}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = PaymentChannelFund::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", + XRPAmount::from("200000"), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(543171558), + ); + let default_json = r#"{"Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","TransactionType":"PaymentChannelFund","Channel":"C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198","Amount":"200000","Expiration":543171558}"#; + + let txn_as_obj: PaymentChannelFund = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/pseudo_transactions/enable_amendment.rs b/src/models/transactions/pseudo_transactions/enable_amendment.rs new file mode 100644 index 00000000..a7476a73 --- /dev/null +++ b/src/models/transactions/pseudo_transactions/enable_amendment.rs @@ -0,0 +1,130 @@ +use crate::_serde::txn_flags; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Flag, Transaction, TransactionType}, +}; + +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum EnableAmendmentFlag { + /// Support for this amendment increased to at least 80% of trusted + /// validators starting with this ledger version. + TfGotMajority = 0x00010000, + /// Support for this amendment decreased to less than 80% of trusted + /// validators starting with this ledger version. + TfLostMajority = 0x00020000, +} + +/// See EnableAmendment: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct EnableAmendment<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::enable_amendment")] + transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// The custom fields for the EnableAmendment model. + /// + /// See EnableAmendment fields: + /// `` + pub amendment: &'a str, + pub ledger_sequence: u32, +} + +impl<'a> Model for EnableAmendment<'a> {} + +impl<'a> Transaction for EnableAmendment<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + match flag { + Flag::EnableAmendment(enable_amendment_flag) => match enable_amendment_flag { + EnableAmendmentFlag::TfGotMajority => self + .flags + .as_ref() + .unwrap() + .contains(&EnableAmendmentFlag::TfGotMajority), + EnableAmendmentFlag::TfLostMajority => self + .flags + .as_ref() + .unwrap() + .contains(&EnableAmendmentFlag::TfLostMajority), + }, + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> EnableAmendment<'a> { + fn new( + account: &'a str, + amendment: &'a str, + ledger_sequence: u32, + fee: Option>, + sequence: Option, + signing_pub_key: Option<&'a str>, + source_tag: Option, + txn_signature: Option<&'a str>, + flags: Option>, + ) -> Self { + Self { + transaction_type: TransactionType::EnableAmendment, + account, + fee, + sequence, + signing_pub_key, + source_tag, + txn_signature, + flags, + amendment, + ledger_sequence, + } + } +} diff --git a/src/models/transactions/pseudo_transactions/mod.rs b/src/models/transactions/pseudo_transactions/mod.rs new file mode 100644 index 00000000..7a0d52b2 --- /dev/null +++ b/src/models/transactions/pseudo_transactions/mod.rs @@ -0,0 +1,7 @@ +pub mod enable_amendment; +pub mod set_fee; +pub mod unl_modify; + +pub use enable_amendment::*; +pub use set_fee::*; +pub use unl_modify::*; diff --git a/src/models/transactions/pseudo_transactions/set_fee.rs b/src/models/transactions/pseudo_transactions/set_fee.rs new file mode 100644 index 00000000..b1aeced5 --- /dev/null +++ b/src/models/transactions/pseudo_transactions/set_fee.rs @@ -0,0 +1,101 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Transaction, TransactionType}, +}; + +/// See SetFee: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct SetFee<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::set_fee")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// The custom fields for the SetFee model. + /// + /// See SetFee fields: + /// `` + pub base_fee: XRPAmount<'a>, + pub reference_fee_units: u32, + pub reserve_base: u32, + pub reserve_increment: u32, + pub ledger_sequence: u32, +} + +impl<'a> Model for SetFee<'a> {} + +impl<'a> Transaction for SetFee<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> SetFee<'a> { + fn new( + account: &'a str, + base_fee: XRPAmount<'a>, + reference_fee_units: u32, + reserve_base: u32, + reserve_increment: u32, + ledger_sequence: u32, + fee: Option>, + sequence: Option, + signing_pub_key: Option<&'a str>, + source_tag: Option, + txn_signature: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::SetFee, + account, + fee, + sequence, + signing_pub_key, + source_tag, + txn_signature, + flags: None, + base_fee, + reference_fee_units, + reserve_base, + reserve_increment, + ledger_sequence, + } + } +} diff --git a/src/models/transactions/pseudo_transactions/unl_modify.rs b/src/models/transactions/pseudo_transactions/unl_modify.rs new file mode 100644 index 00000000..b9a3d7fd --- /dev/null +++ b/src/models/transactions/pseudo_transactions/unl_modify.rs @@ -0,0 +1,106 @@ +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use crate::models::{ + amount::XRPAmount, + model::Model, + transactions::{Transaction, TransactionType}, +}; + +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum UNLModifyDisabling { + Disable = 0, + Enable = 1, +} + +/// See UNLModify: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct UNLModify<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::unl_modify")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// The custom fields for the UNLModify model. + /// + /// See UNLModify fields: + /// `` + pub ledger_sequence: u32, + pub unlmodify_disabling: UNLModifyDisabling, + pub unlmodify_validator: &'a str, +} + +impl<'a> Model for UNLModify<'a> {} + +impl<'a> Transaction for UNLModify<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> UNLModify<'a> { + fn new( + account: &'a str, + ledger_sequence: u32, + unlmodify_disabling: UNLModifyDisabling, + unlmodify_validator: &'a str, + fee: Option>, + sequence: Option, + signing_pub_key: Option<&'a str>, + source_tag: Option, + txn_signature: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::UNLModify, + account, + fee, + sequence, + signing_pub_key, + source_tag, + txn_signature, + flags: None, + ledger_sequence, + unlmodify_disabling, + unlmodify_validator, + } + } +} diff --git a/src/models/transactions/set_regular_key.rs b/src/models/transactions/set_regular_key.rs new file mode 100644 index 00000000..38697431 --- /dev/null +++ b/src/models/transactions/set_regular_key.rs @@ -0,0 +1,200 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// You can protect your account by assigning a regular key pair to +/// it and using it instead of the master key pair to sign transactions +/// whenever possible. If your regular key pair is compromised, but +/// your master key pair is not, you can use a SetRegularKey transaction +/// to regain control of your account. +/// +/// See SetRegularKey: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct SetRegularKey<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::set_regular_key")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the SetRegularKey model. + /// + /// See SetRegularKey fields: + /// `` + pub regular_key: Option<&'a str>, +} + +impl<'a> Default for SetRegularKey<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::SetRegularKey, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + regular_key: Default::default(), + } + } +} + +impl<'a> Model for SetRegularKey<'a> {} + +impl<'a> Transaction for SetRegularKey<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> SetRegularKey<'a> { + fn new( + account: &'a str, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + regular_key: Option<&'a str>, + ) -> Self { + Self { + transaction_type: TransactionType::SetRegularKey, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + regular_key, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = SetRegularKey::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"), + ); + let default_json = r#"{"TransactionType":"SetRegularKey","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"12","RegularKey":"rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = SetRegularKey::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"), + ); + let default_json = r#"{"TransactionType":"SetRegularKey","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"12","RegularKey":"rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"}"#; + + let txn_as_obj: SetRegularKey = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/signer_list_set.rs b/src/models/transactions/signer_list_set.rs new file mode 100644 index 00000000..8223c03e --- /dev/null +++ b/src/models/transactions/signer_list_set.rs @@ -0,0 +1,498 @@ +use alloc::borrow::Cow; +use alloc::vec::Vec; +use anyhow::Result; +use derive_new::new; +use serde::{ser::SerializeMap, Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use alloc::string::ToString; + +use crate::models::transactions::XRPLSignerListSetException; +use crate::models::{ + amount::XRPAmount, + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; +use crate::{serde_with_tag, Err}; + +serde_with_tag! { + #[derive(Debug, PartialEq, Eq, Default, Clone, new)] + #[skip_serializing_none] + pub struct SignerEntry { + pub account: Cow<'static, str>, + pub signer_weight: u16, + } +} + +/// The SignerList object type represents a list of parties that, +/// as a group, are authorized to sign a transaction in place of an +/// individual account. You can create, replace, or remove a signer +/// list using a SignerListSet transaction. +/// +/// See TicketCreate: +/// `` +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +#[skip_serializing_none] +pub struct SignerListSet<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::signer_list_set")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the TicketCreate model. + /// + /// See TicketCreate fields: + /// `` + pub signer_quorum: u32, + pub signer_entries: Option>, +} + +impl<'a> Default for SignerListSet<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::SignerListSet, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + signer_quorum: Default::default(), + signer_entries: Default::default(), + } + } +} + +impl<'a> Model for SignerListSet<'a> { + fn get_errors(&self) -> Result<()> { + match self._get_signer_entries_error() { + Err(error) => Err!(error), + Ok(_no_error) => match self._get_signer_quorum_error() { + Err(error) => Err!(error), + Ok(_no_error) => Ok(()), + }, + } + } +} + +impl<'a> Transaction for SignerListSet<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> SignerListSetError for SignerListSet<'a> { + fn _get_signer_entries_error(&self) -> Result<(), XRPLSignerListSetException> { + if let Some(signer_entries) = &self.signer_entries { + if self.signer_quorum == 0 { + Err(XRPLSignerListSetException::ValueCausesValueDeletion { + field1: "signer_entries", + field2: "signer_quorum", + resource: "", + }) + } else if signer_entries.is_empty() { + Err(XRPLSignerListSetException::CollectionTooFewItems { + field: "signer_entries", + min: 1_usize, + found: signer_entries.len(), + resource: "", + }) + } else if signer_entries.len() > 8 { + Err(XRPLSignerListSetException::CollectionTooManyItems { + field: "signer_entries", + max: 8_usize, + found: signer_entries.len(), + resource: "", + }) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + fn _get_signer_quorum_error(&self) -> Result<(), XRPLSignerListSetException> { + let mut accounts = Vec::new(); + let mut signer_weight_sum: u32 = 0; + if self.signer_entries.is_some() { + for signer_entry in self.signer_entries.as_ref().unwrap() { + accounts.push(&signer_entry.account); + let weight: u32 = signer_entry.signer_weight.into(); + signer_weight_sum += weight; + } + } + accounts.sort_unstable(); + let mut check_account = Vec::new(); + for account in accounts.clone() { + if check_account.contains(&account) { + return Err(XRPLSignerListSetException::CollectionItemDuplicate { + field: "signer_entries", + found: account, + resource: "", + }); + } else { + check_account.push(account); + } + } + if let Some(_signer_entries) = &self.signer_entries { + if accounts.contains(&&Cow::Borrowed(self.account)) { + Err(XRPLSignerListSetException::CollectionInvalidItem { + field: "signer_entries", + found: self.account, + resource: "", + }) + } else if self.signer_quorum > signer_weight_sum { + Err( + XRPLSignerListSetException::SignerQuorumExceedsSignerWeight { + max: signer_weight_sum, + found: self.signer_quorum, + resource: "", + }, + ) + } else { + Ok(()) + } + } else if self.signer_quorum != 0 { + Err(XRPLSignerListSetException::InvalidValueForValueDeletion { + field: "signer_quorum", + expected: 0, + found: self.signer_quorum, + resource: "", + }) + } else { + Ok(()) + } + } +} + +impl<'a> SignerListSet<'a> { + fn new( + account: &'a str, + signer_quorum: u32, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + signer_entries: Option>, + ) -> Self { + Self { + transaction_type: TransactionType::SignerListSet, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + signer_quorum, + signer_entries, + } + } +} + +pub trait SignerListSetError { + fn _get_signer_entries_error(&self) -> Result<(), XRPLSignerListSetException>; + fn _get_signer_quorum_error(&self) -> Result<(), XRPLSignerListSetException>; +} + +#[cfg(test)] +mod test_signer_list_set_error { + use alloc::borrow::Cow::Borrowed; + use alloc::string::ToString; + use alloc::vec; + + use crate::models::Model; + + use super::*; + + #[test] + fn test_signer_list_deleted_error() { + let mut signer_list_set = SignerListSet { + transaction_type: TransactionType::SignerListSet, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + signer_quorum: 0, + signer_entries: Some(vec![SignerEntry { + account: Borrowed("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), + signer_weight: 2, + }]), + }; + + assert_eq!( + signer_list_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `signer_entries` can not be defined with the field `signer_quorum` because it would cause the deletion of `signer_entries`. For more information see: " + ); + + signer_list_set.signer_quorum = 3; + signer_list_set.signer_entries = None; + + assert_eq!( + signer_list_set.validate().unwrap_err().to_string().as_str(), + "The field `signer_quorum` has the wrong value to be deleted (expected 0, found 3). For more information see: " + ); + } + + #[test] + fn test_signer_entries_error() { + let mut signer_list_set = SignerListSet { + transaction_type: TransactionType::SignerListSet, + account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb", + fee: None, + sequence: None, + last_ledger_sequence: None, + account_txn_id: None, + signing_pub_key: None, + source_tag: None, + ticket_sequence: None, + txn_signature: None, + flags: None, + memos: None, + signers: None, + signer_quorum: 3, + signer_entries: Some(vec![]), + }; + + assert_eq!( + signer_list_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `signer_entries` has too few items in it (min 1, found 0). For more information see: " + ); + + signer_list_set.signer_entries = Some(vec![ + SignerEntry { + account: Borrowed("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), + signer_weight: 1, + }, + SignerEntry { + account: Borrowed("rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v"), + signer_weight: 1, + }, + SignerEntry { + account: Borrowed("rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v"), + signer_weight: 2, + }, + SignerEntry { + account: Borrowed("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"), + signer_weight: 2, + }, + SignerEntry { + account: Borrowed("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), + signer_weight: 1, + }, + SignerEntry { + account: Borrowed("rXTZ5g8X7mrAYEe7iFeM9fiS4ccueyurG"), + signer_weight: 1, + }, + SignerEntry { + account: Borrowed("rPbMHxs7vy5t6e19tYfqG7XJ6Fog8EPZLk"), + signer_weight: 2, + }, + SignerEntry { + account: Borrowed("r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W"), + signer_weight: 3, + }, + SignerEntry { + account: Borrowed("rchGBxcD1A1C2tdxF6papQYZ8kjRKMYcL"), + signer_weight: 2, + }, + ]); + + assert_eq!( + signer_list_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `signer_entries` has too many items in it (max 8, found 9). For more information see: " + ); + + signer_list_set.signer_entries = Some(vec![ + SignerEntry { + account: Borrowed("rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb"), + signer_weight: 1, + }, + SignerEntry { + account: Borrowed("rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v"), + signer_weight: 2, + }, + SignerEntry { + account: Borrowed("rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"), + signer_weight: 2, + }, + ]); + + assert_eq!( + signer_list_set.validate().unwrap_err().to_string().as_str(), + "The field `signer_entries` contains an invalid value (found rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb). For more information see: " + ); + + signer_list_set.signer_entries = Some(vec![SignerEntry { + account: Borrowed("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), + signer_weight: 3, + }]); + signer_list_set.signer_quorum = 10; + + assert_eq!( + signer_list_set.validate().unwrap_err().to_string().as_str(), + "The field `signer_quorum` must be below or equal to the sum of `signer_weight` in `signer_entries`. For more information see: " + ); + + signer_list_set.signer_entries = Some(vec![ + SignerEntry { + account: Borrowed("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), + signer_weight: 3, + }, + SignerEntry { + account: Borrowed("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), + signer_weight: 2, + }, + ]); + signer_list_set.signer_quorum = 2; + + assert_eq!( + signer_list_set.validate().unwrap_err().to_string().as_str(), + "The value of the field `signer_entries` has a duplicate in it (found rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW). For more information see: " + ); + } +} + +#[cfg(test)] +mod test_serde { + use alloc::borrow::Cow::Borrowed; + use alloc::vec; + + use super::*; + + #[test] + fn test_serialize() { + let default_txn = SignerListSet::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 3, + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(vec![ + SignerEntry::new(Borrowed("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), 2), + SignerEntry::new(Borrowed("rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v"), 1), + SignerEntry::new(Borrowed("raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n"), 1), + ]), + ); + let default_json = r#"{"TransactionType":"SignerListSet","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"12","Sequence":null,"LastLedgerSequence":null,"AccountTxnID":null,"SigningPubKey":null,"SourceTag":null,"TicketSequence":null,"TxnSignature":null,"Flags":null,"Memos":null,"Signers":null,"SignerQuorum":3,"SignerEntries":[{"SignerEntry":{"Account":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","SignerWeight":2}},{"SignerEntry":{"Account":"rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v","SignerWeight":1}},{"SignerEntry":{"Account":"raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n","SignerWeight":1}}]}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = SignerListSet::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 3, + Some("12".into()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(vec![ + SignerEntry::new(Borrowed("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"), 2), + SignerEntry::new(Borrowed("rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v"), 1), + SignerEntry::new(Borrowed("raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n"), 1), + ]), + ); + let default_json = r#"{"TransactionType":"SignerListSet","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"12","SignerQuorum":3,"SignerEntries":[{"SignerEntry":{"Account":"rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW","SignerWeight":2}},{"SignerEntry":{"Account":"rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v","SignerWeight":1}},{"SignerEntry":{"Account":"raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n","SignerWeight":1}}]}"#; + + let txn_as_obj: SignerListSet = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/ticket_create.rs b/src/models/transactions/ticket_create.rs new file mode 100644 index 00000000..2efb723d --- /dev/null +++ b/src/models/transactions/ticket_create.rs @@ -0,0 +1,196 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::models::amount::XRPAmount; +use crate::models::{ + model::Model, + transactions::{Memo, Signer, Transaction, TransactionType}, +}; + +/// Sets aside one or more sequence numbers as Tickets. +/// +/// See TicketCreate: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct TicketCreate<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::ticket_create")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + pub flags: Option, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the TicketCreate model. + /// + /// See TicketCreate fields: + /// `` + pub ticket_count: u32, +} + +impl<'a> Default for TicketCreate<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::TicketCreate, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + ticket_count: Default::default(), + } + } +} + +impl<'a> Model for TicketCreate<'a> {} + +impl<'a> Transaction for TicketCreate<'a> { + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> TicketCreate<'a> { + fn new( + account: &'a str, + ticket_count: u32, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + memos: Option>>, + signers: Option>>, + ) -> Self { + Self { + transaction_type: TransactionType::TicketCreate, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags: None, + memos, + signers, + ticket_count, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + + #[test] + fn test_serialize() { + let default_txn = TicketCreate::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 10, + Some("10".into()), + Some(381), + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"TicketCreate","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"10","Sequence":381,"TicketCount":10}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = TicketCreate::new( + "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + 10, + Some("10".into()), + Some(381), + None, + None, + None, + None, + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"TicketCreate","Account":"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn","Fee":"10","Sequence":381,"TicketCount":10}"#; + + let txn_as_obj: TicketCreate = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/transactions/trust_set.rs b/src/models/transactions/trust_set.rs new file mode 100644 index 00000000..5b0727d8 --- /dev/null +++ b/src/models/transactions/trust_set.rs @@ -0,0 +1,268 @@ +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::skip_serializing_none; +use strum_macros::{AsRefStr, Display, EnumIter}; + +use crate::models::{ + model::Model, + transactions::{Flag, Memo, Signer, Transaction, TransactionType}, +}; + +use crate::_serde::txn_flags; +use crate::models::amount::{IssuedCurrencyAmount, XRPAmount}; + +/// Transactions of the TrustSet type support additional values +/// in the Flags field. This enum represents those options. +/// +/// See TrustSet flags: +#[derive( + Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, +)] +#[repr(u32)] +pub enum TrustSetFlag { + /// Authorize the other party to hold currency issued by this account. + /// (No effect unless using the asfRequireAuth AccountSet flag.) Cannot be unset. + TfSetAuth = 0x00010000, + /// Enable the No Ripple flag, which blocks rippling between two trust lines + /// of the same currency if this flag is enabled on both. + TfSetNoRipple = 0x00020000, + /// Disable the No Ripple flag, allowing rippling on this trust line.) + TfClearNoRipple = 0x00040000, + /// Freeze the trust line. + TfSetFreeze = 0x00100000, + /// Unfreeze the trust line. + TfClearFreeze = 0x00200000, +} + +/// Create or modify a trust line linking two accounts. +/// +/// See TrustSet: +/// `` +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct TrustSet<'a> { + // The base fields for all transaction models. + // + // See Transaction Types: + // `` + // + // See Transaction Common Fields: + // `` + /// The type of transaction. + #[serde(default = "TransactionType::trust_set")] + pub transaction_type: TransactionType, + /// The unique address of the account that initiated the transaction. + pub account: &'a str, + /// Integer amount of XRP, in drops, to be destroyed as a cost + /// for distributing this transaction to the network. Some + /// transaction types have different minimum requirements. + /// See Transaction Cost for details. + pub fee: Option>, + /// The sequence number of the account sending the transaction. + /// A transaction is only valid if the Sequence number is exactly + /// 1 greater than the previous transaction from the same account. + /// The special case 0 means the transaction is using a Ticket instead. + pub sequence: Option, + /// Highest ledger index this transaction can appear in. + /// Specifying this field places a strict upper limit on how long + /// the transaction can wait to be validated or rejected. + /// See Reliable Transaction Submission for more details. + pub last_ledger_sequence: Option, + /// Hash value identifying another transaction. If provided, this + /// transaction is only valid if the sending account's + /// previously-sent transaction matches the provided hash. + #[serde(rename = "AccountTxnID")] + pub account_txn_id: Option<&'a str>, + /// Hex representation of the public key that corresponds to the + /// private key used to sign this transaction. If an empty string, + /// indicates a multi-signature is present in the Signers field instead. + pub signing_pub_key: Option<&'a str>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction + /// is made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub source_tag: Option, + /// The sequence number of the ticket to use in place + /// of a Sequence number. If this is provided, Sequence must + /// be 0. Cannot be used with AccountTxnID. + pub ticket_sequence: Option, + /// The signature that verifies this transaction as originating + /// from the account it says it is from. + pub txn_signature: Option<&'a str>, + /// Set of bit-flags for this transaction. + #[serde(default)] + #[serde(with = "txn_flags")] + pub flags: Option>, + /// Additional arbitrary information used to identify this transaction. + pub memos: Option>>, + /// Arbitrary integer used to identify the reason for this + /// payment, or a sender on whose behalf this transaction is + /// made. Conventionally, a refund should specify the initial + /// payment's SourceTag as the refund payment's DestinationTag. + pub signers: Option>>, + /// The custom fields for the TrustSet model. + /// + /// See TrustSet fields: + /// `` + pub limit_amount: IssuedCurrencyAmount<'a>, + pub quality_in: Option, + pub quality_out: Option, +} + +impl<'a> Default for TrustSet<'a> { + fn default() -> Self { + Self { + transaction_type: TransactionType::TrustSet, + account: Default::default(), + fee: Default::default(), + sequence: Default::default(), + last_ledger_sequence: Default::default(), + account_txn_id: Default::default(), + signing_pub_key: Default::default(), + source_tag: Default::default(), + ticket_sequence: Default::default(), + txn_signature: Default::default(), + flags: Default::default(), + memos: Default::default(), + signers: Default::default(), + limit_amount: Default::default(), + quality_in: Default::default(), + quality_out: Default::default(), + } + } +} + +impl<'a> Model for TrustSet<'a> {} + +impl<'a> Transaction for TrustSet<'a> { + fn has_flag(&self, flag: &Flag) -> bool { + let mut flags = &Vec::new(); + + if let Some(flag_set) = self.flags.as_ref() { + flags = flag_set; + } + + match flag { + Flag::TrustSet(trust_set_flag) => match trust_set_flag { + TrustSetFlag::TfClearFreeze => flags.contains(&TrustSetFlag::TfClearFreeze), + TrustSetFlag::TfClearNoRipple => flags.contains(&TrustSetFlag::TfClearNoRipple), + TrustSetFlag::TfSetAuth => flags.contains(&TrustSetFlag::TfSetAuth), + TrustSetFlag::TfSetFreeze => flags.contains(&TrustSetFlag::TfSetFreeze), + TrustSetFlag::TfSetNoRipple => flags.contains(&TrustSetFlag::TfSetNoRipple), + }, + _ => false, + } + } + + fn get_transaction_type(&self) -> TransactionType { + self.transaction_type.clone() + } +} + +impl<'a> TrustSet<'a> { + fn new( + account: &'a str, + limit_amount: IssuedCurrencyAmount<'a>, + fee: Option>, + sequence: Option, + last_ledger_sequence: Option, + account_txn_id: Option<&'a str>, + signing_pub_key: Option<&'a str>, + source_tag: Option, + ticket_sequence: Option, + txn_signature: Option<&'a str>, + flags: Option>, + memos: Option>>, + signers: Option>>, + quality_in: Option, + quality_out: Option, + ) -> Self { + Self { + transaction_type: TransactionType::TrustSet, + account, + fee, + sequence, + last_ledger_sequence, + account_txn_id, + signing_pub_key, + source_tag, + ticket_sequence, + txn_signature, + flags, + memos, + signers, + limit_amount, + quality_in, + quality_out, + } + } +} + +#[cfg(test)] +mod test_serde { + use super::*; + use alloc::vec; + + #[test] + fn test_serialize() { + let default_txn = TrustSet::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + IssuedCurrencyAmount::new( + "USD".into(), + "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc".into(), + "100".into(), + ), + Some("12".into()), + Some(12), + Some(8007750), + None, + None, + None, + None, + None, + Some(vec![TrustSetFlag::TfClearNoRipple]), + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"TrustSet","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Fee":"12","Sequence":12,"LastLedgerSequence":8007750,"Flags":262144,"LimitAmount":{"currency":"USD","issuer":"rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc","value":"100"}}"#; + + let txn_as_string = serde_json::to_string(&default_txn).unwrap(); + let txn_json = txn_as_string.as_str(); + + assert_eq!(txn_json, default_json); + } + + #[test] + fn test_deserialize() { + let default_txn = TrustSet::new( + "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + IssuedCurrencyAmount::new( + "USD".into(), + "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc".into(), + "100".into(), + ), + Some("12".into()), + Some(12), + Some(8007750), + None, + None, + None, + None, + None, + Some(vec![TrustSetFlag::TfClearNoRipple]), + None, + None, + None, + None, + ); + let default_json = r#"{"TransactionType":"TrustSet","Account":"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX","Fee":"12","Flags":262144,"LastLedgerSequence":8007750,"LimitAmount":{"currency":"USD","issuer":"rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc","value":"100"},"Sequence":12}"#; + + let txn_as_obj: TrustSet = serde_json::from_str(default_json).unwrap(); + + assert_eq!(txn_as_obj, default_txn); + } +} diff --git a/src/models/utils.rs b/src/models/utils.rs index 10dda9bb..cf1c177c 100644 --- a/src/models/utils.rs +++ b/src/models/utils.rs @@ -1 +1,22 @@ //! Helper util functions for the models module. +use crate::models::exceptions::JSONRPCException; +use alloc::string::String; +use serde::{Deserialize, Serialize}; + +/// JSONRPC Request +#[derive(Debug, Clone, Serialize)] +pub struct Request { + pub method: String, + pub params: Option, + pub id: serde_json::Value, + pub jsonrpc: Option, +} + +/// JSONRPC Response +#[derive(Debug, Clone, Deserialize)] +pub struct Response { + pub id: serde_json::Value, + pub result: Option, + pub error: Option, + pub jsonrpc: Option, +} diff --git a/src/utils/exceptions.rs b/src/utils/exceptions.rs index 914c9456..ad034724 100644 --- a/src/utils/exceptions.rs +++ b/src/utils/exceptions.rs @@ -1,14 +1,16 @@ //! Exception for invalid XRP Ledger amount data. use alloc::string::String; +use strum_macros::Display; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] pub enum XRPLTimeRangeException { InvalidTimeBeforeEpoch { min: i64, found: i64 }, UnexpectedTimeOverflow { max: i64, found: i64 }, + InvalidLocalTime, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] pub enum XRPRangeException { InvalidXRPAmount, @@ -25,7 +27,7 @@ pub enum XRPRangeException { DecimalError(rust_decimal::Error), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] pub enum ISOCodeException { InvalidISOCode, @@ -41,7 +43,7 @@ pub enum ISOCodeException { DecimalError(rust_decimal::Error), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Display)] #[non_exhaustive] pub enum JSONParseException { ISOCodeError(ISOCodeException), @@ -101,23 +103,8 @@ impl From for JSONParseException { } } -impl core::fmt::Display for XRPRangeException { - fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { - write!(f, "XRPRangeException: {:?}", self) - } -} - -impl core::fmt::Display for ISOCodeException { - fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { - write!(f, "ISOCodeException: {:?}", self) - } -} - -impl core::fmt::Display for XRPLTimeRangeException { - fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { - write!(f, "XRPLTimeRangeException: {:?}", self) - } -} +#[cfg(feature = "std")] +impl alloc::error::Error for XRPLTimeRangeException {} #[cfg(feature = "std")] impl alloc::error::Error for XRPRangeException {} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c18a644c..41508f7c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -7,7 +7,7 @@ pub mod xrpl_conversion; pub use self::time_conversion::*; pub use self::xrpl_conversion::*; -use crate::constants::HEX_CURRENCY_REGEX; +use crate::constants::*; use alloc::vec::Vec; use regex::Regex; @@ -29,6 +29,42 @@ pub fn is_hex_address(value: &str) -> bool { regex.is_match(value) } +/// Tests if value is a valid 3-char iso code. +/// +/// # Examples +/// +/// ## Basic usage +/// +/// ``` +/// use xrpl::utils::is_iso_code; +/// +/// let value: &str = "USD"; +/// +/// assert!(is_iso_code(value)); +/// ``` +pub fn is_iso_code(value: &str) -> bool { + let regex = Regex::new(ISO_CURRENCY_REGEX).expect("is_iso_code"); + regex.is_match(value) +} + +/// Tests if value is a valid 40-char hex currency string. +/// +/// # Examples +/// +/// ## Basic usage +/// +/// ``` +/// use xrpl::utils::is_iso_hex; +/// +/// let value: &str = "0000000000000000000000005553440000000000"; +/// +/// assert!(is_iso_hex(value)); +/// ``` +pub fn is_iso_hex(value: &str) -> bool { + let regex = Regex::new(HEX_CURRENCY_REGEX).expect("_is_hex"); + regex.is_match(value) +} + /// Converter to byte array with endianness. pub trait ToBytes { /// Return the byte array of self. @@ -45,4 +81,31 @@ mod test { fn test_is_hex_address() { assert!(is_hex_address(HEX_ENCODING)); } + + #[test] + fn test_is_iso_code() { + let valid_code = "ABC"; + let valid_code_numeric = "123"; + let invalid_code_long = "LONG"; + let invalid_code_short = "NO"; + + assert!(is_iso_code(valid_code)); + assert!(is_iso_code(valid_code_numeric)); + assert!(!is_iso_code(invalid_code_long)); + assert!(!is_iso_code(invalid_code_short)); + } + + #[test] + fn test_is_hex() { + // Valid = 40 char length and only valid hex chars + let valid_hex: &str = "0000000000000000000000005553440000000000"; + let invalid_hex_chars: &str = "USD0000000000000000000005553440000000000"; + let invalid_hex_long: &str = "0000000000000000000000005553440000000000123455"; + let invalid_hex_short: &str = "1234"; + + assert!(is_iso_hex(valid_hex)); + assert!(!is_iso_hex(invalid_hex_long)); + assert!(!is_iso_hex(invalid_hex_short)); + assert!(!is_iso_hex(invalid_hex_chars)); + } } diff --git a/src/utils/time_conversion.rs b/src/utils/time_conversion.rs index e15e5da0..f20d403d 100644 --- a/src/utils/time_conversion.rs +++ b/src/utils/time_conversion.rs @@ -2,9 +2,9 @@ //! data types. use crate::utils::exceptions::XRPLTimeRangeException; -use chrono::DateTime; use chrono::TimeZone; use chrono::Utc; +use chrono::{DateTime, LocalResult}; /// The "Ripple Epoch" of 2000-01-01T00:00:00 UTC pub const RIPPLE_EPOCH: i64 = 946684800; @@ -32,7 +32,11 @@ fn _ripple_check_max(time: i64, ok: T) -> Result { pub(crate) fn ripple_time_to_datetime( ripple_time: i64, ) -> Result, XRPLTimeRangeException> { - _ripple_check_max(ripple_time, Utc.timestamp(ripple_time + RIPPLE_EPOCH, 0)) + let datetime = Utc.timestamp_opt(ripple_time + RIPPLE_EPOCH, 0); + match datetime { + LocalResult::Single(dt) => _ripple_check_max(ripple_time, dt), + _ => Err(XRPLTimeRangeException::InvalidLocalTime), + } } /// Convert from a [`chrono::DateTime`] object to an XRP Ledger diff --git a/src/utils/xrpl_conversion.rs b/src/utils/xrpl_conversion.rs index afa87eef..2376aba3 100644 --- a/src/utils/xrpl_conversion.rs +++ b/src/utils/xrpl_conversion.rs @@ -35,15 +35,13 @@ fn _calculate_precision(value: &str) -> Result { if decimal.checked_rem(Decimal::ONE).is_some() { let stripped = regex .replace(&decimal.to_string(), "") - .replace('.', "") - .replace('0', ""); + .replace(['.', '0'], ""); Ok(stripped.len()) } else { let quantized = decimal.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero); let stripped = regex .replace(&quantized.to_string(), "") - .replace('.', "") - .replace('0', ""); + .replace(['.', '0'], ""); Ok(stripped.len()) } } @@ -51,18 +49,16 @@ fn _calculate_precision(value: &str) -> Result { /// Ensure that the value after being multiplied by the /// exponent does not contain a decimal. fn _verify_no_decimal(decimal: Decimal) -> Result<(), XRPRangeException> { - let value: String; let decimal = Decimal::from_u32(decimal.scale()).expect("_verify_no_decimal"); - if decimal == Decimal::ZERO { - value = decimal.mantissa().to_string(); + let value: String = if decimal == Decimal::ZERO { + decimal.mantissa().to_string() } else { - value = decimal + decimal .checked_mul(decimal) - .or(Some(Decimal::ZERO)) - .unwrap() - .to_string(); - } + .unwrap_or(Decimal::ZERO) + .to_string() + }; if value.contains('.') { Err(XRPRangeException::InvalidValueContainsDecimal) @@ -221,16 +217,16 @@ pub fn verify_valid_ic_value(ic_value: &str) -> Result<(), XRPRangeException> { match decimal { ic if ic.is_zero() => Ok(()), - _ if prec > MAX_IOU_PRECISION as usize || scale > MAX_IOU_EXPONENT as i32 => { + _ if prec > MAX_IOU_PRECISION as usize || scale > MAX_IOU_EXPONENT => { Err(XRPRangeException::InvalidICPrecisionTooLarge { - max: MAX_IOU_EXPONENT as i32, + max: MAX_IOU_EXPONENT, found: scale, }) } - _ if prec > MAX_IOU_PRECISION as usize || scale < MIN_IOU_EXPONENT as i32 => { + _ if prec > MAX_IOU_PRECISION as usize || scale < MIN_IOU_EXPONENT => { Err(XRPRangeException::InvalidICPrecisionTooSmall { - min: MIN_IOU_EXPONENT as i32, - found: scale as i32, + min: MIN_IOU_EXPONENT, + found: scale, }) } _ => _verify_no_decimal(decimal), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 5864d07a..0d36f11c 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -7,11 +7,11 @@ use crate::core::keypairs::derive_classic_address; use crate::core::keypairs::derive_keypair; use crate::core::keypairs::exceptions::XRPLKeypairsException; use crate::core::keypairs::generate_seed; -use alloc::borrow::Cow; use alloc::format; use alloc::string::String; use alloc::string::ToString; use alloc::vec; +use zeroize::Zeroize; /// The cryptographic keys needed to control an /// XRP Ledger account. @@ -21,24 +21,35 @@ use alloc::vec; struct Wallet { /// The seed from which the public and private keys /// are derived. - seed: String, + pub seed: String, /// The public key that is used to identify this wallet's /// signatures, as a hexadecimal string. - public_key: Cow<'static, str>, + pub public_key: String, /// The private key that is used to create signatures, as /// a hexadecimal string. MUST be kept secret! /// /// TODO Use seckey - private_key: Cow<'static, str>, + pub private_key: String, /// The address that publicly identifies this wallet, as /// a base58 string. - classic_address: Cow<'static, str>, + pub classic_address: String, /// The next available sequence number to use for /// transactions from this wallet. Must be updated by the /// user. Increments on the ledger with every successful /// transaction submission, and stays the same with every /// failed transaction submission. - sequence: u64, + pub sequence: u64, +} + +// Zeroize the memory where sensitive data is stored. +impl Drop for Wallet { + fn drop(&mut self) { + self.seed.zeroize(); + self.public_key.zeroize(); + self.private_key.zeroize(); + self.classic_address.zeroize(); + self.sequence.zeroize(); + } } impl Wallet { @@ -49,9 +60,9 @@ impl Wallet { Ok(Wallet { seed: seed.into(), - public_key: public_key.into(), - private_key: private_key.into(), - classic_address: classic_address.into(), + public_key, + private_key, + classic_address, sequence, }) } diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 00000000..7db0151d --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,3 @@ +/// Setup common testing prerequisites here such as connecting a client +/// to a server or creating required files/directories if needed. +pub fn setup() {} diff --git a/tests/test_utils.rs b/tests/test_utils.rs new file mode 100644 index 00000000..eca718e9 --- /dev/null +++ b/tests/test_utils.rs @@ -0,0 +1,11 @@ +use xrpl::utils::{posix_to_ripple_time, ripple_time_to_posix}; + +#[test] +fn it_converts_posix_to_ripple_time() { + assert_eq!(posix_to_ripple_time(1660187459), Ok(713502659_i64)); +} + +#[test] +fn it_converts_ripple_time_to_posix() { + assert_eq!(ripple_time_to_posix(713502659), Ok(1660187459)); +}