Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(XRP): Add support for signing a raw JSON transaction #4258

Merged
merged 5 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions rust/chains/tw_ripple/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//
// 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, TxPreImage};
use crate::modules::transaction_signer::TransactionSigner;
use tw_coin_entry::coin_context::CoinContext;
use tw_coin_entry::coin_entry::{PublicKeyBytes, SignatureBytes};
use tw_coin_entry::common::compile_input::SingleSignaturePubkey;
Expand All @@ -31,16 +31,12 @@ impl RippleCompiler {
_coin: &dyn CoinContext,
input: Proto::SigningInput<'_>,
) -> SigningResult<CompilerProto::PreSigningOutput<'static>> {
let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?;
let TxPreImage {
pre_image_tx_data,
hash_to_sign,
..
} = TransactionSigner::pre_image(&unsigned_tx)?;
let unsigned_tx = ProtobufBuilder::new(&input).build()?;
let pre_image = TransactionSigner::pre_image(&unsigned_tx)?;

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()
})
}
Expand Down Expand Up @@ -74,11 +70,10 @@ impl RippleCompiler {
.into_tw()
.context("Invalid public key")?;

let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?;
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()
Expand Down
11 changes: 10 additions & 1 deletion rust/chains/tw_ripple/src/encode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -26,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<Transaction: RippleTransaction>(
tx: &Transaction,
signing_only: bool,
mode: EncodeMode,
) -> SigningResult<TxEncoded> {
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,
Expand Down
41 changes: 41 additions & 0 deletions rust/chains/tw_ripple/src/encode/st_array.rs
Original file line number Diff line number Diff line change
@@ -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:
/// `<https://xrpl.org/serialization.html#array-fields>`
#[derive(Debug, Deserialize, Serialize)]
pub struct STArray(Vec<Json>);

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(())
}
}
8 changes: 8 additions & 0 deletions rust/chains/tw_ripple/src/encode/st_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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(())
}
}
11 changes: 9 additions & 2 deletions rust/chains/tw_ripple/src/encode/xrpl_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,6 +36,8 @@ pub enum XRPLTypes {
Hash160(H160),
Hash256(H256),
Vector256(Vector256),
STArray(STArray),
STObject(STObject),
UInt8(u8),
UInt16(u16),
UInt32(u32),
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand Down
5 changes: 2 additions & 3 deletions rust/chains/tw_ripple/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
121 changes: 105 additions & 16 deletions rust/chains/tw_ripple/src/modules/protobuf_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,52 @@

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;
use tw_encoding::hex::DecodeHex;
use tw_hash::H256;
use tw_keypair::ecdsa::secp256k1;
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>,
}
Expand All @@ -30,7 +59,60 @@ impl<'a> ProtobufBuilder<'a> {
ProtobufBuilder { input }
}

pub fn build_tx(self) -> SigningResult<TransactionType> {
pub fn build(self) -> SigningResult<SigningRequest> {
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`].
///
/// Note [`JsonTransaction`] implements [`RippleTransaction`]
pub fn build_tx_json(self) -> SigningResult<JsonTransaction> {
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_typed(self) -> SigningResult<TransactionType> {
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),
Expand Down Expand Up @@ -235,21 +317,8 @@ impl<'a> ProtobufBuilder<'a> {
}

pub fn prepare_builder(&self) -> SigningResult<TransactionBuilder> {
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
Expand All @@ -265,6 +334,26 @@ impl<'a> ProtobufBuilder<'a> {
Ok(builder)
}

fn signing_public_key(&self) -> SigningResult<secp256k1::PublicKey> {
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> {
NativeAmount::new(self.input.fee).context("Invalid fee")
}

fn issued_currency(input: &Proto::CurrencyAmount) -> SigningResult<IssuedCurrency> {
let value = BigDecimal::from_str(input.value.as_ref())
.tw_err(SigningErrorType::Error_invalid_requested_token_amount)
Expand Down
Loading
Loading