Skip to content

feat: add option to do search via payment id #6993

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

Merged
merged 9 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 9 additions & 1 deletion applications/minotari_app_grpc/proto/wallet.proto
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,15 @@ message GetCompletedTransactionsResponse {
TransactionInfo transaction = 1;
}

message GetBalanceRequest {}
message GetBalanceRequest {
user_payment_id payment_id = 1;
}

message user_payment_id {
bytes u256 = 1;
string utf8_string = 2;
bytes user_bytes = 3;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only use U256 or string in this interface. With PaymentId bytes are interpreted as a string.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if your bytes is not a standard utf-8 string?


message GetStateRequest {}

Expand Down
2 changes: 1 addition & 1 deletion applications/minotari_console_wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ tui ={ version = "^0.19", default-features = false, features = ["crossterm"]}
tari_features = { workspace = true }

[features]
default = ["libtor", "ledger"]
default = ["libtor", "ledger", "grpc"]
grpc = []
ledger = ["minotari_ledger_wallet_comms", "minotari_wallet/ledger"]
libtor = ["tari_libtor"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,35 @@ impl wallet_server::Wallet for WalletGrpcServer {
Ok(Response::new(SetBaseNodeResponse {}))
}

async fn get_balance(&self, _request: Request<GetBalanceRequest>) -> Result<Response<GetBalanceResponse>, Status> {
async fn get_balance(&self, request: Request<GetBalanceRequest>) -> Result<Response<GetBalanceResponse>, Status> {
let message = request.into_inner();
let start = std::time::Instant::now();
if let Some(user_payment_id) = message.payment_id{
let bytes = match (user_payment_id.u256.is_empty(), user_payment_id.utf8_string.is_empty(), user_payment_id.user_bytes.is_empty()){
(false, true, true) => {
user_payment_id.u256
},
(true, false, true) => {
user_payment_id.utf8_string.as_bytes().to_vec()
},
(true, true, false) => {
user_payment_id.user_bytes
},
_ => {
return Err(Status::invalid_argument("user_payment_id must be one of u256, utf8_string or user_bytes".to_string()));
}
};
let mut oms = self.get_output_manager_service();
let balance = oms.get_balance_for_payment_id(bytes).await.map_err(|e| {
Status::not_found(format!("WalletDebouncer error! {}", e))
})?;
return Ok(Response::new(GetBalanceResponse {
available_balance: balance.available_balance.into(),
pending_incoming_balance: balance.pending_incoming_balance.into(),
pending_outgoing_balance: balance.pending_outgoing_balance.into(),
timelocked_balance: balance.time_locked_balance.unwrap_or_default().into(),
}))
}
let balance = {
let mut get_balance = self.debouncer.lock().await;
match get_balance.get_balance().await {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- This file should undo anything in `up.sql`
13 changes: 13 additions & 0 deletions base_layer/wallet/migrations/2025-04-25-161400_payment_id/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Any old 'outputs' will not be valid due to the removal of 'coinbase_block_height' and removal of default value for
-- 'spending_priority', so we drop and recreate the table.
ALTER TABLE outputs
ADD user_payment_id BLOB NULL;

ALTER TABLE completed_transactions
ADD user_payment_id BLOB NULL;

ALTER TABLE inbound_transactions
ADD user_payment_id BLOB NULL;

ALTER TABLE outbound_transactions
ADD user_payment_id BLOB NULL;
9 changes: 9 additions & 0 deletions base_layer/wallet/src/output_manager_service/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ use crate::output_manager_service::{
#[allow(clippy::large_enum_variant)]
pub enum OutputManagerRequest {
GetBalance,
GetBalancePaymentId(Vec<u8>),
AddOutput((Box<WalletOutput>, Option<SpendingPriority>)),
AddOutputWithTxId((TxId, Box<WalletOutput>, Option<SpendingPriority>)),
AddUnvalidatedOutput((TxId, Box<WalletOutput>, Option<SpendingPriority>)),
Expand Down Expand Up @@ -165,6 +166,7 @@ impl fmt::Display for OutputManagerRequest {
use OutputManagerRequest::*;
match self {
GetBalance => write!(f, "GetBalance"),
GetBalancePaymentId(_) => write!(f, "GetBalance for user payment id"),
AddOutput((v, _)) => write!(f, "AddOutput ({})", v.value),
AddOutputWithTxId((t, v, _)) => write!(f, "AddOutputWithTxId ({}: {})", t, v.value),
AddUnvalidatedOutput((t, v, _)) => {
Expand Down Expand Up @@ -497,6 +499,13 @@ impl OutputManagerHandle {
}
}

pub async fn get_balance_for_payment_id(&mut self, payment_id: Vec<u8>) -> Result<Balance, OutputManagerError> {
match self.handle.call(OutputManagerRequest::GetBalancePaymentId(payment_id)).await?? {
OutputManagerResponse::Balance(b) => Ok(b),
_ => Err(OutputManagerError::UnexpectedApiResponse),
}
}

pub async fn revalidate_all_outputs(&mut self) -> Result<u64, OutputManagerError> {
match self.handle.call(OutputManagerRequest::RevalidateTxos).await?? {
OutputManagerResponse::TxoValidationStarted(request_key) => Ok(request_key),
Expand Down
14 changes: 14 additions & 0 deletions base_layer/wallet/src/output_manager_service/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,14 @@ where
self.get_balance(current_tip_for_time_lock_calculation)
.map(OutputManagerResponse::Balance)
},
OutputManagerRequest::GetBalancePaymentId(payment_id) => {
let current_tip_for_time_lock_calculation = match self.base_node_service.get_chain_metadata().await {
Ok(metadata) => metadata.map(|m| m.best_block_height()),
Err(_) => None,
};
self.get_balance_payment_id(current_tip_for_time_lock_calculation, payment_id)
.map(OutputManagerResponse::Balance)
},
OutputManagerRequest::GetRecipientTransaction(tsm) => self
.get_default_recipient_transaction(tsm)
.await
Expand Down Expand Up @@ -805,6 +813,12 @@ where
Ok(balance)
}

fn get_balance_payment_id(&self, current_tip_for_time_lock_calculation: Option<u64>, payment_id: Vec<u8>) -> Result<Balance, OutputManagerError> {
let balance = self.resources.db.get_balance_payment_id(current_tip_for_time_lock_calculation, payment_id)?;
trace!(target: LOG_TARGET, "Balance: {:?}", balance);
Ok(balance)
}

/// Request a receiver transaction be generated from the supplied Sender Message
#[allow(clippy::too_many_lines)]
async fn get_default_recipient_transaction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ pub trait OutputManagerBackend: Send + Sync + Clone {
fn reinstate_cancelled_inbound_output(&self, tx_id: TxId) -> Result<(), OutputManagerStorageError>;
/// Return the available, time locked, pending incoming and pending outgoing balance
fn get_balance(&self, tip: Option<u64>) -> Result<Balance, OutputManagerStorageError>;
/// Return the available, time locked, pending incoming and pending outgoing balance only matching the payment id
fn get_balance_payment_id(&self, tip: Option<u64>, payment_id: Vec<u8>) -> Result<Balance, OutputManagerStorageError>;
/// Import unvalidated output
fn add_unvalidated_output(&self, output: DbWalletOutput, tx_id: TxId) -> Result<(), OutputManagerStorageError>;
fn fetch_unspent_outputs_for_spending(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ where T: OutputManagerBackend + 'static
self.db.get_balance(current_tip_for_time_lock_calculation)
}

pub fn get_balance_payment_id(
&self,
current_tip_for_time_lock_calculation: Option<u64>, payment_id : Vec<u8>
) -> Result<Balance, OutputManagerStorageError> {
self.db.get_balance_payment_id(current_tip_for_time_lock_calculation, payment_id)
}

/// This method is called when a transaction is built to be sent. It will encumber unspent outputs against a pending
/// transaction in the short term.
pub fn encumber_outputs(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ use tari_core::transactions::{
use tari_crypto::tari_utilities::{hex::Hex, ByteArray};
use tari_script::{ExecutionStack, TariScript};
use tokio::time::Instant;

use crate::{
output_manager_service::{
error::OutputManagerStorageError,
Expand Down Expand Up @@ -966,6 +965,28 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase {
result
}

fn get_balance_payment_id(
&self,
current_tip_for_time_lock_calculation: Option<u64>,
payment_id: Vec<u8>
) -> Result<Balance, OutputManagerStorageError> {
let start = Instant::now();
let mut conn = self.database_connection.get_pooled_connection()?;
let acquire_lock = start.elapsed();

let result = OutputSql::get_balance_payment_id(current_tip_for_time_lock_calculation, payment_id, &mut conn);
if start.elapsed().as_millis() > 0 {
trace!(
target: LOG_TARGET,
"sqlite profile - get_balance: lock {} + db_op {} = {} ms",
acquire_lock.as_millis(),
(start.elapsed() - acquire_lock).as_millis(),
start.elapsed().as_millis()
);
}
result
}

fn cancel_pending_transaction(&self, tx_id: TxId) -> Result<(), OutputManagerStorageError> {
let start = Instant::now();
let mut conn = self.database_connection.get_pooled_connection()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub struct NewOutputSql {
pub source: i32,
pub spending_priority: i32,
pub payment_id: Option<Vec<u8>>,
pub user_payment_id: Option<Vec<u8>>,
}

impl NewOutputSql {
Expand All @@ -78,6 +79,13 @@ impl NewOutputSql {
let mut covenant = Vec::new();
BorshSerialize::serialize(&output.wallet_output.covenant, &mut covenant)?;

let user_payment_id = output.payment_id.user_data_as_bytes();
let user_payment_id = if user_payment_id.is_empty(){
None
} else {
Some(user_payment_id)
};

let output = Self {
commitment: output.commitment.to_vec(),
spending_key: output.wallet_output.spending_key_id.to_string(),
Expand Down Expand Up @@ -113,6 +121,7 @@ impl NewOutputSql {
source: output.source as i32,
spending_priority: output.spending_priority.into(),
payment_id: Some(output.payment_id.to_bytes()),
user_payment_id,
};

Ok(output)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub struct OutputSql {
pub source: i32,
pub last_validation_timestamp: Option<NaiveDateTime>,
pub payment_id: Option<Vec<u8>>,
pub user_payment_id: Option<Vec<u8>>,
}

impl OutputSql {
Expand Down Expand Up @@ -524,6 +525,125 @@ impl OutputSql {
})
}

/// Return the available, time locked, pending incoming and pending outgoing balance
#[allow(clippy::cast_possible_wrap)]
pub fn get_balance_payment_id(
current_tip_for_time_lock_calculation: Option<u64>,
payment_id:Vec<u8>,
conn: &mut SqliteConnection,
) -> Result<Balance, OutputManagerStorageError> {
#[derive(QueryableByName, Clone)]
struct BalanceQueryResult {
#[diesel(sql_type = diesel::sql_types::BigInt)]
amount: i64,
#[diesel(sql_type = diesel::sql_types::Text)]
category: String,
}
let balance_query_result = if let Some(current_tip) = current_tip_for_time_lock_calculation {
let balance_query = sql_query(
"SELECT coalesce(sum(value), 0) as amount, 'available_balance' as category \
FROM outputs WHERE status = ? AND maturity <= ? AND script_lock_height <= ? AND user_payment_id = ? \
UNION ALL \
SELECT coalesce(sum(value), 0) as amount, 'time_locked_balance' as category \
FROM outputs WHERE status = ? AND maturity > ? OR script_lock_height > ? AND user_payment_id = ? \
UNION ALL \
SELECT coalesce(sum(value), 0) as amount, 'pending_incoming_balance' as category \
FROM outputs WHERE source != ? AND status = ? OR status = ? OR status = ? AND user_payment_id = ? \
UNION ALL \
SELECT coalesce(sum(value), 0) as amount, 'pending_outgoing_balance' as category \
FROM outputs WHERE status = ? OR status = ? OR status = ? AND user_payment_id = ?",
)
// available_balance
.bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
.bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
.bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
.bind::<diesel::sql_types::Binary, _>(payment_id.clone())
// time_locked_balance
.bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
.bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
.bind::<diesel::sql_types::BigInt, _>(current_tip as i64)
.bind::<diesel::sql_types::Binary, _>(payment_id.clone())
// pending_incoming_balance
.bind::<diesel::sql_types::Integer, _>(OutputSource::Coinbase as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeReceived as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeReceived as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::UnspentMinedUnconfirmed as i32)
.bind::<diesel::sql_types::Binary, _>(payment_id.clone())
// pending_outgoing_balance
.bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeSpent as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeSpent as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::SpentMinedUnconfirmed as i32)
.bind::<diesel::sql_types::Binary, _>(payment_id);
balance_query.load::<BalanceQueryResult>(conn)?
} else {
let balance_query = sql_query(
"SELECT coalesce(sum(value), 0) as amount, 'available_balance' as category \
FROM outputs WHERE status = ? AND user_payment_id = ?\
UNION ALL \
SELECT coalesce(sum(value), 0) as amount, 'pending_incoming_balance' as category \
FROM outputs WHERE source != ? AND status = ? OR status = ? OR status = ? AND user_payment_id = ? \
UNION ALL \
SELECT coalesce(sum(value), 0) as amount, 'pending_outgoing_balance' as category \
FROM outputs WHERE status = ? OR status = ? OR status = ? AND user_payment_id = ?",
)
// available_balance
.bind::<diesel::sql_types::Integer, _>(OutputStatus::Unspent as i32)
.bind::<diesel::sql_types::Binary, _>(payment_id.clone())
// pending_incoming_balance
.bind::<diesel::sql_types::Integer, _>(OutputSource::Coinbase as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeReceived as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeReceived as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::UnspentMinedUnconfirmed as i32)
.bind::<diesel::sql_types::Binary, _>(payment_id.clone())
// pending_outgoing_balance
.bind::<diesel::sql_types::Integer, _>(OutputStatus::EncumberedToBeSpent as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::ShortTermEncumberedToBeSpent as i32)
.bind::<diesel::sql_types::Integer, _>(OutputStatus::SpentMinedUnconfirmed as i32)
.bind::<diesel::sql_types::Binary, _>(payment_id);
balance_query.load::<BalanceQueryResult>(conn)?
};
let mut available_balance = None;
let mut time_locked_balance = Some(None);
let mut pending_incoming_balance = None;
let mut pending_outgoing_balance = None;
for balance in balance_query_result {
match balance.category.as_str() {
"available_balance" => available_balance = Some(MicroMinotari::from(balance.amount as u64)),
"time_locked_balance" => time_locked_balance = Some(Some(MicroMinotari::from(balance.amount as u64))),
"pending_incoming_balance" => {
pending_incoming_balance = Some(MicroMinotari::from(balance.amount as u64))
},
"pending_outgoing_balance" => {
pending_outgoing_balance = Some(MicroMinotari::from(balance.amount as u64))
},
_ => {
return Err(OutputManagerStorageError::UnexpectedResult(
"Unexpected category in balance query".to_string(),
))
},
}
}

Ok(Balance {
available_balance: available_balance.ok_or_else(|| {
OutputManagerStorageError::UnexpectedResult("Available balance could not be calculated".to_string())
})?,
time_locked_balance: time_locked_balance.ok_or_else(|| {
OutputManagerStorageError::UnexpectedResult("Time locked balance could not be calculated".to_string())
})?,
pending_incoming_balance: pending_incoming_balance.ok_or_else(|| {
OutputManagerStorageError::UnexpectedResult(
"Pending incoming balance could not be calculated".to_string(),
)
})?,
pending_outgoing_balance: pending_outgoing_balance.ok_or_else(|| {
OutputManagerStorageError::UnexpectedResult(
"Pending outgoing balance could not be calculated".to_string(),
)
})?,
})
}

pub fn find_by_commitment(
commitment: &[u8],
conn: &mut SqliteConnection,
Expand Down
4 changes: 4 additions & 0 deletions base_layer/wallet/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ diesel::table! {
transaction_signature_nonce -> Binary,
transaction_signature_key -> Binary,
payment_id -> Nullable<Binary>,
user_payment_id -> Nullable<Binary>,
}
}

Expand All @@ -52,6 +53,7 @@ diesel::table! {
send_count -> Integer,
last_send_timestamp -> Nullable<Timestamp>,
payment_id -> Nullable<Binary>,
user_payment_id -> Nullable<Binary>,
}
}

Expand All @@ -78,6 +80,7 @@ diesel::table! {
send_count -> Integer,
last_send_timestamp -> Nullable<Timestamp>,
payment_id -> Nullable<Binary>,
user_payment_id -> Nullable<Binary>,
}
}

Expand Down Expand Up @@ -118,6 +121,7 @@ diesel::table! {
source -> Integer,
last_validation_timestamp -> Nullable<Timestamp>,
payment_id -> Nullable<Binary>,
user_payment_id -> Nullable<Binary>,
}
}

Expand Down
Loading
Loading