diff --git a/full-service/src/db/transaction_log.rs b/full-service/src/db/transaction_log.rs index 0774b4946..cbf9f0328 100644 --- a/full-service/src/db/transaction_log.rs +++ b/full-service/src/db/transaction_log.rs @@ -592,14 +592,10 @@ impl TransactionLogModel for TransactionLog { .execute(conn)?; } - // TODO - Get each payload txo and add it to the transaction_output_txos - // table for this transactions. for payload_txo in unsigned_tx_proposal.payload_txos.iter() { Txo::create_new_output(payload_txo, false, &transaction_log_id, conn)?; } - // TODO - Get each change txo and add it to the transaction_output_txos - // table for this transaction as change. for change_txo in unsigned_tx_proposal.change_txos.iter() { Txo::create_new_output(change_txo, true, &transaction_log_id, conn)?; } diff --git a/full-service/src/db/txo.rs b/full-service/src/db/txo.rs index eb1687e7d..bc1d5b7a3 100644 --- a/full-service/src/db/txo.rs +++ b/full-service/src/db/txo.rs @@ -785,6 +785,11 @@ impl TxoModel for Txo { let txo_id = TxoID::from(&output_txo.tx_out); let encoded_confirmation = mc_util_serial::encode(&output_txo.confirmation_number); + + let shared_secret_bytes = output_txo + .shared_secret + .map(|shared_secret| shared_secret.to_bytes().to_vec()); + let new_txo = NewTxo { id: &txo_id.to_string(), account_id: None, @@ -798,7 +803,7 @@ impl TxoModel for Txo { received_block_index: None, spent_block_index: None, confirmation: Some(&encoded_confirmation), - shared_secret: None, // no account id so we don't + shared_secret: shared_secret_bytes.as_deref(), }; diesel::insert_into(txos::table) diff --git a/full-service/src/json_rpc/v2/api/wallet.rs b/full-service/src/json_rpc/v2/api/wallet.rs index d8e0a9d90..ff28374de 100644 --- a/full-service/src/json_rpc/v2/api/wallet.rs +++ b/full-service/src/json_rpc/v2/api/wallet.rs @@ -357,7 +357,7 @@ where None => None, }; - let unsigned_tx_proposal: UnsignedTxProposal = service + let unsigned_tx_proposal: UnsignedTxProposal = (&service .build_transaction( &account_id, &[( @@ -372,7 +372,7 @@ where TransactionMemo::BurnRedemption(memo_data), block_version, ) - .map_err(format_error)? + .map_err(format_error)?) .try_into() .map_err(format_error)?; @@ -406,7 +406,7 @@ where None => None, }; - let unsigned_tx_proposal: UnsignedTxProposal = service + let unsigned_tx_proposal: UnsignedTxProposal = (&service .build_transaction( &account_id, &addresses_and_amounts, @@ -418,7 +418,7 @@ where TransactionMemo::Empty, block_version, ) - .map_err(format_error)? + .map_err(format_error)?) .try_into() .map_err(format_error)?; diff --git a/full-service/src/json_rpc/v2/models/tx_proposal.rs b/full-service/src/json_rpc/v2/models/tx_proposal.rs index 07678bd5d..2340059cb 100644 --- a/full-service/src/json_rpc/v2/models/tx_proposal.rs +++ b/full-service/src/json_rpc/v2/models/tx_proposal.rs @@ -9,14 +9,14 @@ use protobuf::Message; use serde_derive::{Deserialize, Serialize}; use std::convert::TryFrom; -#[derive(Deserialize, Serialize, Default, Debug)] +#[derive(Deserialize, Serialize, Default, Debug, PartialEq)] pub struct UnsignedInputTxo { pub tx_out_proto: String, pub amount: AmountJSON, pub subaddress_index: String, } -#[derive(Clone, Deserialize, Serialize, Default, Debug)] +#[derive(Clone, Deserialize, Serialize, Default, Debug, PartialEq)] pub struct InputTxo { pub tx_out_proto: String, pub amount: AmountJSON, @@ -24,15 +24,16 @@ pub struct InputTxo { pub key_image: String, } -#[derive(Clone, Deserialize, Serialize, Default, Debug)] +#[derive(Clone, Deserialize, Serialize, Default, Debug, PartialEq)] pub struct OutputTxo { pub tx_out_proto: String, pub amount: AmountJSON, pub recipient_public_address_b58: String, pub confirmation_number: String, + pub shared_secret: Option, } -#[derive(Deserialize, Serialize, Debug, Default)] +#[derive(Deserialize, Serialize, Debug, Default, PartialEq)] pub struct UnsignedTxProposal { pub unsigned_tx_proto_bytes_hex: String, pub unsigned_input_txos: Vec, @@ -40,11 +41,11 @@ pub struct UnsignedTxProposal { pub change_txos: Vec, } -impl TryFrom for UnsignedTxProposal { +impl TryFrom<&crate::service::models::tx_proposal::UnsignedTxProposal> for UnsignedTxProposal { type Error = String; fn try_from( - src: crate::service::models::tx_proposal::UnsignedTxProposal, + src: &crate::service::models::tx_proposal::UnsignedTxProposal, ) -> Result { let unsigned_input_txos = src .unsigned_input_txos @@ -67,6 +68,9 @@ impl TryFrom for Unsign &output_txo.recipient_public_address, )?, confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + shared_secret: output_txo + .shared_secret + .map(|shared_secret| hex::encode(shared_secret.to_bytes())), }) }) .collect::, B58Error>>() @@ -83,6 +87,9 @@ impl TryFrom for Unsign &output_txo.recipient_public_address, )?, confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + shared_secret: output_txo + .shared_secret + .map(|shared_secret| hex::encode(shared_secret.to_bytes())), }) }) .collect::, B58Error>>() @@ -103,7 +110,7 @@ impl TryFrom for Unsign } } -#[derive(Clone, Deserialize, Serialize, Default, Debug)] +#[derive(Clone, Deserialize, Serialize, Default, Debug, PartialEq)] pub struct TxProposal { pub input_txos: Vec, pub payload_txos: Vec, @@ -139,6 +146,9 @@ impl TryFrom<&crate::service::models::tx_proposal::TxProposal> for TxProposal { &output_txo.recipient_public_address, )?, confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + shared_secret: output_txo + .shared_secret + .map(|shared_secret| hex::encode(shared_secret.to_bytes())), }) }) .collect::, B58Error>>() @@ -155,6 +165,9 @@ impl TryFrom<&crate::service::models::tx_proposal::TxProposal> for TxProposal { &output_txo.recipient_public_address, )?, confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + shared_secret: output_txo + .shared_secret + .map(|shared_secret| hex::encode(shared_secret.to_bytes())), }) }) .collect::, B58Error>>() diff --git a/full-service/src/service/models/tx_proposal.rs b/full-service/src/service/models/tx_proposal.rs index 643ba53f8..0b103347f 100644 --- a/full-service/src/service/models/tx_proposal.rs +++ b/full-service/src/service/models/tx_proposal.rs @@ -43,6 +43,7 @@ pub struct OutputTxo { pub recipient_public_address: PublicAddress, pub confirmation_number: TxOutConfirmationNumber, pub amount: Amount, + pub shared_secret: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -53,7 +54,7 @@ pub struct TxProposal { pub change_txos: Vec, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct UnsignedTxProposal { pub unsigned_tx: UnsignedTx, pub unsigned_input_txos: Vec, @@ -114,11 +115,11 @@ impl UnsignedTxProposal { } } -impl TryFrom for UnsignedTxProposal { +impl TryFrom<&crate::json_rpc::v2::models::tx_proposal::UnsignedTxProposal> for UnsignedTxProposal { type Error = String; fn try_from( - src: crate::json_rpc::v2::models::tx_proposal::UnsignedTxProposal, + src: &crate::json_rpc::v2::models::tx_proposal::UnsignedTxProposal, ) -> Result { let unsigned_input_txos = src .unsigned_input_txos @@ -160,11 +161,25 @@ impl TryFrom for U let amount = Amount::try_from(&txo.amount)?; + let shared_secret = match &txo.shared_secret { + Some(shared_secret) => { + let shared_secret_bytes = + hex::decode(shared_secret).map_err(|e| e.to_string())?; + Some( + RistrettoPublic::try_from(shared_secret_bytes.as_slice()).map_err(|e| { + format!("error converting shared secret to RistrettoPublic: {e}") + })?, + ) + } + None => None, + }; + let output_txo = OutputTxo { tx_out, recipient_public_address, confirmation_number, amount, + shared_secret, }; payload_txos.push(output_txo); @@ -190,11 +205,25 @@ impl TryFrom for U let amount = Amount::try_from(&txo.amount)?; + let shared_secret = match &txo.shared_secret { + Some(shared_secret) => { + let shared_secret_bytes = + hex::decode(shared_secret).map_err(|e| e.to_string())?; + Some( + RistrettoPublic::try_from(shared_secret_bytes.as_slice()).map_err(|e| { + format!("error converting shared secret to RistrettoPublic: {e}") + })?, + ) + } + None => None, + }; + let output_txo = OutputTxo { tx_out, recipient_public_address, confirmation_number, amount, + shared_secret, }; change_txos.push(output_txo); @@ -278,6 +307,7 @@ impl TryFrom<&crate::json_rpc::v1::models::tx_proposal::TxProposal> for TxPropos recipient_public_address: public_address, confirmation_number, amount: Amount::new(outlay.value.0, Mob::ID), + shared_secret: None, }; payload_txos.push(payload_txo); @@ -344,11 +374,25 @@ impl TryFrom<&crate::json_rpc::v2::models::tx_proposal::TxProposal> for TxPropos let amount = Amount::try_from(&txo.amount)?; + let shared_secret = match &txo.shared_secret { + Some(shared_secret) => { + let shared_secret_bytes = + hex::decode(shared_secret).map_err(|e| e.to_string())?; + Some( + RistrettoPublic::try_from(shared_secret_bytes.as_slice()).map_err(|e| { + format!("error converting shared secret to RistrettoPublic: {e}") + })?, + ) + } + None => None, + }; + let output_txo = OutputTxo { tx_out, recipient_public_address, confirmation_number, amount, + shared_secret, }; payload_txos.push(output_txo); @@ -374,11 +418,25 @@ impl TryFrom<&crate::json_rpc::v2::models::tx_proposal::TxProposal> for TxPropos let amount = Amount::try_from(&txo.amount)?; + let shared_secret = match &txo.shared_secret { + Some(shared_secret) => { + let shared_secret_bytes = + hex::decode(shared_secret).map_err(|e| e.to_string())?; + Some( + RistrettoPublic::try_from(shared_secret_bytes.as_slice()).map_err(|e| { + format!("error converting shared secret to RistrettoPublic: {e}") + })?, + ) + } + None => None, + }; + let output_txo = OutputTxo { tx_out, recipient_public_address, confirmation_number, amount, + shared_secret, }; change_txos.push(output_txo); @@ -392,3 +450,115 @@ impl TryFrom<&crate::json_rpc::v2::models::tx_proposal::TxProposal> for TxPropos }) } } + +#[cfg(test)] +mod tests { + use crate::{ + db::account::AccountID, + json_rpc::v2::models::amount::Amount as AmountJSON, + service::{ + account::AccountService, + address::AddressService, + transaction::{TransactionMemo, TransactionService}, + }, + test_utils::{ + add_block_to_ledger_db, get_test_ledger, manually_sync_account, setup_wallet_service, + MOB, + }, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_rand::RngCore; + use rand::{rngs::StdRng, SeedableRng}; + + use super::*; + + #[test_with_logger] + fn test_v2_tx_proposal_converts_correctly(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let known_recipients: Vec = Vec::new(); + let mut ledger_db = get_test_ledger(5, &known_recipients, 12, &mut rng); + + let service = setup_wallet_service(ledger_db.clone(), logger.clone()); + + // Create our main account for the wallet + let alice = service + .create_account( + Some("Alice's Main Account".to_string()), + "".to_string(), + "".to_string(), + ) + .unwrap(); + + // Add a block with a transaction for Alice + let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); + let alice_account_id = AccountID::from(&alice_account_key); + let alice_public_address = alice_account_key.default_subaddress(); + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address.clone()], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); + + // Add an account for Bob + let bob = service + .create_account( + Some("Bob's Main Account".to_string()), + "".to_string(), + "".to_string(), + ) + .unwrap(); + + // Create an assigned subaddress for Bob to receive funds from Alice + let bob_address_from_alice = service + .assign_address_for_account(&AccountID(bob.id.clone()), Some("From Alice")) + .unwrap(); + + // Create an assigned subaddress for Alice to receive from Bob, which will be + // used to authenticate the sender (Alice) + let alice_address_from_bob = service + .assign_address_for_account(&alice_account_id, Some("From Bob")) + .unwrap(); + + let unsigned_tx_proposal = service + .build_transaction( + &alice.id, + &[( + bob_address_from_alice.public_address_b58, + AmountJSON::new(42 * MOB, Mob::ID), + )], + None, + None, + None, + None, + None, + TransactionMemo::RTH(Some(alice_address_from_bob.subaddress_index as u64), None), + None, + ) + .unwrap(); + + let unsigned_tx_proposal_v2_json_model = + crate::json_rpc::v2::models::tx_proposal::UnsignedTxProposal::try_from( + &unsigned_tx_proposal, + ) + .unwrap(); + + let unsigned_tx_proposal_converted_from_v2_json_model = + UnsignedTxProposal::try_from(&unsigned_tx_proposal_v2_json_model).unwrap(); + + assert_eq!( + unsigned_tx_proposal, + unsigned_tx_proposal_converted_from_v2_json_model + ); + } +} diff --git a/full-service/src/service/transaction_builder.rs b/full-service/src/service/transaction_builder.rs index b2dcf0ef4..5daf56306 100644 --- a/full-service/src/service/transaction_builder.rs +++ b/full-service/src/service/transaction_builder.rs @@ -383,6 +383,7 @@ impl WalletTransactionBuilder { recipient_public_address: receiver, confirmation_number: tx_out_context.confirmation, amount, + shared_secret: Some(tx_out_context.shared_secret), }; payload_txos.push(payload_txo); } @@ -439,6 +440,7 @@ impl WalletTransactionBuilder { recipient_public_address: reserved_subaddresses.change_subaddress.clone(), confirmation_number: tx_out_context.confirmation, amount: change_amount, + shared_secret: Some(tx_out_context.shared_secret), }; change_txos.push(change_txo); } diff --git a/signer/src/service/api/mod.rs b/signer/src/service/api/mod.rs index f1ef2592f..91971a9e6 100644 --- a/signer/src/service/api/mod.rs +++ b/signer/src/service/api/mod.rs @@ -82,7 +82,7 @@ fn signer_service_api_inner(command: JsonCommandRequest) -> Result { let signed_tx = service::sign_tx( &mnemonic, - unsigned_tx_proposal + (&unsigned_tx_proposal) .try_into() .map_err(|e: String| anyhow!(e))?, )?;