Skip to content

Commit

Permalink
Feature/destination memos (#939)
Browse files Browse the repository at this point in the history
  • Loading branch information
briancorbin authored Nov 28, 2023
1 parent f07f6cd commit 62e997c
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE destination_memos;
13 changes: 13 additions & 0 deletions full-service/migrations/2023-11-27-175035_destination_memos/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE destination_memos (
txo_id TEXT PRIMARY KEY NOT NULL,
recipient_address_hash TEXT NOT NULL,
num_recipients INT NOT NULL,
fee UNSIGNED BIG INT NOT NULL,
total_outlay UNSIGNED BIG INT NOT NULL,
payment_request_id UNSIGNED BIG INT,
payment_intent_id UNSIGNED BIG INT,
FOREIGN KEY (txo_id) REFERENCES txos(id)
);

UPDATE accounts SET next_block_index = 0;
UPDATE accounts SET resyncing = TRUE;
29 changes: 28 additions & 1 deletion full-service/src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

use super::schema::{
__diesel_schema_migrations, accounts, assigned_subaddresses, authenticated_sender_memos,
gift_codes, transaction_input_txos, transaction_logs, transaction_output_txos, txos,
destination_memos, gift_codes, transaction_input_txos, transaction_logs,
transaction_output_txos, txos,
};
use mc_crypto_keys::CompressedRistrettoPublic;
use serde::Serialize;
Expand Down Expand Up @@ -256,6 +257,32 @@ pub struct NewAuthenticatedSenderMemo<'a> {
pub payment_intent_id: Option<i64>,
}

#[derive(Clone, Serialize, Associations, Identifiable, Queryable, PartialEq, Eq, Debug)]
#[diesel(belongs_to(Txo, foreign_key = txo_id))]
#[diesel(table_name = destination_memos)]
#[diesel(primary_key(txo_id))]
pub struct DestinationMemo {
pub txo_id: String,
pub recipient_address_hash: String,
pub num_recipients: i32,
pub fee: i64,
pub total_outlay: i64,
pub payment_request_id: Option<i64>,
pub payment_intent_id: Option<i64>,
}

#[derive(Insertable)]
#[diesel(table_name = destination_memos)]
pub struct NewDestinationMemo<'a> {
pub txo_id: &'a str,
pub recipient_address_hash: &'a str,
pub num_recipients: i32,
pub fee: i64,
pub total_outlay: i64,
pub payment_request_id: Option<i64>,
pub payment_intent_id: Option<i64>,
}

#[derive(Queryable, Insertable)]
#[diesel(table_name = __diesel_schema_migrations)]
pub struct Migration {
Expand Down
13 changes: 13 additions & 0 deletions full-service/src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ diesel::table! {
}
}

diesel::table! {
destination_memos (txo_id) {
txo_id -> Text,
recipient_address_hash -> Text,
num_recipients -> Integer,
fee -> BigInt,
total_outlay -> BigInt,
payment_request_id -> Nullable<BigInt>,
payment_intent_id -> Nullable<BigInt>,
}
}

diesel::table! {
gift_codes (id) {
id -> Integer,
Expand Down Expand Up @@ -115,6 +127,7 @@ diesel::allow_tables_to_appear_in_same_query!(
accounts,
assigned_subaddresses,
authenticated_sender_memos,
destination_memos,
gift_codes,
transaction_input_txos,
transaction_logs,
Expand Down
93 changes: 85 additions & 8 deletions full-service/src/db/txo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ use mc_transaction_core::{
};
use mc_transaction_extra::{
AuthenticatedSenderMemo, AuthenticatedSenderWithPaymentIntentIdMemo,
AuthenticatedSenderWithPaymentRequestIdMemo, MemoType, RegisteredMemoType,
TxOutConfirmationNumber, UnusedMemo,
AuthenticatedSenderWithPaymentRequestIdMemo, DestinationMemo,
DestinationWithPaymentIntentIdMemo, DestinationWithPaymentRequestIdMemo, MemoType,
RegisteredMemoType, TxOutConfirmationNumber, UnusedMemo,
};
use mc_util_serial::Message;
use std::{convert::TryFrom, fmt, str::FromStr};
Expand All @@ -31,7 +32,8 @@ use crate::{
assigned_subaddress::AssignedSubaddressModel,
models::{
Account, AssignedSubaddress, AuthenticatedSenderMemo as AuthenticatedSenderMemoModel,
NewAuthenticatedSenderMemo, NewTransactionOutputTxo, NewTxo, TransactionOutputTxo, Txo,
DestinationMemo as DestinationMemoModel, NewAuthenticatedSenderMemo,
NewDestinationMemo, NewTransactionOutputTxo, NewTxo, TransactionOutputTxo, Txo,
},
transaction_log::TransactionId,
Conn, WalletDbError,
Expand Down Expand Up @@ -75,6 +77,7 @@ pub enum TxoStatus {
pub enum TxoMemo {
Unused,
AuthenticatedSender(AuthenticatedSenderMemoModel),
Destination(DestinationMemoModel),
}

impl fmt::Display for TxoStatus {
Expand Down Expand Up @@ -1968,7 +1971,8 @@ impl TxoModel for Txo {

fn delete_unreferenced(conn: Conn) -> Result<(), WalletDbError> {
use crate::db::schema::{
authenticated_sender_memos, transaction_input_txos, transaction_output_txos, txos,
authenticated_sender_memos, destination_memos, transaction_input_txos,
transaction_output_txos, txos,
};

/*
Expand All @@ -1990,9 +1994,13 @@ impl TxoModel for Txo {
let unreferenced_authenticated_sender_memos = authenticated_sender_memos::table
.filter(authenticated_sender_memos::txo_id.eq_any(unreferenced_txos.select(txos::id)));

let unreferenced_destination_memos = destination_memos::table
.filter(destination_memos::txo_id.eq_any(unreferenced_txos.select(txos::id)));

// Delete all associated memos in the database with these unreferenced txos, and
// then delete the unreferenced txos themselves.
diesel::delete(unreferenced_authenticated_sender_memos).execute(conn)?;
diesel::delete(unreferenced_destination_memos).execute(conn)?;
diesel::delete(unreferenced_txos).execute(conn)?;

Ok(())
Expand Down Expand Up @@ -2071,7 +2079,7 @@ impl TxoModel for Txo {
}

fn memo(&self, conn: Conn) -> Result<TxoMemo, WalletDbError> {
use crate::db::schema::authenticated_sender_memos;
use crate::db::schema::{authenticated_sender_memos, destination_memos};
Ok(
match self.memo_type {
None => TxoMemo::Unused,
Expand All @@ -2086,6 +2094,15 @@ impl TxoModel for Txo {
).first::<AuthenticatedSenderMemoModel>(conn)?;
TxoMemo::AuthenticatedSender(db_memo)
},
<DestinationMemo as RegisteredMemoType>::MEMO_TYPE_BYTES |
<DestinationWithPaymentIntentIdMemo as RegisteredMemoType>::MEMO_TYPE_BYTES |
<DestinationWithPaymentRequestIdMemo as RegisteredMemoType>::MEMO_TYPE_BYTES
=> {
let db_memo = destination_memos::table.filter(
destination_memos::txo_id.eq(&self.id),
).first::<DestinationMemoModel>(conn)?;
TxoMemo::Destination(db_memo)
},
_ => TxoMemo::Unused,
}
}
Expand Down Expand Up @@ -2154,11 +2171,10 @@ impl TxoModel for Txo {
}

fn account(&self, conn: Conn) -> Result<Option<Account>, WalletDbError> {
Ok(self
.account_id
self.account_id
.as_ref()
.map(|account_id| Account::get(&AccountID(account_id.to_string()), conn))
.transpose()?)
.transpose()
}
}

Expand Down Expand Up @@ -2186,6 +2202,37 @@ fn add_authenticated_memo_to_database(
Ok(())
}

#[allow(clippy::too_many_arguments)]
fn add_destination_memo_to_database(
txo_id: &str,
recipient_address_hash: &str,
num_recipients: i32,
fee: i64,
total_outlay: i64,
payment_request_id: Option<i64>,
payment_intent_id: Option<i64>,
conn: Conn,
) -> Result<(), WalletDbError> {
use crate::db::schema::destination_memos;

let new_memo = NewDestinationMemo {
txo_id,
recipient_address_hash,
num_recipients,
fee,
total_outlay,
payment_request_id,
payment_intent_id,
};

diesel::insert_into(destination_memos::table)
.values(&new_memo)
.on_conflict_do_nothing()
.execute(conn)?;

Ok(())
}

fn i32_to_two_bytes(value: i32) -> [u8; 2] {
[(value >> 8) as u8, (value & 0xFF) as u8]
}
Expand Down Expand Up @@ -2227,6 +2274,36 @@ fn add_memo_to_database(
conn,
)
}
Ok(MemoType::Destination(memo)) => add_destination_memo_to_database(
txo_id,
&memo.get_address_hash().to_string(),
memo.get_num_recipients() as i32,
memo.get_fee() as i64,
memo.get_total_outlay() as i64,
None,
None,
conn,
),
Ok(MemoType::DestinationWithPaymentRequestId(memo)) => add_destination_memo_to_database(
txo_id,
&memo.get_address_hash().to_string(),
memo.get_num_recipients() as i32,
memo.get_fee() as i64,
memo.get_total_outlay() as i64,
Some(memo.get_payment_request_id() as i64),
None,
conn,
),
Ok(MemoType::DestinationWithPaymentIntentId(memo)) => add_destination_memo_to_database(
txo_id,
&memo.get_address_hash().to_string(),
memo.get_num_recipients() as i32,
memo.get_fee() as i64,
memo.get_total_outlay() as i64,
None,
Some(memo.get_payment_intent_id() as i64),
conn,
),
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
Expand Down
106 changes: 105 additions & 1 deletion full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_txo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ mod e2e_transaction {
db::{account::AccountID, txo::TxoStatus},
json_rpc::v2::{
api::test_utils::{dispatch, setup},
models::tx_proposal::TxProposal as TxProposalJSON,
models::{
memo::{DestinationMemo, Memo},
transaction_log::TransactionLog as TransactionLogJSON,
tx_proposal::TxProposal as TxProposalJSON,
txo::Txo as TxoJSON,
},
},
service::models::tx_proposal::TxProposal,
test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account},
util::b58::b58_decode_public_address,
};

use mc_account_keys::ShortAddressHash;
use mc_common::logger::{test_with_logger, Logger};
use mc_rand::rand_core::RngCore;
use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token};
Expand Down Expand Up @@ -594,4 +600,102 @@ mod e2e_transaction {
let unspent = balance_mob["unspent"].as_str().unwrap();
assert_eq!(unspent, "100");
}

#[test_with_logger]
fn test_txo_info_includes_correct_memos(logger: Logger) {
let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]);
let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone());

// Add an account
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "create_account",
"params": {
"name": "Alice Main Account",
}
});
let res = dispatch(&client, body, &logger);
let result = res.get("result").unwrap();
let account_obj = result.get("account").unwrap();
let account_id = account_obj.get("id").unwrap().as_str().unwrap();
let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap();
let public_address = b58_decode_public_address(b58_public_address).unwrap();
let short_address_hash: ShortAddressHash = (&public_address).into();

// Add a block with a txo for this address (note that value is smaller than
// MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept
// up when we construct the transaction)
add_block_to_ledger_db(
&mut ledger_db,
&vec![public_address.clone()],
100,
&[KeyImage::from(rng.next_u64())],
&mut rng,
);

manually_sync_account(
&ledger_db,
&db_ctx.get_db_instance(logger.clone()),
&AccountID(account_id.to_string()),
&logger,
);

// Add a block with significantly more MOB
add_block_to_ledger_db(
&mut ledger_db,
&vec![public_address.clone()],
100_000_000_000_000, // 100.0 MOB
&[KeyImage::from(rng.next_u64())],
&mut rng,
);

manually_sync_account(
&ledger_db,
&db_ctx.get_db_instance(logger.clone()),
&AccountID(account_id.to_string()),
&logger,
);

// Create a tx proposal to ourselves
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "build_and_submit_transaction",
"params": {
"account_id": account_id,
"recipient_public_address": b58_public_address,
"amount": { "value": "42000000000000", "token_id": "0" }, // 42.0 MOB
}
});
let res = dispatch(&client, body, &logger);
let result = res.get("result").unwrap();
let transaction_log: TransactionLogJSON =
serde_json::from_value(result.get("transaction_log").unwrap().clone()).unwrap();

let change_txo = transaction_log.change_txos[0].clone();

let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "get_txo",
"params": {
"txo_id": change_txo.txo_id,
}
});
let res = dispatch(&client, body, &logger);
let result = res.get("result").unwrap();
let txo: TxoJSON = serde_json::from_value(result.get("txo").unwrap().clone()).unwrap();

let expected_memo = Memo::Destination(DestinationMemo {
recipient_address_hash: short_address_hash.to_string(),
num_recipients: "1".to_string(),
fee: "400000000".to_string(),
total_outlay: "42000400000000".to_string(),
payment_request_id: None,
payment_intent_id: None,
});

assert_eq!(txo.memo, expected_memo);
}
}
Loading

0 comments on commit 62e997c

Please sign in to comment.