From abd901b7f65d7f3fde3f2436b9762c5619cd5a27 Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Mon, 10 Feb 2025 13:29:17 +0100 Subject: [PATCH 1/4] feat(xrp): Add support for signing JSON Transaction --- rust/chains/tw_ripple/src/compiler.rs | 35 +++++--- .../tw_ripple/src/modules/protobuf_builder.rs | 84 +++++++++++++++---- .../src/modules/transaction_signer.rs | 17 ++-- rust/chains/tw_ripple/src/signer.rs | 15 +++- .../src/transaction/json_transaction.rs | 27 ++++++ rust/chains/tw_ripple/src/transaction/mod.rs | 1 + src/proto/Ripple.proto | 9 ++ 7 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 rust/chains/tw_ripple/src/transaction/json_transaction.rs diff --git a/rust/chains/tw_ripple/src/compiler.rs b/rust/chains/tw_ripple/src/compiler.rs index 804b0e140b6..f77d58d8844 100644 --- a/rust/chains/tw_ripple/src/compiler.rs +++ b/rust/chains/tw_ripple/src/compiler.rs @@ -4,7 +4,8 @@ use crate::encode::encode_tx; use crate::modules::protobuf_builder::ProtobufBuilder; -use crate::modules::transaction_signer::{TransactionSigner, TxPreImage}; +use crate::modules::transaction_signer::TransactionSigner; +use crate::transaction::RippleTransaction; use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; use tw_coin_entry::common::compile_input::SingleSignaturePubkey; @@ -31,16 +32,17 @@ impl RippleCompiler { _coin: &dyn CoinContext, input: Proto::SigningInput<'_>, ) -> SigningResult> { - let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; - let TxPreImage { - pre_image_tx_data, - hash_to_sign, - .. - } = TransactionSigner::pre_image(&unsigned_tx)?; + let pre_image = if input.raw_json.is_empty() { + let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; + TransactionSigner::pre_image(&unsigned_tx)? + } else { + let tx_json = ProtobufBuilder::new(&input).build_tx_json()?; + TransactionSigner::pre_image(&tx_json)? + }; Ok(CompilerProto::PreSigningOutput { - data_hash: hash_to_sign.to_vec().into(), - data: pre_image_tx_data.into(), + data_hash: pre_image.hash_to_sign.to_vec().into(), + data: pre_image.pre_image_tx_data.into(), ..CompilerProto::PreSigningOutput::default() }) } @@ -61,6 +63,20 @@ impl RippleCompiler { input: Proto::SigningInput<'_>, signatures: Vec, public_keys: Vec, + ) -> SigningResult> { + if input.raw_json.is_empty() { + let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; + Self::compile_tx(unsigned_tx, signatures, public_keys) + } else { + let json_tx = ProtobufBuilder::new(&input).build_tx_json()?; + Self::compile_tx(json_tx, signatures, public_keys) + } + } + + fn compile_tx( + unsigned_tx: Transaction, + signatures: Vec, + public_keys: Vec, ) -> SigningResult> { let SingleSignaturePubkey { signature, @@ -74,7 +90,6 @@ impl RippleCompiler { .into_tw() .context("Invalid public key")?; - let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; let signed_tx = TransactionSigner::compile(unsigned_tx, &signature, &public_key)?; let signing_only = false; diff --git a/rust/chains/tw_ripple/src/modules/protobuf_builder.rs b/rust/chains/tw_ripple/src/modules/protobuf_builder.rs index 7d57c6e9236..23673a4b997 100644 --- a/rust/chains/tw_ripple/src/modules/protobuf_builder.rs +++ b/rust/chains/tw_ripple/src/modules/protobuf_builder.rs @@ -4,6 +4,7 @@ use crate::address::classic_address::ClassicAddress; use crate::address::RippleAddress; +use crate::transaction::json_transaction::JsonTransaction; use crate::transaction::transaction_builder::TransactionBuilder; use crate::transaction::transaction_type::TransactionType; use crate::types::account_id::AccountId; @@ -14,6 +15,7 @@ use crate::types::currency::Currency; use bigdecimal::BigDecimal; use std::str::FromStr; use tw_coin_entry::error::prelude::*; +use tw_encoding::hex::as_hex::AsHex; use tw_encoding::hex::DecodeHex; use tw_hash::H256; use tw_keypair::ecdsa::secp256k1; @@ -30,6 +32,51 @@ impl<'a> ProtobufBuilder<'a> { ProtobufBuilder { input } } + /// Builds a transaction from `SigningInput.rawJson` JSON object, + /// returns a [`JsonTransaction`] with deserialized [`CommonFields`]. + /// + /// Note [`JsonTransaction`] implements [`RippleTransaction`] + pub fn build_tx_json(self) -> SigningResult { + let mut tx: JsonTransaction = serde_json::from_str(self.input.raw_json.as_ref()) + .tw_err(SigningErrorType::Error_input_parse) + .context("Invalid 'SigningInput.rawJson'")?; + + let expected_signing_pubkey = self.signing_public_key()?; + + // Check whether JSON transaction contains `SigningPubKey` field, otherwise set it. + if tx.common_fields.signing_pub_key.is_none() { + tx.common_fields.signing_pub_key = Some(AsHex(expected_signing_pubkey.compressed())); + } + + // Check whether JSON transaction contains `Account` field, otherwise set it. + if tx.common_fields.account.is_none() { + let address = ClassicAddress::with_public_key(&expected_signing_pubkey) + .into_tw() + .context("Internal: error generating an address for the signing public key")?; + tx.common_fields.account = Some(RippleAddress::Classic(address)); + } + + // Check whether `SigningInput.fee` is specified, or JSON transaction doesn't contain that field, + // then override the field. + if self.input.fee != 0 || tx.common_fields.fee.is_none() { + tx.common_fields.fee = Some(self.fee()?); + } + + // Check whether `SigningInput.sequence` is specified, or JSON transaction doesn't contain that field, + // then override the field. + if self.input.sequence != 0 || tx.common_fields.sequence.is_none() { + tx.common_fields.sequence = Some(self.input.sequence); + } + + // Check whether `SigningInput.last_ledger_sequence` is specified, or JSON transaction doesn't contain that field, + // then override the field. + if self.input.last_ledger_sequence != 0 || tx.common_fields.last_ledger_sequence.is_none() { + tx.common_fields.last_ledger_sequence = Some(self.input.last_ledger_sequence); + } + + Ok(tx) + } + pub fn build_tx(self) -> SigningResult { match self.input.operation_oneof { OperationType::op_payment(ref payment) => self.payment(payment), @@ -235,21 +282,8 @@ impl<'a> ProtobufBuilder<'a> { } pub fn prepare_builder(&self) -> SigningResult { - let signing_public_key = if !self.input.private_key.is_empty() { - secp256k1::PrivateKey::try_from(self.input.private_key.as_ref()) - .into_tw() - .context("Invalid private key")? - .public() - } else if !self.input.public_key.is_empty() { - secp256k1::PublicKey::try_from(self.input.public_key.as_ref()) - .into_tw() - .context("Invalid public key")? - } else { - return SigningError::err(SigningErrorType::Error_invalid_params) - .context("Expected either 'privateKey' or 'publicKey' to be provided"); - }; - - let fee = NativeAmount::new(self.input.fee).context("Invalid fee")?; + let signing_public_key = self.signing_public_key()?; + let fee = self.fee()?; let mut builder = TransactionBuilder::default(); builder @@ -265,6 +299,26 @@ impl<'a> ProtobufBuilder<'a> { Ok(builder) } + fn signing_public_key(&self) -> SigningResult { + if !self.input.private_key.is_empty() { + secp256k1::PrivateKey::try_from(self.input.private_key.as_ref()) + .into_tw() + .context("Invalid private key") + .map(|key| key.public()) + } else if !self.input.public_key.is_empty() { + secp256k1::PublicKey::try_from(self.input.public_key.as_ref()) + .into_tw() + .context("Invalid public key") + } else { + SigningError::err(SigningErrorType::Error_invalid_params) + .context("Expected either 'privateKey' or 'publicKey' to be provided") + } + } + + fn fee(&self) -> SigningResult { + NativeAmount::new(self.input.fee).context("Invalid fee") + } + fn issued_currency(input: &Proto::CurrencyAmount) -> SigningResult { let value = BigDecimal::from_str(input.value.as_ref()) .tw_err(SigningErrorType::Error_invalid_requested_token_amount) diff --git a/rust/chains/tw_ripple/src/modules/transaction_signer.rs b/rust/chains/tw_ripple/src/modules/transaction_signer.rs index 137b1afc1c4..6efa9792dc4 100644 --- a/rust/chains/tw_ripple/src/modules/transaction_signer.rs +++ b/rust/chains/tw_ripple/src/modules/transaction_signer.rs @@ -4,7 +4,6 @@ use crate::address::classic_address::ClassicAddress; use crate::encode::{encode_tx, TxEncoded}; -use crate::transaction::transaction_type::TransactionType; use crate::transaction::RippleTransaction; use serde_json::Value as Json; use tw_coin_entry::error::prelude::*; @@ -26,10 +25,10 @@ pub struct TxPreImage { pub struct TransactionSigner; impl TransactionSigner { - pub fn sign( - tx: TransactionType, + pub fn sign( + tx: Transaction, private_key: &secp256k1::PrivateKey, - ) -> SigningResult { + ) -> SigningResult { let public_key = private_key.public(); Self::check_signing_public_key(&tx, &public_key)?; Self::check_source_account(&tx, &public_key)?; @@ -47,7 +46,9 @@ impl TransactionSigner { Self::compile_unchecked(tx, &signature) } - pub fn pre_image(tx: &TransactionType) -> SigningResult { + pub fn pre_image( + tx: &Transaction, + ) -> SigningResult { let signing_only = true; let TxEncoded { json, encoded } = encode_tx(tx, signing_only)?; let pre_image: Data = NETWORK_PREFIX.iter().copied().chain(encoded).collect(); @@ -62,11 +63,11 @@ impl TransactionSigner { } /// Compiles `signature` into the `transaction` validating the signature. - pub fn compile( - tx: TransactionType, + pub fn compile( + tx: Transaction, signature: &secp256k1::Signature, public_key: &secp256k1::PublicKey, - ) -> SigningResult { + ) -> SigningResult { let TxPreImage { hash_to_sign, .. } = Self::pre_image(&tx)?; Self::check_source_account(&tx, public_key)?; Self::check_signing_public_key(&tx, public_key)?; diff --git a/rust/chains/tw_ripple/src/signer.rs b/rust/chains/tw_ripple/src/signer.rs index f26662e2f26..933cd8b1507 100644 --- a/rust/chains/tw_ripple/src/signer.rs +++ b/rust/chains/tw_ripple/src/signer.rs @@ -5,6 +5,7 @@ use crate::encode::encode_tx; use crate::modules::protobuf_builder::ProtobufBuilder; use crate::modules::transaction_signer::TransactionSigner; +use crate::transaction::RippleTransaction; use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; @@ -26,7 +27,19 @@ impl RippleSigner { _coin: &dyn CoinContext, input: Proto::SigningInput<'_>, ) -> SigningResult> { - let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; + if input.raw_json.is_empty() { + let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; + Self::sign_tx(unsigned_tx, input) + } else { + let json_tx = ProtobufBuilder::new(&input).build_tx_json()?; + Self::sign_tx(json_tx, input) + } + } + + fn sign_tx( + unsigned_tx: Transaction, + input: Proto::SigningInput<'_>, + ) -> SigningResult> { let private_key = secp256k1::PrivateKey::try_from(input.private_key.as_ref()) .into_tw() .context("Invalid private key")?; diff --git a/rust/chains/tw_ripple/src/transaction/json_transaction.rs b/rust/chains/tw_ripple/src/transaction/json_transaction.rs new file mode 100644 index 00000000000..d9a75723a7b --- /dev/null +++ b/rust/chains/tw_ripple/src/transaction/json_transaction.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::ripple_tx; +use crate::transaction::common_fields::CommonFields; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use std::collections::BTreeMap; + +/// `AnyTransaction` designed to facilitate the deserialization, modification, +/// and serialization of XRP Ledger transactions. +/// It allows for the common fields present in all transactions to be accessed and modified directly, +/// while also supporting the storage of additional fields in a dynamic and extensible manner. +/// +/// The `#[serde(flatten)]` attribute is used to merge the fields of `common_fields` and +/// `other_fields` into the top-level structure during serialization and deserialization, +/// enabling seamless handling of transaction data. +#[derive(Serialize, Deserialize)] +pub struct JsonTransaction { + #[serde(flatten)] + pub common_fields: CommonFields, + #[serde(flatten)] + pub other_fields: BTreeMap, +} + +ripple_tx!(JsonTransaction); diff --git a/rust/chains/tw_ripple/src/transaction/mod.rs b/rust/chains/tw_ripple/src/transaction/mod.rs index 89d46b064b8..ff92fd04342 100644 --- a/rust/chains/tw_ripple/src/transaction/mod.rs +++ b/rust/chains/tw_ripple/src/transaction/mod.rs @@ -6,6 +6,7 @@ use serde::de::DeserializeOwned; use serde::Serialize; pub mod common_fields; +pub mod json_transaction; pub mod transaction_builder; pub mod transaction_type; pub mod transactions; diff --git a/src/proto/Ripple.proto b/src/proto/Ripple.proto index 8107d3016ae..4962acac13f 100644 --- a/src/proto/Ripple.proto +++ b/src/proto/Ripple.proto @@ -156,6 +156,15 @@ message SigningInput { // Only used by tss chain-integration. bytes public_key = 15; + // Generate a transaction from its JSON representation. + // The following parameters can be replaced from the `SigningInput` Protobuf: + // * Account + // * SigningPubKey + // * Fee + // * Sequence + // * LastLedgerSequence + string raw_json = 20; + // 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. uint32 source_tag = 25; From 8d3af6b8a7fa3d61d11e2823c636dda1c1b63c0a Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Mon, 10 Feb 2025 17:49:54 +0100 Subject: [PATCH 2/4] feat(xrp): Add `STArray` and `STObject` XRPL types --- rust/chains/tw_ripple/src/encode/mod.rs | 1 + rust/chains/tw_ripple/src/encode/st_array.rs | 41 +++++++++++++++++++ rust/chains/tw_ripple/src/encode/st_object.rs | 8 ++++ .../chains/tw_ripple/src/encode/xrpl_types.rs | 11 ++++- .../src/transaction/common_fields.rs | 3 +- .../src/transaction/transaction_builder.rs | 2 +- .../tw_ripple/tests/data/transactions.json | 27 ++++++++++++ .../tests/chains/ripple/ripple_sign.rs | 37 +++++++++++++++++ 8 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 rust/chains/tw_ripple/src/encode/st_array.rs diff --git a/rust/chains/tw_ripple/src/encode/mod.rs b/rust/chains/tw_ripple/src/encode/mod.rs index 6cff6a4afcc..19880787ac4 100644 --- a/rust/chains/tw_ripple/src/encode/mod.rs +++ b/rust/chains/tw_ripple/src/encode/mod.rs @@ -14,6 +14,7 @@ pub const TRANSACTION_SIGNATURE_PREFIX: i32 = 0x53545800; pub mod encoder; pub mod field_instance; pub mod impls; +pub mod st_array; pub mod st_object; pub mod xrpl_types; diff --git a/rust/chains/tw_ripple/src/encode/st_array.rs b/rust/chains/tw_ripple/src/encode/st_array.rs new file mode 100644 index 00000000000..6f415b25d55 --- /dev/null +++ b/rust/chains/tw_ripple/src/encode/st_array.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::encode::encoder::Encoder; +use crate::encode::st_object::STObject; +use crate::encode::Encodable; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use tw_coin_entry::error::prelude::{ResultContext, SigningError, SigningErrorType, SigningResult}; + +const ARRAY_END_MARKER: u8 = 0xF1; + +/// Class for serializing and deserializing Lists of objects. +/// +/// See Array Fields: +/// `` +#[derive(Debug, Deserialize, Serialize)] +pub struct STArray(Vec); + +impl Encodable for STArray { + fn encode(&self, dst: &mut Encoder) -> SigningResult<()> { + let whether_all_objects = self.0.iter().all(|v| v.is_object()); + + if !self.0.is_empty() && !whether_all_objects { + return SigningError::err(SigningErrorType::Error_input_parse) + .context("STArray is expected to be an array of objects"); + } + + for object in self.0.iter() { + let signing_only = false; + let serialized_object = STObject::try_from_value(object.clone(), signing_only) + .context("Error parsing/serializing STArray")?; + + dst.append_raw_slice(&serialized_object.0); + } + + dst.push_byte(ARRAY_END_MARKER); + Ok(()) + } +} diff --git a/rust/chains/tw_ripple/src/encode/st_object.rs b/rust/chains/tw_ripple/src/encode/st_object.rs index 7ca67671cee..5e0598ddf1a 100644 --- a/rust/chains/tw_ripple/src/encode/st_object.rs +++ b/rust/chains/tw_ripple/src/encode/st_object.rs @@ -8,6 +8,7 @@ use crate::definitions::DEFINITIONS; use crate::encode::encoder::Encoder; use crate::encode::field_instance::FieldInstance; use crate::encode::xrpl_types::XRPLTypes; +use crate::encode::Encodable; use serde_json::{Map as JsonMap, Value as Json}; use std::str::FromStr; use tw_coin_entry::error::prelude::*; @@ -213,3 +214,10 @@ impl STObject { Ok(()) } } + +impl Encodable for STObject { + fn encode(&self, dst: &mut Encoder) -> SigningResult<()> { + dst.append_raw_slice(&self.0); + Ok(()) + } +} diff --git a/rust/chains/tw_ripple/src/encode/xrpl_types.rs b/rust/chains/tw_ripple/src/encode/xrpl_types.rs index 4d3c54c3d26..9d7f8431b14 100644 --- a/rust/chains/tw_ripple/src/encode/xrpl_types.rs +++ b/rust/chains/tw_ripple/src/encode/xrpl_types.rs @@ -3,6 +3,8 @@ // Copyright © 2017 Trust Wallet. use crate::encode::encoder::Encoder; +use crate::encode::st_array::STArray; +use crate::encode::st_object::STObject; use crate::encode::Encodable; use crate::types::account_id::AccountId; use crate::types::amount::Amount; @@ -34,6 +36,8 @@ pub enum XRPLTypes { Hash160(H160), Hash256(H256), Vector256(Vector256), + STArray(STArray), + STObject(STObject), UInt8(u8), UInt16(u16), UInt32(u32), @@ -73,13 +77,14 @@ impl XRPLTypes { } else if value.is_object() { match type_name { "Amount" => Ok(XRPLTypes::Amount(Amount::try_from(value)?)), - // `STObject`, `XChainBridge` types aren't supported yet. + "STObject" => Ok(XRPLTypes::STObject(STObject::try_from_value(value, false)?)), + // `XChainBridge` types isn't supported yet. _ => unsupported_error(type_name), } } else if value.is_array() { match type_name { "Vector256" => Ok(XRPLTypes::Vector256(deserialize_json(value)?)), - // `STObject`, `XChainBridge` types aren't supported yet. + "STArray" => Ok(XRPLTypes::STArray(deserialize_json(value)?)), _ => unsupported_error(type_name), } } else { @@ -99,6 +104,8 @@ impl Encodable for XRPLTypes { XRPLTypes::Hash160(ty) => ty.encode(dst), XRPLTypes::Hash256(ty) => ty.encode(dst), XRPLTypes::Vector256(ty) => ty.encode(dst), + XRPLTypes::STArray(ty) => ty.encode(dst), + XRPLTypes::STObject(ty) => ty.encode(dst), XRPLTypes::UInt8(ty) => ty.encode(dst), XRPLTypes::UInt16(ty) => ty.encode(dst), XRPLTypes::UInt32(ty) => ty.encode(dst), diff --git a/rust/chains/tw_ripple/src/transaction/common_fields.rs b/rust/chains/tw_ripple/src/transaction/common_fields.rs index 38b841d7d46..9619dc6a9e4 100644 --- a/rust/chains/tw_ripple/src/transaction/common_fields.rs +++ b/rust/chains/tw_ripple/src/transaction/common_fields.rs @@ -34,7 +34,8 @@ pub struct CommonFields { #[serde(skip_serializing_if = "Option::is_none")] pub fee: Option, /// Set of bit-flags for this transaction. - pub flags: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub flags: 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. diff --git a/rust/chains/tw_ripple/src/transaction/transaction_builder.rs b/rust/chains/tw_ripple/src/transaction/transaction_builder.rs index 6a76f645bd7..bb61898538f 100644 --- a/rust/chains/tw_ripple/src/transaction/transaction_builder.rs +++ b/rust/chains/tw_ripple/src/transaction/transaction_builder.rs @@ -49,7 +49,7 @@ impl TransactionBuilder { } pub fn flags(&mut self, flags: u32) -> &mut Self { - self.common_fields.flags = flags; + self.common_fields.flags = Some(flags); self } diff --git a/rust/chains/tw_ripple/tests/data/transactions.json b/rust/chains/tw_ripple/tests/data/transactions.json index bed2727e414..67d2d082369 100644 --- a/rust/chains/tw_ripple/tests/data/transactions.json +++ b/rust/chains/tw_ripple/tests/data/transactions.json @@ -252,5 +252,32 @@ "TransactionType": "Payment", "TxnSignature": "304502210091DCA7AF189CD9DC93BDE24DEAE87381FBF16789C43113EE312241D648982B2402201C6055FEFFF1F119640AAC0B32C4F37375B0A96033E0527A21C1366920D6A524" } + }, + { + "description": "Memos is a STArray", + "binary_no_signing": "120007220008000024000003EB2019000003EA201B005EE96764400000000D4D5FFA65D5038D7EA4C68000000000000000000000000000584D4D0000000000A426093A78AA86EB2B878E5C2E33FEC224A0184968400000000000000F8114F990B9E746546554A7B50A5E013BCB57095C6BB8F9EA7C09584D4D2076616C75657D07322E3230393635E1F1", + "json": { + "TakerPays": "223174650", + "Account": "rPk2dXr27rMw9G5Ej9ad2Tt7RJzGy8ycBp", + "TransactionType": "OfferCreate", + "Memos": [ + { + "Memo": { + "MemoType": "584D4D2076616C7565", + "MemoData": "322E3230393635" + } + } + ], + "Fee": "15", + "OfferSequence": 1002, + "TakerGets": { + "currency": "XMM", + "value": "100", + "issuer": "rExAPEZvbkZqYPuNcZ7XEBLENEshsWDQc8" + }, + "Flags": 524288, + "Sequence": 1003, + "LastLedgerSequence": 6220135 + } } ] \ No newline at end of file diff --git a/rust/tw_tests/tests/chains/ripple/ripple_sign.rs b/rust/tw_tests/tests/chains/ripple/ripple_sign.rs index 2c65e4be70d..dae815bdbb0 100644 --- a/rust/tw_tests/tests/chains/ripple/ripple_sign.rs +++ b/rust/tw_tests/tests/chains/ripple/ripple_sign.rs @@ -761,3 +761,40 @@ fn test_ripple_sign_nftoken_cancel_offer() { "12001c220000000024015cc86e201b015cc88368400000000000000a7321022250f103fd045edf2e552df2d20aca01a52dc6aedd522d68767f1c744fedb39d74463044022015fff495fc5d61cd71e5815e4d23845ec26f4dc94adb85207feba2c97e19856502207297ec84afc0bb74aa8a20d7254025a82d9b9f177f648845d8c72ee62884ff618114fa84c77f2a5245ef774845d40428d2a6f9603415041320000b013a95f14b0044f78a264e41713c64b5f89242540ee208c3098e00000d65" ); } + +#[test] +fn test_ripple_sign_raw_json() { + let private_key = "6da2485443b6856cef6414d45d880434371522cdceb5baf7bd7114e135d71424" + .decode_hex() + .unwrap(); + + let raw_json = r#"{ + "TransactionType": "Payment", + "Destination": "rM27yzkCw6WA3T4g1sPaeC1kpxHUhuxRWn", + "Amount": "100000", + "Memos": [ + { + "Memo": { + "MemoData": "7b2266726f6d546f6b656e223a22307865656565656565656565656565656565656565656565656565656565656565656565656565656565222c22746f546f6b656e223a225452587c783561763039222c2273656e646572223a2272554653613244324a59476e354169525a6951785375705a7856354b6348454347222c2264657374696e6174696f6e223a22544255437a6763323976796b6b7646614547326d675274784b76614b6536736b7758222c226d696e52657475726e416d6f756e74223a2231333239353831303030303030222c2266726f6d416d6f756e74223a22313030303030227d" + } + } + ] + }"#; + + let input = Proto::SigningInput { + fee: 10, + sequence: 93_933_585, + last_ledger_sequence: 94_054_671, + account: "rUFSa2D2JYGn5AiRZiQxSupZxV5KcHECG".into(), + private_key: private_key.into(), + raw_json: raw_json.into(), + ..Proto::SigningInput::default() + }; + + let mut signer = AnySignerHelper::::default(); + let output = signer.sign(CoinType::XRP, input); + assert_eq!(output.error, SigningError::OK, "{}", output.error_message); + + // https://devnet.xrpl.org/transactions/D93B0B6983134BC6C2880B27708E8C1E932CB9E6D9E78773AD31B797741944FF + assert_eq!(output.encoded.to_hex(), "1200002405995011201b059b290f6140000000000186a068400000000000000a732103df650aab92e1b0a95cbda6a5a0fc3bfdbe991901e5b1cdfcd238b769cb4934a7744730450221008a5dba92a63fa82987a9ec3035febd1d5fe802f9a1baf3760090bb117281684e022056e4feca51231f719ca16cbfe370d312704e2cd7b94a28548f7f7886141d8bb28114023c2b9f15b95198d270b1bf92a4700c40272ca48314e1b799e72e5785c6a84af0f9c01424d4256b14aff9ea7dc1287b2266726f6d546f6b656e223a22307865656565656565656565656565656565656565656565656565656565656565656565656565656565222c22746f546f6b656e223a225452587c783561763039222c2273656e646572223a2272554653613244324a59476e354169525a6951785375705a7856354b6348454347222c2264657374696e6174696f6e223a22544255437a6763323976796b6b7646614547326d675274784b76614b6536736b7758222c226d696e52657475726e416d6f756e74223a2231333239353831303030303030222c2266726f6d416d6f756e74223a22313030303030227de1f1"); +} From df8684dd91196d9a5fc70532205afb44e0af29ca Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Tue, 11 Feb 2025 13:21:48 +0100 Subject: [PATCH 3/4] feat(xrp): Review comments --- rust/chains/tw_ripple/src/compiler.rs | 25 ++----------- .../tw_ripple/src/modules/protobuf_builder.rs | 37 ++++++++++++++++++- rust/chains/tw_ripple/src/signer.rs | 14 +------ 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/rust/chains/tw_ripple/src/compiler.rs b/rust/chains/tw_ripple/src/compiler.rs index f77d58d8844..ab8d9c089cf 100644 --- a/rust/chains/tw_ripple/src/compiler.rs +++ b/rust/chains/tw_ripple/src/compiler.rs @@ -5,7 +5,6 @@ use crate::encode::encode_tx; use crate::modules::protobuf_builder::ProtobufBuilder; use crate::modules::transaction_signer::TransactionSigner; -use crate::transaction::RippleTransaction; use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes}; use tw_coin_entry::common::compile_input::SingleSignaturePubkey; @@ -32,13 +31,8 @@ impl RippleCompiler { _coin: &dyn CoinContext, input: Proto::SigningInput<'_>, ) -> SigningResult> { - let pre_image = if input.raw_json.is_empty() { - let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; - TransactionSigner::pre_image(&unsigned_tx)? - } else { - let tx_json = ProtobufBuilder::new(&input).build_tx_json()?; - TransactionSigner::pre_image(&tx_json)? - }; + let unsigned_tx = ProtobufBuilder::new(&input).build()?; + let pre_image = TransactionSigner::pre_image(&unsigned_tx)?; Ok(CompilerProto::PreSigningOutput { data_hash: pre_image.hash_to_sign.to_vec().into(), @@ -63,20 +57,6 @@ impl RippleCompiler { input: Proto::SigningInput<'_>, signatures: Vec, public_keys: Vec, - ) -> SigningResult> { - if input.raw_json.is_empty() { - let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; - Self::compile_tx(unsigned_tx, signatures, public_keys) - } else { - let json_tx = ProtobufBuilder::new(&input).build_tx_json()?; - Self::compile_tx(json_tx, signatures, public_keys) - } - } - - fn compile_tx( - unsigned_tx: Transaction, - signatures: Vec, - public_keys: Vec, ) -> SigningResult> { let SingleSignaturePubkey { signature, @@ -90,6 +70,7 @@ impl RippleCompiler { .into_tw() .context("Invalid public key")?; + let unsigned_tx = ProtobufBuilder::new(&input).build()?; let signed_tx = TransactionSigner::compile(unsigned_tx, &signature, &public_key)?; let signing_only = false; diff --git a/rust/chains/tw_ripple/src/modules/protobuf_builder.rs b/rust/chains/tw_ripple/src/modules/protobuf_builder.rs index 23673a4b997..e5749695f60 100644 --- a/rust/chains/tw_ripple/src/modules/protobuf_builder.rs +++ b/rust/chains/tw_ripple/src/modules/protobuf_builder.rs @@ -4,15 +4,18 @@ use crate::address::classic_address::ClassicAddress; use crate::address::RippleAddress; +use crate::transaction::common_fields::CommonFields; use crate::transaction::json_transaction::JsonTransaction; use crate::transaction::transaction_builder::TransactionBuilder; use crate::transaction::transaction_type::TransactionType; +use crate::transaction::RippleTransaction; use crate::types::account_id::AccountId; use crate::types::amount::issued_currency::IssuedCurrency; use crate::types::amount::native_amount::NativeAmount; use crate::types::amount::Amount; use crate::types::currency::Currency; use bigdecimal::BigDecimal; +use serde::{Deserialize, Serialize}; use std::str::FromStr; use tw_coin_entry::error::prelude::*; use tw_encoding::hex::as_hex::AsHex; @@ -23,6 +26,30 @@ use tw_misc::traits::{OptionalEmpty, OptionalInt}; use tw_proto::Ripple::Proto; use tw_proto::Ripple::Proto::mod_SigningInput::OneOfoperation_oneof as OperationType; +#[derive(Deserialize, Serialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum SigningRequest { + Typed(TransactionType), + RawJSON(JsonTransaction), +} + +impl RippleTransaction for SigningRequest { + fn common_types(&self) -> &CommonFields { + match self { + SigningRequest::Typed(ty) => ty.common_types(), + SigningRequest::RawJSON(json) => json.common_types(), + } + } + + fn common_types_mut(&mut self) -> &mut CommonFields { + match self { + SigningRequest::Typed(ty) => ty.common_types_mut(), + SigningRequest::RawJSON(json) => json.common_types_mut(), + } + } +} + pub struct ProtobufBuilder<'a> { input: &'a Proto::SigningInput<'a>, } @@ -32,6 +59,14 @@ impl<'a> ProtobufBuilder<'a> { ProtobufBuilder { input } } + pub fn build(self) -> SigningResult { + if self.input.raw_json.is_empty() { + self.build_typed().map(SigningRequest::Typed) + } else { + self.build_tx_json().map(SigningRequest::RawJSON) + } + } + /// Builds a transaction from `SigningInput.rawJson` JSON object, /// returns a [`JsonTransaction`] with deserialized [`CommonFields`]. /// @@ -77,7 +112,7 @@ impl<'a> ProtobufBuilder<'a> { Ok(tx) } - pub fn build_tx(self) -> SigningResult { + pub fn build_typed(self) -> SigningResult { match self.input.operation_oneof { OperationType::op_payment(ref payment) => self.payment(payment), OperationType::op_trust_set(ref trust_set) => self.trust_set(trust_set), diff --git a/rust/chains/tw_ripple/src/signer.rs b/rust/chains/tw_ripple/src/signer.rs index 933cd8b1507..30ddee5af93 100644 --- a/rust/chains/tw_ripple/src/signer.rs +++ b/rust/chains/tw_ripple/src/signer.rs @@ -5,7 +5,6 @@ use crate::encode::encode_tx; use crate::modules::protobuf_builder::ProtobufBuilder; use crate::modules::transaction_signer::TransactionSigner; -use crate::transaction::RippleTransaction; use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::error::prelude::*; use tw_coin_entry::signing_output_error; @@ -27,19 +26,8 @@ impl RippleSigner { _coin: &dyn CoinContext, input: Proto::SigningInput<'_>, ) -> SigningResult> { - if input.raw_json.is_empty() { - let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?; - Self::sign_tx(unsigned_tx, input) - } else { - let json_tx = ProtobufBuilder::new(&input).build_tx_json()?; - Self::sign_tx(json_tx, input) - } - } + let unsigned_tx = ProtobufBuilder::new(&input).build()?; - fn sign_tx( - unsigned_tx: Transaction, - input: Proto::SigningInput<'_>, - ) -> SigningResult> { let private_key = secp256k1::PrivateKey::try_from(input.private_key.as_ref()) .into_tw() .context("Invalid private key")?; From 2990295b35f2e6d065b16b6c6351ba40386ea5e1 Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Tue, 11 Feb 2025 13:29:09 +0100 Subject: [PATCH 4/4] feat(xrp): Small refactoring: add `EncodeMode` --- rust/chains/tw_ripple/src/compiler.rs | 5 ++--- rust/chains/tw_ripple/src/encode/mod.rs | 10 +++++++++- rust/chains/tw_ripple/src/lib.rs | 5 ++--- .../chains/tw_ripple/src/modules/transaction_signer.rs | 5 ++--- rust/chains/tw_ripple/src/signer.rs | 5 ++--- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/rust/chains/tw_ripple/src/compiler.rs b/rust/chains/tw_ripple/src/compiler.rs index ab8d9c089cf..6cc4aa11212 100644 --- a/rust/chains/tw_ripple/src/compiler.rs +++ b/rust/chains/tw_ripple/src/compiler.rs @@ -2,7 +2,7 @@ // // Copyright © 2017 Trust Wallet. -use crate::encode::encode_tx; +use crate::encode::{encode_tx, EncodeMode}; use crate::modules::protobuf_builder::ProtobufBuilder; use crate::modules::transaction_signer::TransactionSigner; use tw_coin_entry::coin_context::CoinContext; @@ -73,8 +73,7 @@ impl RippleCompiler { let unsigned_tx = ProtobufBuilder::new(&input).build()?; let signed_tx = TransactionSigner::compile(unsigned_tx, &signature, &public_key)?; - let signing_only = false; - let encoded = encode_tx(&signed_tx, signing_only)?.encoded; + let encoded = encode_tx(&signed_tx, EncodeMode::All)?.encoded; Ok(Proto::SigningOutput { encoded: encoded.into(), ..Proto::SigningOutput::default() diff --git a/rust/chains/tw_ripple/src/encode/mod.rs b/rust/chains/tw_ripple/src/encode/mod.rs index 19880787ac4..b43e764e5f3 100644 --- a/rust/chains/tw_ripple/src/encode/mod.rs +++ b/rust/chains/tw_ripple/src/encode/mod.rs @@ -27,14 +27,22 @@ pub struct TxEncoded { pub encoded: Data, } +pub enum EncodeMode { + /// Encode all fields (only if `isSerialized: true`). + All, + /// Encode `isSigningField: true` transaction fields only. + SigningOnly, +} + pub fn encode_tx( tx: &Transaction, - signing_only: bool, + mode: EncodeMode, ) -> SigningResult { let json = serde_json::to_value(tx) .into_tw() .context("Error serializing a Ripple transaction as JSON")?; + let signing_only = matches!(mode, EncodeMode::SigningOnly); let st_object = STObject::try_from_value(json.clone(), signing_only)?; Ok(TxEncoded { json, diff --git a/rust/chains/tw_ripple/src/lib.rs b/rust/chains/tw_ripple/src/lib.rs index 5f8c776a7f5..0ffd2e6ea08 100644 --- a/rust/chains/tw_ripple/src/lib.rs +++ b/rust/chains/tw_ripple/src/lib.rs @@ -44,7 +44,7 @@ //! # use tw_keypair::traits::KeyPairTrait; //! # use tw_ripple::address::classic_address::ClassicAddress; //! # use tw_ripple::address::RippleAddress; -//! # use tw_ripple::encode::encode_tx; +//! # use tw_ripple::encode::{encode_tx, EncodeMode}; //! # use tw_ripple::modules::transaction_signer::TransactionSigner; //! # use tw_ripple::transaction::transaction_builder::TransactionBuilder; //! # use tw_ripple::transaction::transaction_type::TransactionType; @@ -70,8 +70,7 @@ //! let unsigned_tx = TransactionType::Payment(payment); //! //! let signed_tx = TransactionSigner::sign(unsigned_tx, key.private()).unwrap(); -//! let signing_only = false; -//! let _encoded_tx = encode_tx(&signed_tx, signing_only).unwrap(); +//! let _encoded_tx = encode_tx(&signed_tx, EncodeMode::All).unwrap(); //! ``` //! //! ## Integration with WalletCore diff --git a/rust/chains/tw_ripple/src/modules/transaction_signer.rs b/rust/chains/tw_ripple/src/modules/transaction_signer.rs index 6efa9792dc4..e7c234c1f85 100644 --- a/rust/chains/tw_ripple/src/modules/transaction_signer.rs +++ b/rust/chains/tw_ripple/src/modules/transaction_signer.rs @@ -3,7 +3,7 @@ // Copyright © 2017 Trust Wallet. use crate::address::classic_address::ClassicAddress; -use crate::encode::{encode_tx, TxEncoded}; +use crate::encode::{encode_tx, EncodeMode, TxEncoded}; use crate::transaction::RippleTransaction; use serde_json::Value as Json; use tw_coin_entry::error::prelude::*; @@ -49,8 +49,7 @@ impl TransactionSigner { pub fn pre_image( tx: &Transaction, ) -> SigningResult { - let signing_only = true; - let TxEncoded { json, encoded } = encode_tx(tx, signing_only)?; + let TxEncoded { json, encoded } = encode_tx(tx, EncodeMode::SigningOnly)?; let pre_image: Data = NETWORK_PREFIX.iter().copied().chain(encoded).collect(); let hash512 = sha512(&pre_image); diff --git a/rust/chains/tw_ripple/src/signer.rs b/rust/chains/tw_ripple/src/signer.rs index 30ddee5af93..f25c133acef 100644 --- a/rust/chains/tw_ripple/src/signer.rs +++ b/rust/chains/tw_ripple/src/signer.rs @@ -2,7 +2,7 @@ // // Copyright © 2017 Trust Wallet. -use crate::encode::encode_tx; +use crate::encode::{encode_tx, EncodeMode}; use crate::modules::protobuf_builder::ProtobufBuilder; use crate::modules::transaction_signer::TransactionSigner; use tw_coin_entry::coin_context::CoinContext; @@ -33,8 +33,7 @@ impl RippleSigner { .context("Invalid private key")?; let signed_tx = TransactionSigner::sign(unsigned_tx, &private_key)?; - let signing_only = false; - let encoded = encode_tx(&signed_tx, signing_only)?.encoded; + let encoded = encode_tx(&signed_tx, EncodeMode::All)?.encoded; Ok(Proto::SigningOutput { encoded: encoded.into(), ..Proto::SigningOutput::default()