Skip to content

Commit f247880

Browse files
authored
feat: payref tracking (#7734)
Description --- Add historical payref tracking
1 parent 5294228 commit f247880

File tree

15 files changed

+848
-202
lines changed

15 files changed

+848
-202
lines changed

applications/minotari_app_grpc/proto/wallet.proto

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2151,6 +2151,9 @@ message TransactionInfo {
21512151
repeated bytes payment_references_sent = 17;
21522152
repeated bytes payment_references_received = 18;
21532153
repeated bytes payment_references_change = 19;
2154+
// The reason a transaction was rejected/cancelled, if applicable.
2155+
// Empty string if the transaction is not rejected.
2156+
string rejected_reason = 20;
21542157
}
21552158

21562159
enum TransactionDirection {
@@ -2497,6 +2500,16 @@ message GetTransactionPayRefsResponse {
24972500
repeated bytes payment_references = 1 [deprecated = true];
24982501
// List of input commitment info for the transaction - hash, commitment, payment reference, output category.
24992502
repeated CommitmentInfo output_commitments_info = 2;
2503+
// Historical PayRefs that were superseded by reorgs (output_hash + previous payref pairs).
2504+
repeated HistoricalPayRef historical_payment_references = 3;
2505+
}
2506+
2507+
// A historical PayRef that was superseded due to a blockchain reorg.
2508+
message HistoricalPayRef {
2509+
// The output hash this PayRef was generated for.
2510+
bytes output_hash = 1;
2511+
// The previous PayRef value (before the reorg changed the block hash).
2512+
bytes payment_reference = 2;
25002513
}
25012514

25022515
message CommitmentInfo {
@@ -2530,9 +2543,11 @@ message GetPaymentByReferenceRequest {
25302543

25312544
// Response message containing transaction information for a payment reference
25322545
message GetPaymentByReferenceResponse {
2533-
// The transaction information if PayRef is found (optional).
2546+
// The transaction information if PayRef is found via current PayRef (optional).
25342547
// Returns full transaction details
25352548
TransactionInfo transaction = 1;
2549+
// Transactions found via historical (superseded) PayRefs from reorgs.
2550+
repeated TransactionInfo historical_transactions = 2;
25362551
}
25372552

25382553

applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs

Lines changed: 125 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,7 @@ impl wallet_server::Wallet for WalletGrpcServer {
16841684
.into_iter()
16851685
.map(|pr| pr.to_vec())
16861686
.collect(),
1687+
rejected_reason: txn.cancelled.map(|r| r.to_string()).unwrap_or_default(),
16871688
}),
16881689
};
16891690
match sender.send(Ok(response)).await {
@@ -1822,6 +1823,7 @@ impl wallet_server::Wallet for WalletGrpcServer {
18221823
.into_iter()
18231824
.map(|pr| pr.to_vec())
18241825
.collect(),
1826+
rejected_reason: txn.cancelled.map(|r| r.to_string()).unwrap_or_default(),
18251827
});
18261828
}
18271829

@@ -1983,6 +1985,7 @@ impl wallet_server::Wallet for WalletGrpcServer {
19831985
.into_iter()
19841986
.map(|pr| pr.to_vec())
19851987
.collect(),
1988+
rejected_reason: txn.cancelled.map(|r| r.to_string()).unwrap_or_default(),
19861989
};
19871990

19881991
let response = GetCompletedTransactionsResponse {
@@ -2125,6 +2128,7 @@ impl wallet_server::Wallet for WalletGrpcServer {
21252128
.into_iter()
21262129
.map(|pr| pr.to_vec())
21272130
.collect(),
2131+
rejected_reason: txn.cancelled.map(|r| r.to_string()).unwrap_or_default(),
21282132
});
21292133
}
21302134

@@ -2647,8 +2651,52 @@ impl wallet_server::Wallet for WalletGrpcServer {
26472651
.map_err(|_| Status::invalid_argument("payment_reference must be exactly 32 bytes".to_string()))?;
26482652
let mut tms = self.get_transaction_service();
26492653

2650-
match tms.get_transaction_by_payref(payment_ref).await {
2651-
Ok(txn) => {
2654+
// Look up current payref
2655+
let current_tx = tms.get_transaction_by_payref(payment_ref).await.ok();
2656+
2657+
// Look up historical payrefs (from reorgs)
2658+
let historical_txs = tms
2659+
.get_transaction_by_historical_payref(payment_ref)
2660+
.await
2661+
.unwrap_or_else(|e| {
2662+
debug!(
2663+
target: LOG_TARGET,
2664+
"get_payment_by_reference: Error looking up historical PayRef: {e}"
2665+
);
2666+
vec![]
2667+
});
2668+
2669+
if current_tx.is_none() && historical_txs.is_empty() {
2670+
return Err(Status::not_found(format!("PayRef {} not found", payment_ref.to_hex())));
2671+
}
2672+
2673+
let transaction = current_tx.map(|txn| {
2674+
let output_commitments: Vec<Vec<u8>> = txn
2675+
.transaction
2676+
.body
2677+
.outputs()
2678+
.iter()
2679+
.map(|o| o.commitment().as_bytes().to_vec())
2680+
.collect();
2681+
let input_commitments: Vec<Vec<u8>> = txn
2682+
.transaction
2683+
.body
2684+
.inputs()
2685+
.iter()
2686+
.map(|i| match i.commitment() {
2687+
Ok(c) => c.as_bytes().to_vec(),
2688+
Err(e) => {
2689+
warn!(target: LOG_TARGET, "Failed to get input commitment: {e}");
2690+
vec![]
2691+
},
2692+
})
2693+
.collect();
2694+
completed_tx_to_transaction_info(&txn, output_commitments, input_commitments)
2695+
});
2696+
2697+
let historical_transactions = historical_txs
2698+
.iter()
2699+
.map(|txn| {
26522700
let output_commitments: Vec<Vec<u8>> = txn
26532701
.transaction
26542702
.body
@@ -2669,57 +2717,14 @@ impl wallet_server::Wallet for WalletGrpcServer {
26692717
},
26702718
})
26712719
.collect();
2672-
let transaction_info = TransactionInfo {
2673-
tx_id: txn.tx_id.into(),
2674-
source_address: txn.source_address.to_vec(),
2675-
dest_address: txn.destination_address.to_vec(),
2676-
status: TransactionStatus::from(txn.status) as i32,
2677-
amount: txn.amount.into(),
2678-
is_cancelled: txn.cancelled.is_some(),
2679-
direction: TransactionDirection::from(txn.direction) as i32,
2680-
fee: txn.fee.into(),
2681-
timestamp: txn.timestamp.timestamp() as u64,
2682-
excess_sig: txn
2683-
.transaction
2684-
.first_kernel_excess_sig()
2685-
.unwrap_or(&CompressedSignature::default())
2686-
.get_signature()
2687-
.to_vec(),
2688-
raw_payment_id: txn.payment_id.to_bytes(),
2689-
user_payment_id: txn.payment_id.payment_id_as_bytes(),
2690-
mined_in_block_height: txn.mined_height.unwrap_or(0),
2691-
output_commitments,
2692-
input_commitments,
2693-
payment_references_sent: txn
2694-
.calculate_sent_payment_references()
2695-
.into_iter()
2696-
.map(|pr| pr.to_vec())
2697-
.collect(),
2698-
payment_references_received: txn
2699-
.calculate_received_payment_references()
2700-
.into_iter()
2701-
.map(|pr| pr.to_vec())
2702-
.collect(),
2703-
payment_references_change: txn
2704-
.calculate_change_payment_references()
2705-
.into_iter()
2706-
.map(|pr| pr.to_vec())
2707-
.collect(),
2708-
};
2709-
Ok(Response::new(GetPaymentByReferenceResponse {
2710-
transaction: Some(transaction_info),
2711-
}))
2712-
},
2713-
Err(e) => {
2714-
warn!(
2715-
target: LOG_TARGET,
2716-
"get_transaction_by_payref: Error looking up PayRef {}: {}",
2717-
payment_ref.to_hex(),
2718-
e
2719-
);
2720-
Err(Status::internal(format!("Error looking up payment reference: {e}")))
2721-
},
2722-
}
2720+
completed_tx_to_transaction_info(txn, output_commitments, input_commitments)
2721+
})
2722+
.collect();
2723+
2724+
Ok(Response::new(GetPaymentByReferenceResponse {
2725+
transaction,
2726+
historical_transactions,
2727+
}))
27232728
}
27242729

27252730
async fn get_transaction_pay_refs(
@@ -2792,10 +2797,29 @@ impl wallet_server::Wallet for WalletGrpcServer {
27922797
}
27932798
};
27942799

2800+
// Fetch historical payrefs for this transaction
2801+
let historical_payment_references = match transaction_service.get_payref_history_by_tx_id(tx_id).await {
2802+
Ok(history) => history
2803+
.into_iter()
2804+
.map(|(output_hash, payref)| tari_rpc::HistoricalPayRef {
2805+
output_hash: output_hash.to_vec(),
2806+
payment_reference: payref.to_vec(),
2807+
})
2808+
.collect(),
2809+
Err(e) => {
2810+
debug!(
2811+
target: LOG_TARGET,
2812+
"get_transaction_pay_refs: Error fetching payref history for {}: {}", tx_id, e
2813+
);
2814+
vec![]
2815+
},
2816+
};
2817+
27952818
Ok(Response::new(GetTransactionPayRefsResponse {
27962819
#[allow(deprecated)]
27972820
payment_references,
27982821
output_commitments_info: get_transaction_output_commitments_info(&completed_tx),
2822+
historical_payment_references,
27992823
}))
28002824
}
28012825

@@ -3477,6 +3501,51 @@ async fn import_output_to_oms<KM: LegacyTransactionKeyManagerInterface>(
34773501
Ok(found_outputs.first().expect("already checked for empty").1.clone())
34783502
}
34793503

3504+
fn completed_tx_to_transaction_info(
3505+
txn: &CompletedTransaction,
3506+
output_commitments: Vec<Vec<u8>>,
3507+
input_commitments: Vec<Vec<u8>>,
3508+
) -> TransactionInfo {
3509+
TransactionInfo {
3510+
tx_id: txn.tx_id.into(),
3511+
source_address: txn.source_address.to_vec(),
3512+
dest_address: txn.destination_address.to_vec(),
3513+
status: TransactionStatus::from(txn.status) as i32,
3514+
amount: txn.amount.into(),
3515+
is_cancelled: txn.cancelled.is_some(),
3516+
direction: TransactionDirection::from(txn.direction) as i32,
3517+
fee: txn.fee.into(),
3518+
timestamp: txn.timestamp.timestamp() as u64,
3519+
excess_sig: txn
3520+
.transaction
3521+
.first_kernel_excess_sig()
3522+
.unwrap_or(&CompressedSignature::default())
3523+
.get_signature()
3524+
.to_vec(),
3525+
raw_payment_id: txn.payment_id.to_bytes(),
3526+
user_payment_id: txn.payment_id.payment_id_as_bytes(),
3527+
mined_in_block_height: txn.mined_height.unwrap_or(0),
3528+
output_commitments,
3529+
input_commitments,
3530+
payment_references_sent: txn
3531+
.calculate_sent_payment_references()
3532+
.into_iter()
3533+
.map(|pr| pr.to_vec())
3534+
.collect(),
3535+
payment_references_received: txn
3536+
.calculate_received_payment_references()
3537+
.into_iter()
3538+
.map(|pr| pr.to_vec())
3539+
.collect(),
3540+
payment_references_change: txn
3541+
.calculate_change_payment_references()
3542+
.into_iter()
3543+
.map(|pr| pr.to_vec())
3544+
.collect(),
3545+
rejected_reason: txn.cancelled.map(|r| r.to_string()).unwrap_or_default(),
3546+
}
3547+
}
3548+
34803549
#[allow(clippy::too_many_lines)]
34813550
fn convert_wallet_transaction_into_transaction_info(
34823551
tx: models::WalletTransaction,
@@ -3508,6 +3577,7 @@ fn convert_wallet_transaction_into_transaction_info(
35083577
payment_references_sent: vec![],
35093578
payment_references_received: vec![],
35103579
payment_references_change: vec![],
3580+
rejected_reason: String::new(),
35113581
}
35123582
},
35133583
PendingOutbound(tx) => {
@@ -3544,6 +3614,7 @@ fn convert_wallet_transaction_into_transaction_info(
35443614
payment_references_sent: vec![],
35453615
payment_references_received: vec![],
35463616
payment_references_change: vec![],
3617+
rejected_reason: String::new(),
35473618
}
35483619
},
35493620
Completed(tx) => {
@@ -3602,6 +3673,7 @@ fn convert_wallet_transaction_into_transaction_info(
36023673
.into_iter()
36033674
.map(|pr| pr.to_vec())
36043675
.collect(),
3676+
rejected_reason: tx.cancelled.map(|r| r.to_string()).unwrap_or_default(),
36053677
}
36063678
},
36073679
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Undo payref history table
2+
DROP INDEX IF EXISTS idx_payref_history_payref;
3+
DROP INDEX IF EXISTS idx_payref_history_tx_id;
4+
DROP TABLE payref_history;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- Migration to add PayRef history tracking for reorgs
2+
3+
CREATE TABLE payref_history
4+
(
5+
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
6+
output_hash BLOB NOT NULL,
7+
payref BLOB NOT NULL,
8+
tx_id BIGINT NOT NULL,
9+
superseded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
10+
);
11+
12+
CREATE INDEX IF NOT EXISTS idx_payref_history_payref ON payref_history(payref);
13+
CREATE INDEX IF NOT EXISTS idx_payref_history_tx_id ON payref_history(tx_id);

0 commit comments

Comments
 (0)