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 3 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
35 changes: 25 additions & 10 deletions rust/chains/tw_ripple/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,16 +32,17 @@ 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 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()
})
}
Expand All @@ -61,6 +63,20 @@ impl RippleCompiler {
input: Proto::SigningInput<'_>,
signatures: Vec<SignatureBytes>,
public_keys: Vec<PublicKeyBytes>,
) -> SigningResult<Proto::SigningOutput<'static>> {
if input.raw_json.is_empty() {
let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?;
gupnik marked this conversation as resolved.
Show resolved Hide resolved
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<Transaction: RippleTransaction>(
unsigned_tx: Transaction,
signatures: Vec<SignatureBytes>,
public_keys: Vec<PublicKeyBytes>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let SingleSignaturePubkey {
signature,
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions 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 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
84 changes: 69 additions & 15 deletions rust/chains/tw_ripple/src/modules/protobuf_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<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_tx(self) -> SigningResult<TransactionType> {
match self.input.operation_oneof {
OperationType::op_payment(ref payment) => self.payment(payment),
Expand Down Expand Up @@ -235,21 +282,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 +299,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
17 changes: 9 additions & 8 deletions rust/chains/tw_ripple/src/modules/transaction_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -26,10 +25,10 @@ pub struct TxPreImage {
pub struct TransactionSigner;

impl TransactionSigner {
pub fn sign(
tx: TransactionType,
pub fn sign<Transaction: RippleTransaction>(
tx: Transaction,
private_key: &secp256k1::PrivateKey,
) -> SigningResult<TransactionType> {
) -> SigningResult<Transaction> {
let public_key = private_key.public();
Self::check_signing_public_key(&tx, &public_key)?;
Self::check_source_account(&tx, &public_key)?;
Expand All @@ -47,7 +46,9 @@ impl TransactionSigner {
Self::compile_unchecked(tx, &signature)
}

pub fn pre_image(tx: &TransactionType) -> SigningResult<TxPreImage> {
pub fn pre_image<Transaction: RippleTransaction>(
tx: &Transaction,
) -> SigningResult<TxPreImage> {
let signing_only = true;
let TxEncoded { json, encoded } = encode_tx(tx, signing_only)?;
let pre_image: Data = NETWORK_PREFIX.iter().copied().chain(encoded).collect();
Expand All @@ -62,11 +63,11 @@ impl TransactionSigner {
}

/// Compiles `signature` into the `transaction` validating the signature.
pub fn compile(
tx: TransactionType,
pub fn compile<Transaction: RippleTransaction>(
tx: Transaction,
signature: &secp256k1::Signature,
public_key: &secp256k1::PublicKey,
) -> SigningResult<TransactionType> {
) -> SigningResult<Transaction> {
let TxPreImage { hash_to_sign, .. } = Self::pre_image(&tx)?;
Self::check_source_account(&tx, public_key)?;
Self::check_signing_public_key(&tx, public_key)?;
Expand Down
15 changes: 14 additions & 1 deletion rust/chains/tw_ripple/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,7 +27,19 @@ impl RippleSigner {
_coin: &dyn CoinContext,
input: Proto::SigningInput<'_>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let unsigned_tx = ProtobufBuilder::new(&input).build_tx()?;
if input.raw_json.is_empty() {
gupnik marked this conversation as resolved.
Show resolved Hide resolved
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<Transaction: RippleTransaction>(
unsigned_tx: Transaction,
input: Proto::SigningInput<'_>,
) -> SigningResult<Proto::SigningOutput<'static>> {
let private_key = secp256k1::PrivateKey::try_from(input.private_key.as_ref())
.into_tw()
.context("Invalid private key")?;
Expand Down
3 changes: 2 additions & 1 deletion rust/chains/tw_ripple/src/transaction/common_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ pub struct CommonFields {
#[serde(skip_serializing_if = "Option::is_none")]
pub fee: Option<NativeAmount>,
/// Set of bit-flags for this transaction.
pub flags: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<u32>,
/// 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.
Expand Down
Loading
Loading