diff --git a/applications/tari_indexer/src/network_client.rs b/applications/tari_indexer/src/network_client.rs index 2c079a1691..8d7a6966ae 100644 --- a/applications/tari_indexer/src/network_client.rs +++ b/applications/tari_indexer/src/network_client.rs @@ -38,7 +38,7 @@ where } pub async fn submit_transaction(&self, transaction: Transaction) -> Result { - if !transaction.is_shard_applicable() { + if !transaction.has_inputs() { return Err(NetworkClientError::NoInputsProvided); } diff --git a/applications/tari_swarm_daemon/src/process_definitions/wallet_daemon_create_key.rs b/applications/tari_swarm_daemon/src/process_definitions/wallet_daemon_create_key.rs index af1a5af153..1b4a8f3540 100644 --- a/applications/tari_swarm_daemon/src/process_definitions/wallet_daemon_create_key.rs +++ b/applications/tari_swarm_daemon/src/process_definitions/wallet_daemon_create_key.rs @@ -43,8 +43,6 @@ impl ProcessDefinition for WalletDaemonCreateAccount { "create-account", "--name", "Validator Fees", - "--key", - "0", "--set-active", "--output", output_path diff --git a/applications/tari_validator_node/src/p2p/services/mempool/service.rs b/applications/tari_validator_node/src/p2p/services/mempool/service.rs index f2c6feca43..f573a014ae 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/service.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/service.rs @@ -236,7 +236,7 @@ where return Err(e.into()); } - if !transaction.is_shard_applicable() { + if !transaction.has_inputs() { warn!(target: LOG_TARGET, "⚠ No involved shards for transaction {tx_id}"); return Err(MempoolError::TransactionValidationError( TransactionValidationError::NoInvolvedShards { transaction_id: tx_id }, diff --git a/applications/tari_validator_node/src/transaction_validators/error.rs b/applications/tari_validator_node/src/transaction_validators/error.rs index 068fad52c8..0416559aa5 100644 --- a/applications/tari_validator_node/src/transaction_validators/error.rs +++ b/applications/tari_validator_node/src/transaction_validators/error.rs @@ -36,6 +36,8 @@ pub enum TransactionValidationError { NoInvolvedShards { transaction_id: TransactionId }, #[error("Invalid transaction signature")] InvalidSignature, + #[error("Transaction {transaction_id} has no main signer")] + NoMainSigner { transaction_id: TransactionId }, #[error("Transaction {transaction_id} is not signed")] TransactionNotSigned { transaction_id: TransactionId }, #[error("Network error: {0}")] diff --git a/applications/tari_validator_node/src/transaction_validators/is_shard_applicable.rs b/applications/tari_validator_node/src/transaction_validators/is_shard_applicable.rs index fb5753a0a7..afa11ca32e 100644 --- a/applications/tari_validator_node/src/transaction_validators/is_shard_applicable.rs +++ b/applications/tari_validator_node/src/transaction_validators/is_shard_applicable.rs @@ -24,7 +24,7 @@ impl Validator for IsShardApplicable { type Error = TransactionValidationError; fn validate(&self, _context: &(), transaction: &Transaction) -> Result<(), Self::Error> { - if !transaction.is_shard_applicable() { + if !transaction.has_inputs() { warn!(target: LOG_TARGET, "HasInputs - FAIL: No input shards"); return Err(TransactionValidationError::NoInputs { transaction_id: transaction.calculate_id(), diff --git a/applications/tari_validator_node/src/transaction_validators/signature.rs b/applications/tari_validator_node/src/transaction_validators/signature.rs index 9a6678d6ef..d1e2430fc4 100644 --- a/applications/tari_validator_node/src/transaction_validators/signature.rs +++ b/applications/tari_validator_node/src/transaction_validators/signature.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use log::warn; +use tari_engine::executables::Executable; use tari_transaction::Transaction; use crate::{transaction_validators::TransactionValidationError, validator::Validator}; @@ -16,6 +17,13 @@ impl Validator for TransactionSignatureValidator { type Error = TransactionValidationError; fn validate(&self, _context: &(), transaction: &Transaction) -> Result<(), TransactionValidationError> { + if transaction.main_signer().is_none() { + warn!(target: LOG_TARGET, "TransactionSignatureValidator - FAIL: No main signer"); + return Err(TransactionValidationError::NoMainSigner { + transaction_id: transaction.to_id(), + }); + } + if !transaction.verify_all_signatures() { warn!(target: LOG_TARGET, "TransactionSignatureValidator - FAIL: Invalid signature"); return Err(TransactionValidationError::InvalidSignature); diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index 7baf2348fe..83e36084e9 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -18,9 +18,9 @@ use tari_engine_types::{ use tari_ootle_common_types::{optional::Optional, SubstateRequirement}; use tari_ootle_wallet_crypto::{ memo::Memo, - UnblindedOutputStatement, - UnblindedStealthInputStatement, - UnblindedStealthOutputStatement, + UnblindedOutputWitness, + UnblindedStealthInputWitness, + UnblindedStealthOutputWitness, }; use tari_ootle_wallet_sdk::{ apis::{ @@ -513,8 +513,8 @@ pub async fn handle_claim_burn( sdk.stealth_crypto_api() .derive_stealth_owner_public_key(network, &account_owner_public_key, &nonce); - let output_statement = UnblindedStealthOutputStatement { - statement: UnblindedOutputStatement { + let output_statement = UnblindedStealthOutputWitness { + witness: UnblindedOutputWitness { amount: final_amount, mask: mask.key, sender_public_nonce: output_public_nonce.clone(), @@ -527,7 +527,7 @@ pub async fn handle_claim_burn( }; // Generate the correct secret to spend the claimed output - let input = UnblindedStealthInputStatement { + let input = UnblindedStealthInputWitness { mask_and_value: decrypted.into_mask_and_value(), owner_secret: claim_nonce_keypair.secret_key().clone(), public_nonce: reciprocal_claim_public_key_expanded, @@ -946,6 +946,12 @@ pub async fn handle_stealth_transfer( let network = sdk.sdk_config().network; let notifier = context.notifier().clone(); let owner_account = get_account(&req.owner_account, &sdk.accounts_api())?; + let Some(owner_key_id) = owner_account.owner_key_id() else { + return Err(invalid_params( + "owner_account", + Some("cannot transfer from an account without an owner key"), + )); + }; let params = StealthTransferParams { input_selection: req.input_selection, @@ -968,42 +974,56 @@ pub async fn handle_stealth_transfer( task::spawn(async move { let transfer = sdk.stealth_transfer_api().transfer(owner_account, params).await?; + let must_sign_with_account_key = + transfer.fee_inputs.revealed.is_positive() || transfer.transfer_inputs.revealed.is_positive(); + let signer_key = if must_sign_with_account_key { + sdk.key_manager_api().get_account_owner_key(owner_key_id)? + } else { + // Since we don't require account auth, use a throwaway nonce to sign the transaction + sdk.key_manager_api().next_key(KeyBranch::Nonce)?.into() + }; + + let transaction = transfer + .transaction + .authorized_sealed_signer() + .build(vec![]) + .seal(&signer_key.secret); + // TODO: if submitting fails we need to unlock the inputs again if req.dry_run { - let transaction_id = transfer.transaction.calculate_id(); - let result = transaction_service - .submit_dry_run_transaction(transfer.transaction) - .await; + // Release the lock immediately as dry run does not submit the transaction + // TODO: maybe transfer() should not lock the outputs if it's a dry run + if let Err(err) = sdk.stealth_outputs_api().release_lock(transfer.lock_id) { + error!( + target: LOG_TARGET, + "Failed to release locked outputs for dry run : {}", + err + ); + } + let result = transaction_service.submit_dry_run_transaction(transaction).await; return match result { - Ok(_) => Ok(StealthTransferResponse { transaction_id }), + Ok(res) => Ok(StealthTransferResponse { + transaction_id: res.finalize.transaction_hash.into(), + }), Err(e) => { - if let Err(err) = sdk - .stealth_outputs_api() - .release_locked_outputs(transfer.transaction_lock_id) - { + if let Err(err) = sdk.stealth_outputs_api().release_lock(transfer.lock_id) { error!( target: LOG_TARGET, "Failed to release locked outputs after dry run failure: {}", err ); } - if let Err(err) = sdk - .stealth_outputs_api() - .release_revealed_funds(transfer.transaction_lock_id) - { - error!( - target: LOG_TARGET, - "Failed to release revealed funds after dry run failure: {}", - err - ); - } Err(anyhow::anyhow!("Dry run transaction failed: {}", e)) }, }; } - let result = transaction_service.submit_transaction(transfer.transaction).await; + // Associate lock with transaction + sdk.stealth_outputs_api() + .locks_set_transaction_id(transfer.lock_id, transaction.calculate_id())?; + + let result = transaction_service.submit_transaction(transaction).await; match result { Ok(tx_id) => { notifier.notify(TransactionSubmittedEvent { @@ -1014,26 +1034,13 @@ pub async fn handle_stealth_transfer( Ok(StealthTransferResponse { transaction_id: tx_id }) }, Err(e) => { - if let Err(err) = sdk - .stealth_outputs_api() - .release_locked_outputs(transfer.transaction_lock_id) - { + if let Err(err) = sdk.stealth_outputs_api().release_lock(transfer.lock_id) { error!( target: LOG_TARGET, "Failed to release locked outputs after submission failure: {}", err ); } - if let Err(err) = sdk - .stealth_outputs_api() - .release_revealed_funds(transfer.transaction_lock_id) - { - error!( - target: LOG_TARGET, - "Failed to release revealed funds after submission failure: {}", - err - ); - } Err(anyhow::anyhow!("Transaction submission failed: {}", e)) }, @@ -1060,23 +1067,10 @@ pub async fn handle_associate_stealth_resource( )); } - // Ensure the resource is in the local cache - if !sdk.resources_api().exists(&req.resource_address)? { - let substate = sdk - .substate_api() - .get_substate_from_network(req.resource_address.into()) - .await?; - let resource = substate.into_substate_value().into_resource().ok_or_else(|| { - general_error(format!( - "Indexer returned Substate at address {} is not a resource", - req.resource_address - )) - })?; - sdk.resources_api().upsert_resource(&req.resource_address, &resource)?; - } - - sdk.accounts_api() - .associate_stealth_resource(account.component_address(), req.resource_address)?; + context + .account_monitor() + .associate_resource(*account.component_address(), req.resource_address) + .await?; context .account_monitor() diff --git a/applications/tari_walletd/src/handlers/confidential.rs b/applications/tari_walletd/src/handlers/confidential.rs index 057589b793..d8753b809c 100644 --- a/applications/tari_walletd/src/handlers/confidential.rs +++ b/applications/tari_walletd/src/handlers/confidential.rs @@ -11,7 +11,7 @@ use rand::rngs::OsRng; use serde_json::json; use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::PublicKey as _, ristretto::RistrettoPublicKey}; use tari_engine_types::{crypto::get_commitment_factory, ToByteType}; -use tari_ootle_wallet_crypto::{AlwaysMissLookupTable, IoReaderValueLookup, UnblindedOutputStatement}; +use tari_ootle_wallet_crypto::{AlwaysMissLookupTable, IoReaderValueLookup, UnblindedOutputWitness}; use tari_ootle_wallet_sdk::{ apis::key_manager::KeyBranch, models::{ConfidentialOutputModel, OutputStatus}, @@ -114,7 +114,7 @@ pub async fn handle_create_transfer_proof( ) })?; - let output_statement = UnblindedOutputStatement { + let output_statement = UnblindedOutputWitness { amount: req.amount, mask: output_mask.key, sender_public_nonce: public_nonce, @@ -171,7 +171,7 @@ pub async fn handle_create_transfer_proof( lock_id: Some(lock_id), })?; - Some(UnblindedOutputStatement { + Some(UnblindedOutputWitness { amount: change_amount, mask: change_mask.key, sender_public_nonce: public_nonce, @@ -253,7 +253,7 @@ pub async fn handle_create_output_proof( None, )?; - let statement = UnblindedOutputStatement { + let statement = UnblindedOutputWitness { amount: req.amount, mask: output_mask.key, sender_public_nonce: public_nonce, diff --git a/applications/tari_walletd/src/lib.rs b/applications/tari_walletd/src/lib.rs index 27bc2b90bd..738b120dea 100644 --- a/applications/tari_walletd/src/lib.rs +++ b/applications/tari_walletd/src/lib.rs @@ -35,10 +35,7 @@ use log::*; use tari_common_types::seeds::seed_words::SeedWords; use tari_ootle_common_types::{optional::Optional, NumPreshards}; use tari_ootle_wallet_sdk::{ - apis::{ - config::{ConfigApi, ConfigKey}, - key_manager::KeyBranch, - }, + apis::config::{ConfigApi, ConfigKey}, cipher_seed::CipherSeedRestore, WalletSdk as Sdk, WalletSdkConfig, @@ -80,8 +77,6 @@ pub async fn run_tari_ootle_walletd( let needs_seed_recovery = wallet_sdk.initialize_cipher_seed(seed_words.map(CipherSeedRestore::FromSeedWords).unwrap_or_default())?; - wallet_sdk.key_manager_api().get_or_create_initial(KeyBranch::Account)?; - tokio::spawn({ let wallet_sdk = wallet_sdk.clone(); async move { diff --git a/applications/tari_walletd/src/main.rs b/applications/tari_walletd/src/main.rs index e7c984062f..c7e133e9e8 100644 --- a/applications/tari_walletd/src/main.rs +++ b/applications/tari_walletd/src/main.rs @@ -144,7 +144,9 @@ async fn main() -> Result<(), anyhow::Error> { .map(CipherSeedRestore::FromSeedWords) .unwrap_or(CipherSeedRestore::CreateNewIfRequired), )?; - let seed_words = sdk.load_seed_words()?; + let seed_words = sdk + .load_seed_words()? + .expect("Bug: seed words were initialized however load_seed_words returned None"); println!("{}", seed_words.join(" ").reveal()) }, } diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx index bf31a3266c..7179b4f9b1 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx @@ -206,9 +206,8 @@ export function TransferNftDialog(props: TransferNftDialogProps) { const result = await calculateFeeEstimate?.(); if (result && "Accept" in result.result.result) { - const fee = result.fee + 100; // Add buffer as per original comment - setTransferFormState({ maxFee: fee.toString() }); - return fee; + setTransferFormState({ maxFee: result.fee.toString() }); + return result.fee; } else { console.error("Fee estimation rejected:", result); throw new Error("Could not estimate transfer fee"); @@ -232,9 +231,8 @@ export function TransferNftDialog(props: TransferNftDialogProps) { const result = await calculateFeeEstimate?.(); if (result && "Accept" in result.result.result) { - const fee = result.fee + 100; // Add buffer - setTransferFormState({ maxFee: fee.toString() }); - return fee; + setTransferFormState({ maxFee: result.fee.toString() }); + return result.fee; } else { console.error("Fee estimation rejected:", result); throw new Error("Could not estimate transfer fee"); diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index c3a7c6c527..68d2878198 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -202,7 +202,11 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { throw new Error("Fee estimation failed"); } - const fee = resp.final_fee + 100; + let fee = resp.final_fee; + if (props.resource_type === "Confidential") { + // TODO: Add extra amount for confidential transactions, since the bullet proof size is variable + fee += 100; + } setTransferFormState((prevState) => ({ ...prevState, fee: fee.toString() })); } catch (error) { console.error("Fee estimation error:", error); diff --git a/applications/tari_walletd/web_ui/src/routes/Manifest/Manifest.tsx b/applications/tari_walletd/web_ui/src/routes/Manifest/Manifest.tsx index a49b1f3b8c..60167d8aa5 100644 --- a/applications/tari_walletd/web_ui/src/routes/Manifest/Manifest.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Manifest/Manifest.tsx @@ -80,7 +80,7 @@ function ManifestEditor() { throw new Error("No result returned for dry run"); } if ("Accept" in finalize!.result) { - setFee(BigInt(finalize!.fee_receipt.total_fees_paid) + 100n); + setFee(BigInt(finalize!.fee_receipt.total_fees_paid)); setFinalizeError(null); console.log("Dry run successful:", finalize); } else if ("Reject" in finalize!.result) { diff --git a/crates/engine_types/src/crypto/messages.rs b/crates/engine_types/src/crypto/messages.rs index 14312e7aa8..8932422251 100644 --- a/crates/engine_types/src/crypto/messages.rs +++ b/crates/engine_types/src/crypto/messages.rs @@ -3,7 +3,7 @@ use tari_crypto::ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey}; use tari_template_lib::{ - models::ViewableBalanceProofChallengeFields, + models::{StealthInputsStatement, StealthOutputsStatement, ViewableBalanceProofChallengeFields}, prelude::{PedersenCommitmentBytes, RistrettoPublicKeyBytes}, types::Amount, }; @@ -36,29 +36,25 @@ pub fn viewable_balance_proof64( .result() } -pub fn stealth_transfer64( +pub fn stealth_balance_proof64( public_excess: &RistrettoPublicKey, public_nonce: &RistrettoPublicKey, - input_revealed_amount: &Amount, - output_revealed_amount: &Amount, + stealth_inputs_statement: &StealthInputsStatement, + stealth_outputs_statement: &StealthOutputsStatement, ) -> [u8; 64] { - engine_hasher64(EngineHashDomainLabel::StealthTransfer) + engine_hasher64(EngineHashDomainLabel::StealthBalanceProof) .chain(public_excess) .chain(public_nonce) - .chain(input_revealed_amount) - .chain(output_revealed_amount) + .chain(stealth_inputs_statement) + .chain(stealth_outputs_statement) .result() } pub fn stealth_ownership64( - public_key: &RistrettoPublicKeyBytes, - public_nonce: &RistrettoPublicKeyBytes, commitment: &PedersenCommitmentBytes, public_output_nonce: &RistrettoPublicKeyBytes, ) -> [u8; 64] { engine_hasher64(EngineHashDomainLabel::StealthOwnership) - .chain(public_key) - .chain(public_nonce) .chain(commitment) .chain(public_output_nonce) .result() diff --git a/crates/engine_types/src/crypto/utxo_spend.rs b/crates/engine_types/src/crypto/utxo_spend.rs index 948e0f0df6..839b892504 100644 --- a/crates/engine_types/src/crypto/utxo_spend.rs +++ b/crates/engine_types/src/crypto/utxo_spend.rs @@ -22,19 +22,14 @@ pub fn verify_utxo_spend_permission(utxo: &UtxoOutput, input: &StealthInput) -> details: "Malformed ownership proof".to_string(), })?; - let message = messages::stealth_ownership64( - &utxo.owner_public_key, - input.owner_proof.public_nonce(), - &input.commitment, - &utxo.output.public_nonce, - ); + let message = messages::stealth_ownership64(&input.commitment, &utxo.output.public_nonce); let signer_pk = RistrettoPublicKey::convert_from_byte_type(&utxo.owner_public_key).map_err(|_| { ResourceError::InvalidSpend { details: "Non-canonical compressed owner public key".to_string(), } })?; - if !balance_proof.verify_raw_uniform(&signer_pk, &message) { + if !balance_proof.verify(&signer_pk, message) { return Err(ResourceError::InvalidSpend { details: format!("Invalid ownership proof for input with commitment {}", input.commitment), }); diff --git a/crates/engine_types/src/hashing.rs b/crates/engine_types/src/hashing.rs index e82b617edc..d369ab2fbd 100644 --- a/crates/engine_types/src/hashing.rs +++ b/crates/engine_types/src/hashing.rs @@ -126,7 +126,7 @@ pub enum EngineHashDomainLabel { SubstateValue, ViewableBalanceProof, UtxoAddress, - StealthTransfer, + StealthBalanceProof, StealthOwnership, ValueProof, } @@ -153,7 +153,7 @@ impl EngineHashDomainLabel { Self::ViewableBalanceProof => "ViewableBalanceProof", Self::TemplateAddress => "TemplateAddress", Self::UtxoAddress => "UtxoAddress", - Self::StealthTransfer => "StealthTransfer", + Self::StealthBalanceProof => "StealthBalanceProof", Self::StealthOwnership => "StealthOwnership", Self::ValueProof => "ValueProof", } diff --git a/crates/engine_types/src/stealth/transfer.rs b/crates/engine_types/src/stealth/transfer.rs index d2fd2929c4..0c17bf2fe8 100644 --- a/crates/engine_types/src/stealth/transfer.rs +++ b/crates/engine_types/src/stealth/transfer.rs @@ -113,11 +113,11 @@ pub fn validate_transfer( balance_proof.get_public_nonce() ); - let message = messages::stealth_transfer64( + let message = messages::stealth_balance_proof64( &public_excess, balance_proof.get_public_nonce(), - &transfer.inputs_statement.revealed_amount, - &transfer.outputs_statement.revealed_output_amount, + &transfer.inputs_statement, + &transfer.outputs_statement, ); if !balance_proof.verify_raw_uniform(&public_excess, &message) { diff --git a/crates/template_lib/src/models/stealth.rs b/crates/template_lib/src/models/stealth.rs index 26a2e26634..156d7fe667 100644 --- a/crates/template_lib/src/models/stealth.rs +++ b/crates/template_lib/src/models/stealth.rs @@ -21,8 +21,6 @@ pub struct StealthOutputsStatement { pub revealed_output_amount: Amount, /// Bulletproof range proof for the output commitments proving that values are in the range /// [minimum_value_promise, 2^64) - // TODO: consider creating multiple batches of outputs each with an aggregate BP, since BP+ initialization for - // arbitrary number (tested 512) is expensive and slow pub agg_range_proof: RangeProofBytes, } diff --git a/crates/template_test_tooling/src/support/confidential.rs b/crates/template_test_tooling/src/support/confidential.rs index dc1cdaf0c5..f2b9d7d018 100644 --- a/crates/template_test_tooling/src/support/confidential.rs +++ b/crates/template_test_tooling/src/support/confidential.rs @@ -8,7 +8,7 @@ use tari_crypto::{ ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey}, }; use tari_engine_types::crypto::commit_amount_checked; -use tari_ootle_wallet_crypto::{confidential, MaskAndValue, UnblindedOutputStatement}; +use tari_ootle_wallet_crypto::{confidential, MaskAndValue, UnblindedOutputWitness}; use tari_template_lib::{ models::{ConfidentialOutputStatement, ConfidentialWithdrawProof}, types::{Amount, EncryptedData}, @@ -35,7 +35,7 @@ fn generate_confidential_proof_internal( view_key: Option, ) -> (ConfidentialOutputStatement, PrivateKey, Option) { let mask = PrivateKey::random(&mut OsRng); - let output_statement = UnblindedOutputStatement { + let output_statement = UnblindedOutputWitness { amount: output_amount, mask: mask.clone(), sender_public_nonce: Default::default(), @@ -45,7 +45,7 @@ fn generate_confidential_proof_internal( }; let change_mask = PrivateKey::random(&mut OsRng); - let change_statement = change.map(|amount| UnblindedOutputStatement { + let change_statement = change.map(|amount| UnblindedOutputWitness { amount, mask: change_mask.clone(), sender_public_nonce: Default::default(), @@ -149,7 +149,7 @@ fn generate_withdraw_proof_internal( }; let change_mask = change_amount.map(|_| PrivateKey::random(&mut OsRng)); - let output_proof = UnblindedOutputStatement { + let output_proof = UnblindedOutputWitness { amount: output_amount, mask: output_mask.clone(), sender_public_nonce: Default::default(), @@ -157,7 +157,7 @@ fn generate_withdraw_proof_internal( encrypted_data: EncryptedData::try_from(vec![0; EncryptedData::min_size()]).unwrap(), resource_view_key: view_key.clone(), }; - let change_proof = change_amount.map(|amount| UnblindedOutputStatement { + let change_proof = change_amount.map(|amount| UnblindedOutputWitness { amount, mask: change_mask.clone().unwrap(), sender_public_nonce: Default::default(), diff --git a/crates/template_test_tooling/src/support/stealth.rs b/crates/template_test_tooling/src/support/stealth.rs index 186dc00352..d44d4114e8 100644 --- a/crates/template_test_tooling/src/support/stealth.rs +++ b/crates/template_test_tooling/src/support/stealth.rs @@ -13,9 +13,9 @@ use tari_engine_types::{ use tari_ootle_wallet_crypto::{ stealth, MaskAndValue, - UnblindedOutputStatement, - UnblindedStealthInputStatement, - UnblindedStealthOutputStatement, + UnblindedOutputWitness, + UnblindedStealthInputWitness, + UnblindedStealthOutputWitness, }; use tari_template_lib::{ models::{StealthOutputsStatement, StealthTransferStatement}, @@ -86,8 +86,8 @@ fn generate_stealth_statement_internal( let output_statements = output_amounts .iter() .zip(&masks) - .map(|(amount, mask)| UnblindedStealthOutputStatement { - statement: UnblindedOutputStatement { + .map(|(amount, mask)| UnblindedStealthOutputWitness { + witness: UnblindedOutputWitness { amount: *amount, mask: mask.clone(), sender_public_nonce: test_sender_public_nonce(), @@ -174,7 +174,7 @@ fn generate_transfer_data_internal, A: Into>( }; // For testing purposes, we use the mask as the owner key let output_owner_public_key = RistrettoPublicKey::from_secret_key(&output_mask); - let statement = UnblindedOutputStatement { + let statement = UnblindedOutputWitness { amount, mask: output_mask, resource_view_key: view_key.clone(), @@ -184,8 +184,8 @@ fn generate_transfer_data_internal, A: Into>( encrypted_data: EncryptedData::try_from(vec![0; EncryptedData::min_size()]).unwrap(), }; - UnblindedStealthOutputStatement { - statement, + UnblindedStealthOutputWitness { + witness: statement, output_owner_public_key, tag: UtxoTag::new(0), } @@ -196,7 +196,7 @@ fn generate_transfer_data_internal, A: Into>( .iter() .map(|input| { let mask_and_value = input.clone(); - UnblindedStealthInputStatement { + UnblindedStealthInputWitness { mask_and_value, // For testing purposes, we use the mask as the owner key owner_secret: input.mask.clone(), @@ -214,7 +214,7 @@ fn generate_transfer_data_internal, A: Into>( .unwrap(); StealthUnblindedTransferData { - output_masks: outputs.into_iter().map(|m| m.statement.mask).collect(), + output_masks: outputs.into_iter().map(|m| m.witness.mask).collect(), statement: transfer, } } diff --git a/crates/transaction/src/transaction.rs b/crates/transaction/src/transaction.rs index 88334626be..3c34036f27 100644 --- a/crates/transaction/src/transaction.rs +++ b/crates/transaction/src/transaction.rs @@ -176,8 +176,8 @@ impl Transaction { self.inputs().iter().map(|i| i.substate_id()) } - pub fn is_shard_applicable(&self) -> bool { - self.involved_substate_addresses_iter().next().is_some() + pub fn has_inputs(&self) -> bool { + !self.inputs().is_empty() } /// Returns true if the provided committee is involved in at least one input or known output of this transaction. diff --git a/crates/wallet/crypto/src/balance_proof.rs b/crates/wallet/crypto/src/balance_proof.rs index 204596eaa0..5988115867 100644 --- a/crates/wallet/crypto/src/balance_proof.rs +++ b/crates/wallet/crypto/src/balance_proof.rs @@ -7,12 +7,9 @@ use tari_crypto::{ ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; use tari_engine_types::{crypto::messages, hashing::EngineSchnorrSignature, ToByteType}; -use tari_template_lib::prelude::{ - Amount, - BalanceProofSignature, - PedersenCommitmentBytes, - RistrettoPublicKeyBytes, - SchnorrSignatureBytes, +use tari_template_lib::{ + models::{StealthInputsStatement, StealthOutputsStatement}, + prelude::{Amount, BalanceProofSignature, PedersenCommitmentBytes, RistrettoPublicKeyBytes, SchnorrSignatureBytes}, }; pub(crate) fn generate_confidential_balance_proof( @@ -41,18 +38,13 @@ pub(crate) fn generate_confidential_balance_proof( pub(crate) fn generate_stealth_balance_proof_signature( agg_input_mask: &RistrettoSecretKey, agg_output_mask: &RistrettoSecretKey, - revealed_input_amount: &Amount, - revealed_output_amount: &Amount, + inputs_statement: &StealthInputsStatement, + outputs_statement: &StealthOutputsStatement, ) -> BalanceProofSignature { let secret_excess = agg_input_mask - agg_output_mask; let public_excess = RistrettoPublicKey::from_secret_key(&secret_excess); let (nonce, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); - let message = messages::stealth_transfer64( - &public_excess, - &public_nonce, - revealed_input_amount, - revealed_output_amount, - ); + let message = messages::stealth_balance_proof64(&public_excess, &public_nonce, inputs_statement, outputs_statement); let sig = EngineSchnorrSignature::sign_raw_uniform(&secret_excess, nonce, &message).unwrap(); sig.to_byte_type() @@ -60,18 +52,10 @@ pub(crate) fn generate_stealth_balance_proof_signature( pub(crate) fn generate_stealth_owner_proof_signature( secret_key: &RistrettoSecretKey, - stealth_public_key: &RistrettoPublicKeyBytes, - commitment: &PedersenCommitmentBytes, public_output_nonce: &RistrettoPublicKeyBytes, + commitment: &PedersenCommitmentBytes, ) -> SchnorrSignatureBytes { - let (nonce, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); - let message = messages::stealth_ownership64( - stealth_public_key, - &public_nonce.to_byte_type(), - commitment, - public_output_nonce, - ); - - let sig = EngineSchnorrSignature::sign_raw_uniform(secret_key, nonce, &message).unwrap(); + let message = messages::stealth_ownership64(commitment, public_output_nonce); + let sig = EngineSchnorrSignature::sign(secret_key, message, &mut OsRng).unwrap(); sig.to_byte_type() } diff --git a/crates/wallet/crypto/src/bullet_proof.rs b/crates/wallet/crypto/src/bullet_proof.rs index 3a95460501..08a3c01e0d 100644 --- a/crates/wallet/crypto/src/bullet_proof.rs +++ b/crates/wallet/crypto/src/bullet_proof.rs @@ -15,9 +15,9 @@ use tari_crypto::{ use tari_engine_types::crypto::{get_static_range_proof_service, MAX_LAZY_BP_AGG_FACTORS}; use tari_template_lib::types::crypto::RangeProofBytes; -use crate::UnblindedOutputStatement; +use crate::UnblindedOutputWitness; -pub fn generate_extended_bullet_proof<'a, I: IntoIterator>( +pub fn generate_extended_bullet_proof<'a, I: IntoIterator>( statements: I, ) -> Result { let mut extended_witnesses = statements diff --git a/crates/wallet/crypto/src/confidential.rs b/crates/wallet/crypto/src/confidential.rs index d99f9c7a0c..5cdb32833e 100644 --- a/crates/wallet/crypto/src/confidential.rs +++ b/crates/wallet/crypto/src/confidential.rs @@ -14,16 +14,16 @@ use crate::{ error::ConfidentialProofError, viewable_balance_proof::create_viewable_balance_proof, MaskAndValue, - UnblindedOutputStatement, + UnblindedOutputWitness, WalletCryptoError, }; pub fn create_withdraw_proof( inputs: &[MaskAndValue], input_revealed_amount: Amount, - output_statement: Option<&UnblindedOutputStatement>, + output_statement: Option<&UnblindedOutputWitness>, output_revealed_amount: Amount, - change_statement: Option<&UnblindedOutputStatement>, + change_statement: Option<&UnblindedOutputWitness>, change_revealed_amount: Amount, ) -> Result { let output_proof = create_output_statement( @@ -72,9 +72,9 @@ pub fn create_withdraw_proof( } pub fn create_output_statement( - output_statement: Option<&UnblindedOutputStatement>, + output_statement: Option<&UnblindedOutputWitness>, output_revealed_amount: Amount, - change_statement: Option<&UnblindedOutputStatement>, + change_statement: Option<&UnblindedOutputWitness>, change_revealed_amount: Amount, ) -> Result { let proof_change_statement = change_statement @@ -147,7 +147,7 @@ mod tests { fn create_valid_proof(amount: Amount, minimum_value_promise: u64) -> ConfidentialOutputStatement { let mask = RistrettoSecretKey::random(&mut OsRng); create_output_statement( - Some(&UnblindedOutputStatement { + Some(&UnblindedOutputWitness { amount, minimum_value_promise, mask, diff --git a/crates/wallet/crypto/src/stealth.rs b/crates/wallet/crypto/src/stealth.rs index ec740d395c..dcff123c53 100644 --- a/crates/wallet/crypto/src/stealth.rs +++ b/crates/wallet/crypto/src/stealth.rs @@ -1,10 +1,7 @@ // Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_crypto::{ - keys::PublicKey, - ristretto::{RistrettoPublicKey, RistrettoSecretKey}, -}; +use tari_crypto::ristretto::RistrettoSecretKey; use tari_engine_types::ToByteType; use tari_template_lib::{ models::{ @@ -23,8 +20,8 @@ use crate::{ bullet_proof::generate_extended_bullet_proof, error::ConfidentialProofError, viewable_balance_proof::create_viewable_balance_proof, - UnblindedStealthInputStatement, - UnblindedStealthOutputStatement, + UnblindedStealthInputWitness, + UnblindedStealthOutputWitness, WalletCryptoError, }; @@ -35,8 +32,8 @@ pub fn create_transfer_statement<'a, Inputs, Outputs>( revealed_output_amount: Amount, ) -> Result where - Inputs: IntoIterator, - Outputs: IntoIterator + Clone, + Inputs: IntoIterator, + Outputs: IntoIterator + Clone, { if revealed_input_amount.is_negative() { return Err(WalletCryptoError::InvalidArgument { @@ -67,13 +64,11 @@ where name: "input value", details: format!("Input value {} must be non-negative", input.mask_and_value.value), })?; - let stealth_public_key = RistrettoPublicKey::from_secret_key(&input.owner_secret).to_byte_type(); let signature = generate_stealth_owner_proof_signature( &input.owner_secret, - &stealth_public_key, - &commitment.to_byte_type(), &input.public_nonce.to_byte_type(), + &commitment.to_byte_type(), ); inputs.push(StealthInput { commitment: commitment.to_byte_type(), @@ -86,18 +81,22 @@ where let agg_output_mask = output_statements .clone() .into_iter() - .map(|stmt| &stmt.statement.mask) + .map(|stmt| &stmt.witness.mask) .fold(RistrettoSecretKey::default(), |agg, mask| agg + mask); + let inputs_statement = StealthInputsStatement { + inputs: inputs_to_spend.clone(), + revealed_amount: revealed_input_amount, + }; + let outputs_statement = create_outputs_statement(output_statements, revealed_output_amount)?; + let balance_proof = generate_stealth_balance_proof_signature( &agg_input_mask, &agg_output_mask, - &revealed_input_amount, - &revealed_output_amount, + &inputs_statement, + &outputs_statement, ); - let outputs_statement = create_outputs_statement(output_statements, revealed_output_amount)?; - Ok(StealthTransferStatement { inputs_statement: StealthInputsStatement { inputs: inputs_to_spend, @@ -108,7 +107,7 @@ where }) } -pub fn create_outputs_statement<'a, Outputs: IntoIterator + Clone>( +pub fn create_outputs_statement<'a, Outputs: IntoIterator + Clone>( output_statements: Outputs, revealed_output_amount: Amount, ) -> Result { @@ -116,9 +115,9 @@ pub fn create_outputs_statement<'a, Outputs: IntoIterator, _>>()?; - let output_range_proof = generate_extended_bullet_proof(output_statements.into_iter().map(|o| &o.statement))?; + let output_range_proof = generate_extended_bullet_proof(output_statements.into_iter().map(|o| &o.witness))?; Ok(StealthOutputsStatement { outputs, @@ -164,13 +163,13 @@ mod tests { use tari_template_lib::types::{crypto::UtxoTag, Amount, EncryptedData}; use super::*; - use crate::UnblindedOutputStatement; + use crate::UnblindedOutputWitness; fn create_valid_proof(amount: Amount, minimum_value_promise: u64) -> StealthOutputsStatement { let mask = RistrettoSecretKey::random(&mut OsRng); create_outputs_statement( - &[UnblindedStealthOutputStatement { - statement: UnblindedOutputStatement { + &[UnblindedStealthOutputWitness { + witness: UnblindedOutputWitness { amount, minimum_value_promise, mask, diff --git a/crates/wallet/crypto/src/unblinded_statement.rs b/crates/wallet/crypto/src/unblinded_statement.rs index 673798e3fc..fe6b5f311f 100644 --- a/crates/wallet/crypto/src/unblinded_statement.rs +++ b/crates/wallet/crypto/src/unblinded_statement.rs @@ -8,7 +8,7 @@ use tari_template_lib::types::{crypto::UtxoTag, Amount, EncryptedData}; use crate::memo::Memo; #[derive(Debug, Clone)] -pub struct UnblindedOutputStatement { +pub struct UnblindedOutputWitness { pub amount: Amount, pub mask: RistrettoSecretKey, pub sender_public_nonce: RistrettoPublicKey, @@ -17,15 +17,15 @@ pub struct UnblindedOutputStatement { pub resource_view_key: Option, } -impl UnblindedOutputStatement { +impl UnblindedOutputWitness { pub fn to_commitment(&self) -> Option { commit_amount_checked(&self.mask, self.amount) } } #[derive(Debug, Clone)] -pub struct UnblindedStealthOutputStatement { - pub statement: UnblindedOutputStatement, +pub struct UnblindedStealthOutputWitness { + pub witness: UnblindedOutputWitness, pub output_owner_public_key: RistrettoPublicKey, pub tag: UtxoTag, } @@ -75,7 +75,7 @@ impl DecryptedData { } #[derive(Debug, Clone)] -pub struct UnblindedStealthInputStatement { +pub struct UnblindedStealthInputWitness { pub mask_and_value: MaskAndValue, pub owner_secret: RistrettoSecretKey, pub public_nonce: RistrettoPublicKey, diff --git a/crates/wallet/crypto/tests/output_statement.rs b/crates/wallet/crypto/tests/output_statement.rs index 30a3c3778a..0947c4bbd4 100644 --- a/crates/wallet/crypto/tests/output_statement.rs +++ b/crates/wallet/crypto/tests/output_statement.rs @@ -12,9 +12,9 @@ use tari_ootle_wallet_crypto::{ confidential, stealth::create_transfer_statement, MaskAndValue, - UnblindedOutputStatement, - UnblindedStealthInputStatement, - UnblindedStealthOutputStatement, + UnblindedOutputWitness, + UnblindedStealthInputWitness, + UnblindedStealthOutputWitness, }; use tari_template_lib::types::Amount; @@ -89,12 +89,12 @@ mod stealth_tests { stealth::validate_transfer(&statement, None).unwrap_err(); // Invalid, output is less than input } - fn make_input_statements(amounts: &[(u8, u64)]) -> Vec { + fn make_input_statements(amounts: &[(u8, u64)]) -> Vec { amounts .iter() .map(|&(seed, amount)| { let (mask, public_key) = create_key_pair_from_seed(seed); - UnblindedStealthInputStatement { + UnblindedStealthInputWitness { mask_and_value: MaskAndValue::new(Amount::from(amount), mask.clone()), owner_secret: mask, public_nonce: public_key, @@ -103,7 +103,7 @@ mod stealth_tests { .collect() } - fn make_output_statements + Copy>(amounts: &[A]) -> Vec { + fn make_output_statements + Copy>(amounts: &[A]) -> Vec { amounts .iter() .map(|&amount| { @@ -116,7 +116,7 @@ mod stealth_tests { }; // For testing purposes, we use the mask as the owner key let output_owner_public_key = RistrettoPublicKey::from_secret_key(&output_mask); - let statement = UnblindedOutputStatement { + let statement = UnblindedOutputWitness { amount, mask: output_mask, resource_view_key: None, @@ -129,8 +129,8 @@ mod stealth_tests { encrypted_data: EncryptedData::try_from(vec![0; EncryptedData::min_size()]).unwrap(), }; - UnblindedStealthOutputStatement { - statement, + UnblindedStealthOutputWitness { + witness: statement, output_owner_public_key, tag: UtxoTag::new(0), } diff --git a/crates/wallet/crypto/tests/viewable_balance_proof.rs b/crates/wallet/crypto/tests/viewable_balance_proof.rs index 26681867a0..bba7bb2a26 100644 --- a/crates/wallet/crypto/tests/viewable_balance_proof.rs +++ b/crates/wallet/crypto/tests/viewable_balance_proof.rs @@ -9,16 +9,16 @@ use tari_crypto::{ ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSecretKey}, }; use tari_engine_types::crypto::validate_elgamal_verifiable_balance_proof; -use tari_ootle_wallet_crypto::{confidential, AlwaysMissLookupTable, UnblindedOutputStatement}; +use tari_ootle_wallet_crypto::{confidential, AlwaysMissLookupTable, UnblindedOutputWitness}; use tari_template_lib::{ template_dependencies::{decode_exact, encode_with_len}, types::{Amount, EncryptedData}, }; use tari_utilities::ByteArray; -fn create_output_statement(value: Amount, view_key: &RistrettoPublicKey) -> UnblindedOutputStatement { +fn create_output_statement(value: Amount, view_key: &RistrettoPublicKey) -> UnblindedOutputWitness { let mask = RistrettoSecretKey::random(&mut OsRng); - UnblindedOutputStatement { + UnblindedOutputWitness { amount: value, mask, sender_public_nonce: Default::default(), diff --git a/crates/wallet/sdk/src/apis/confidential_crypto.rs b/crates/wallet/sdk/src/apis/confidential_crypto.rs index 75d319e0d6..f959094b2d 100644 --- a/crates/wallet/sdk/src/apis/confidential_crypto.rs +++ b/crates/wallet/sdk/src/apis/confidential_crypto.rs @@ -11,7 +11,7 @@ use tari_ootle_wallet_crypto::{ ConfidentialProofError, DecryptedData, MaskAndValue, - UnblindedOutputStatement, + UnblindedOutputWitness, WalletCryptoError, }; use tari_template_lib::{ @@ -39,9 +39,9 @@ impl ConfidentialCryptoApi { &self, inputs: &[MaskAndValue], input_revealed_amount: A, - output_statement: Option<&UnblindedOutputStatement>, + output_statement: Option<&UnblindedOutputWitness>, output_revealed_amount: A, - change_statement: Option<&UnblindedOutputStatement>, + change_statement: Option<&UnblindedOutputWitness>, change_revealed_amount: A, ) -> Result { let proof = confidential::create_withdraw_proof( @@ -90,7 +90,7 @@ impl ConfidentialCryptoApi { pub fn generate_output_proof>( &self, - statement: &UnblindedOutputStatement, + statement: &UnblindedOutputWitness, revealed_amount: A, ) -> Result { let proof = confidential::create_output_statement( diff --git a/crates/wallet/sdk/src/apis/confidential_outputs.rs b/crates/wallet/sdk/src/apis/confidential_outputs.rs index 298d616d02..df2d35be3d 100644 --- a/crates/wallet/sdk/src/apis/confidential_outputs.rs +++ b/crates/wallet/sdk/src/apis/confidential_outputs.rs @@ -141,6 +141,14 @@ where TStore: WalletStore Ok(()) } + pub fn release_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { + let mut tx = self.store.create_write_tx()?; + tx.vaults_release_lock_revealed_funds(lock_id)?; + tx.commit()?; + + Ok(()) + } + pub fn finalize_outputs_for_lock(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { let mut tx = self.store.create_write_tx()?; tx.confidential_outputs_finalize_by_lock_id(lock_id)?; @@ -149,6 +157,14 @@ where TStore: WalletStore Ok(()) } + pub fn finalize_locked_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { + let mut tx = self.store.create_write_tx()?; + tx.vaults_finalized_locked_revealed_funds(lock_id)?; + tx.commit()?; + + Ok(()) + } + pub fn resolve_output_masks( &self, outputs: Vec, @@ -192,22 +208,6 @@ where TStore: WalletStore Ok(outputs_with_masks) } - pub fn finalize_locked_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.vaults_finalized_locked_revealed_funds(lock_id)?; - tx.commit()?; - - Ok(()) - } - - pub fn release_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.vaults_release_lock_revealed_funds(lock_id)?; - tx.commit()?; - - Ok(()) - } - pub fn get_unspent_balance(&self, vault_id: &VaultId) -> Result { let mut tx = self.store.create_read_tx()?; let balance = tx.confidential_outputs_get_unspent_balance(vault_id)?; diff --git a/crates/wallet/sdk/src/apis/confidential_transfer.rs b/crates/wallet/sdk/src/apis/confidential_transfer.rs index dd7b40ac5d..c9fd3f3d8a 100644 --- a/crates/wallet/sdk/src/apis/confidential_transfer.rs +++ b/crates/wallet/sdk/src/apis/confidential_transfer.rs @@ -10,7 +10,7 @@ use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; use tari_engine_types::{FromByteType, ToByteType}; use tari_ootle_address::OotleAddress; use tari_ootle_common_types::{optional::IsNotFoundError, SubstateRequirement}; -use tari_ootle_wallet_crypto::{memo::Memo, MaskAndValue, UnblindedOutputStatement}; +use tari_ootle_wallet_crypto::{memo::Memo, MaskAndValue, UnblindedOutputWitness}; use tari_template_lib::{ models::{ComponentAddress, ResourceAddress, VaultId}, types::Amount, @@ -431,7 +431,7 @@ where confidential_amount: Amount, resource_view_key: Option, memo: Option<&Memo>, - ) -> Result { + ) -> Result { if !confidential_amount.is_positive() { return Err(ConfidentialTransferApiError::InvalidParameter { param: "confidential_amount", @@ -457,7 +457,7 @@ where memo, )?; - Ok(UnblindedOutputStatement { + Ok(UnblindedOutputWitness { amount: confidential_amount, mask: mask.key, sender_public_nonce: public_nonce, diff --git a/crates/wallet/sdk/src/apis/key_manager.rs b/crates/wallet/sdk/src/apis/key_manager.rs index 8b9f41b322..e97756eccd 100644 --- a/crates/wallet/sdk/src/apis/key_manager.rs +++ b/crates/wallet/sdk/src/apis/key_manager.rs @@ -102,17 +102,6 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { } } - pub fn get_or_create_initial(&self, branch: KeyBranch) -> Result<(), KeyManagerApiError> { - let mut tx = self.store.create_write_tx()?; - if tx.key_manager_get_active_index(branch.as_str()).optional()?.is_none() { - tx.key_manager_insert_or_ignore(branch.as_str(), 0)?; - tx.commit()?; - } else { - tx.rollback()?; - } - Ok(()) - } - pub fn get_all_derived_keys(&self, branch: KeyBranch) -> Result, KeyManagerApiError> { let all_keys = self.store.with_read_tx(|tx| tx.key_manager_get_all(branch.as_str()))?; let mut keys = Vec::with_capacity(all_keys.len()); @@ -286,26 +275,25 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { /// If the branch does not exist, it will be created with index 0 and the first key will be returned. /// TODO: if there is another active DB transaction this function will block until it can acquire it. pub fn next_key(&self, branch: KeyBranch) -> Result { + let next_key_id = self.next_derived_key_index(branch)?; + let key = self.derive_key(branch, next_key_id)?; + Ok(key) + } + + pub fn next_derived_key_index(&self, branch: KeyBranch) -> Result { let mut tx = self.store.create_write_tx()?; let next_index = tx .key_manager_get_last_index(branch.as_str()) .optional()? .map(|i| i + 1) .unwrap_or(0); - let key_manager = self.get_key_manager(branch.as_str())?; - let key = key_manager - .derive_key(next_index) - // TODO: Key manager shouldn't return other errors - .map_err(key_manager::error::KeyManagerServiceError::from)?; - // Index of account keys and view keys should always match to allow UTXO recovery when the specific account is - // unknown if matches!(branch, KeyBranch::Account) { // Ensure the view key branch is created if it doesn't exist tx.key_manager_insert_or_ignore(KeyBranch::ViewOnlyKey.as_str(), next_index)?; } - tx.key_manager_insert_or_ignore(&key_manager.branch_seed, next_index)?; + tx.key_manager_insert_or_ignore(branch.as_str(), next_index)?; tx.commit()?; - Ok(key.into()) + Ok(next_index) } pub fn create_throwaway_nonce(&self) -> RistrettoSecretKey { diff --git a/crates/wallet/sdk/src/apis/stealth_crypto.rs b/crates/wallet/sdk/src/apis/stealth_crypto.rs index bc6815fbb1..5dd0edbe48 100644 --- a/crates/wallet/sdk/src/apis/stealth_crypto.rs +++ b/crates/wallet/sdk/src/apis/stealth_crypto.rs @@ -16,9 +16,9 @@ use tari_ootle_wallet_crypto::{ stealth, ConfidentialProofError, DecryptedData, - UnblindedOutputStatement, - UnblindedStealthInputStatement, - UnblindedStealthOutputStatement, + UnblindedOutputWitness, + UnblindedStealthInputWitness, + UnblindedStealthOutputWitness, WalletCryptoError, }; use tari_template_lib::{ @@ -54,8 +54,8 @@ impl StealthCryptoApi { ) -> Result where A: Into, - Inputs: IntoIterator, - Outputs: IntoIterator + Clone, + Inputs: IntoIterator, + Outputs: IntoIterator + Clone, { let stmt = stealth::create_transfer_statement( inputs, @@ -115,7 +115,7 @@ impl StealthCryptoApi { pub fn generate_output_proof>( &self, - statement: &UnblindedOutputStatement, + statement: &UnblindedOutputWitness, revealed_amount: A, ) -> Result { let proof = confidential::create_output_statement( diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs index 0abff955d1..f5149859c0 100644 --- a/crates/wallet/sdk/src/apis/stealth_outputs.rs +++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs @@ -1,6 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use digest::crypto_common::rand_core::OsRng; use log::*; use tari_crypto::{ keys::PublicKey, @@ -13,14 +14,20 @@ use tari_engine_types::{ Utxo, UtxoOutput, }; +use tari_ootle_address::RistrettoOotleAddress; use tari_ootle_common_types::{ optional::{IsNotFoundError, Optional}, Network, }; -use tari_ootle_wallet_crypto::UnblindedStealthInputStatement; +use tari_ootle_wallet_crypto::{ + memo::Memo, + UnblindedOutputWitness, + UnblindedStealthInputWitness, + UnblindedStealthOutputWitness, +}; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::{ - models::{ComponentAddress, ResourceAddress, UtxoAddress, VaultId}, + models::{ComponentAddress, ResourceAddress, StealthTransferStatement, UtxoAddress, VaultId}, prelude::PedersenCommitmentBytes, types::Amount, }; @@ -29,12 +36,21 @@ use tari_transaction::TransactionId; use crate::{ apis::{ accounts::AccountsApiError, + confidential_outputs::ConfidentialOutputsApiError, config::{ConfigApi, ConfigApiError}, key_manager::{KeyBranch, KeyManagerApi, KeyManagerApiError}, stealth_crypto::{StealthCryptoApi, StealthCryptoApiError}, - stealth_transfer::InputToSpend, + stealth_transfer::{OutputToCreate, UnblindedInputToSpend}, + }, + models::{ + AccountAndViewKeys, + InputSpendData, + KeyId, + OutputStatus, + StealthBalance, + StealthOutputModel, + WalletLockId, }, - models::{Account, AccountAndViewKeys, OutputStatus, StealthBalance, StealthOutputModel, WalletLockId}, storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; @@ -176,56 +192,70 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(lock_id) } - pub fn release_locked_outputs(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { + pub fn release_lock(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { self.store.with_write_tx(|tx| { tx.stealth_outputs_release_by_lock_id(lock_id)?; - tx.locks_delete(lock_id)?; - Ok(()) - }) + tx.vaults_release_lock_revealed_funds(lock_id).optional()?; + tx.locks_delete(lock_id) + })?; + Ok(()) + } + + pub fn finalize_lock(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { + let mut tx = self.store.create_write_tx()?; + tx.stealth_outputs_finalize_by_lock_id(lock_id)?; + tx.locks_delete(lock_id)?; + tx.commit()?; + Ok(()) } pub fn finalize_outputs(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { self.store.with_write_tx(|tx| { tx.stealth_outputs_finalize_by_lock_id(lock_id)?; + tx.vaults_finalized_locked_revealed_funds(lock_id).optional()?; tx.locks_delete(lock_id)?; Ok(()) }) } + pub fn lock_revealed_funds>( + &self, + lock_id: WalletLockId, + vault_id: &VaultId, + amount_to_lock: A, + ) -> Result<(), StealthOutputsApiError> { + self.store + .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount_to_lock.into()))?; + + Ok(()) + } + pub fn resolve_output_masks_for_spending( &self, - owner_account: &Account, - outputs: Vec, - ) -> Result, StealthOutputsApiError> { + owner_key_id: KeyId, + view_only_key_id: KeyId, + inputs: &[InputSpendData], + ) -> Result, StealthOutputsApiError> { let network = self.config_api.get_network()?; - // Derive owner secret - the sender does not know the owner secret - let owner_key_id = owner_account - .owner_key_id() - .ok_or_else(|| StealthOutputsApiError::InvalidParameter { - param: "owner_key_id", - reason: format!( - "Account {} does not have an owner key. Cannot spend from this account", - owner_account - ), - })?; + let owner_key_part = self.key_manager_api.get_account_owner_key(owner_key_id)?; // Derive the view-only secret, of which the public key is used by senders to encrypt the value and mask. - let view_only = self - .key_manager_api - .get_view_only_key(owner_account.view_only_key_id())?; - let mut inputs_with_masks = Vec::with_capacity(outputs.len()); - for output in outputs { + let view_only = self.key_manager_api.get_view_only_key(view_only_key_id)?; + let mut inputs_with_masks = Vec::with_capacity(inputs.len()); + for input in inputs { // Derive the decryption key from the DHKE(sender's public nonce, encryption secret key); - let nonce = output.sender_public_nonce.try_from_byte_type().map_err(|e| { - StealthOutputsApiError::InvalidParameter { - param: "sender_public_nonce", - reason: format!("Sender public nonce bytes are not a canonical public key: {e}"), - } - })?; + let nonce = + input + .public_nonce + .try_from_byte_type() + .map_err(|e| StealthOutputsApiError::InvalidParameter { + param: "sender_public_nonce", + reason: format!("Sender public nonce bytes are not a canonical public key: {e}"), + })?; let decrypted = self.crypto_api.decrypt_value_and_mask( - &output.encrypted_data, - &output.commitment, + &input.encrypted_data, + &input.commitment, &view_only.secret, &nonce, // We dont need to decrypt the memo to spend the output @@ -236,46 +266,17 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { .crypto_api .derive_stealth_owner_secret(network, &owner_key_part.secret, &nonce); - inputs_with_masks.push(InputToSpend { - statement: UnblindedStealthInputStatement { + inputs_with_masks.push(UnblindedInputToSpend { + witness: UnblindedStealthInputWitness { mask_and_value: decrypted.mask_and_value, owner_secret: stealth_secret, public_nonce: nonce, }, - is_on_chain: output.is_on_chain, }); } Ok(inputs_with_masks) } - pub fn lock_revealed_funds>( - &self, - lock_id: WalletLockId, - vault_id: &VaultId, - amount_to_lock: A, - ) -> Result<(), StealthOutputsApiError> { - self.store - .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount_to_lock.into()))?; - - Ok(()) - } - - pub fn finalize_locked_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.vaults_finalized_locked_revealed_funds(lock_id)?; - tx.commit()?; - - Ok(()) - } - - pub fn release_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.vaults_release_lock_revealed_funds(lock_id)?; - tx.commit()?; - - Ok(()) - } - pub fn get_unspent_outputs_by_account( &self, account_address: &ComponentAddress, @@ -588,6 +589,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { encrypted_data: output.output.encrypted_data.clone(), tag_byte: output.tag, memo, + minimum_value_promise: output.output.minimum_value_promise, status, is_burnt: false, is_frozen, @@ -598,6 +600,125 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(None) } + + pub fn create_output_witness( + &self, + destination: &RistrettoOotleAddress, + amount: Amount, + resource_address: &ResourceAddress, + resource_view_key: Option, + memo: Option<&Memo>, + ) -> Result { + if !amount.is_positive() { + return Err(StealthOutputsApiError::InvalidParameter { + param: "amount", + reason: format!("Amount must be positive, got {}", amount), + }); + } + + let mask = self.key_manager_api.next_key(KeyBranch::StealthMask)?; + + let (nonce_secret, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); + let encrypted_data = self.crypto_api.encrypt_value_and_mask( + amount + .to_u64_checked() + .ok_or_else(|| StealthOutputsApiError::InvalidParameter { + param: "amount", + reason: "Stealth amount exceeds u64::MAX. This is currently a limitation due to the format of \ + EncryptedData" + .to_string(), + })?, + &mask.key, + destination.view_only_key(), + &nonce_secret, + memo, + )?; + + // Create stealth address - used during spend time + let output_owner_public_key = self.crypto_api.derive_stealth_owner_public_key( + destination.network(), + destination.account_key(), + &nonce_secret, + ); + + let witness = UnblindedOutputWitness { + amount, + mask: mask.key, + sender_public_nonce: public_nonce, + encrypted_data, + minimum_value_promise: 0, + resource_view_key, + }; + + let derived_tag = self.crypto_api.derive_stealth_output_tag( + destination.network(), + &nonce_secret, + destination.view_only_key(), + resource_address, + ); + + Ok(UnblindedStealthOutputWitness { + witness, + output_owner_public_key, + tag: derived_tag, + }) + } + + pub fn generate_transfer_statement( + &self, + params: TransferStatementParams<'_, I>, + ) -> Result + where + I: IntoIterator>, + { + let unblinded_inputs = + self.resolve_output_masks_for_spending(params.spend_key_id, params.view_only_key_id, params.inputs)?; + let outputs = params + .outputs + .into_iter() + .map(|output| { + self.create_output_witness( + output.owner_address, + output.amount, + params.resource_address, + params.resource_view_key.clone(), + output.memo, + ) + }) + .collect::, _>>()?; + let total_input_amount = + unblinded_inputs.iter().map(|i| i.value()).sum::() + params.input_revealed_amount; + let total_output_amount = + outputs.iter().map(|o| o.witness.amount).sum::() + params.output_revealed_amount; + if total_input_amount != total_output_amount { + return Err(StealthOutputsApiError::InvalidParameter { + param: "inputs/outputs", + reason: format!( + "Input and output amounts do not balance. Input: {}, Output: {}", + total_input_amount, total_output_amount + ), + }); + } + + let statement = self.crypto_api.generate_transfer_statement( + unblinded_inputs.iter().map(|i| &i.witness), + params.input_revealed_amount, + &outputs, + params.output_revealed_amount, + )?; + Ok(statement) + } +} + +pub struct TransferStatementParams<'a, I> { + pub spend_key_id: KeyId, + pub view_only_key_id: KeyId, + pub resource_address: &'a ResourceAddress, + pub resource_view_key: Option, + pub inputs: &'a [InputSpendData], + pub input_revealed_amount: Amount, + pub outputs: I, + pub output_revealed_amount: Amount, } #[derive(Debug, thiserror::Error)] diff --git a/crates/wallet/sdk/src/apis/stealth_transfer.rs b/crates/wallet/sdk/src/apis/stealth_transfer.rs index aa0f0442ce..2736c75763 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer.rs @@ -3,10 +3,9 @@ use std::cmp; -use digest::crypto_common::rand_core::OsRng; use log::*; -use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; -use tari_engine_types::{substate::SubstateId, ConvertFromByteType, FromByteType, ToByteType}; +use tari_crypto::ristretto::RistrettoPublicKey; +use tari_engine_types::{substate::SubstateId, ConvertFromByteType, FromByteType}; use tari_ootle_address::{OotleAddress, RistrettoOotleAddress}; use tari_ootle_common_types::{ displayable::Displayable, @@ -14,13 +13,7 @@ use tari_ootle_common_types::{ Network, SubstateRequirement, }; -use tari_ootle_wallet_crypto::{ - memo::Memo, - MaskAndValue, - UnblindedOutputStatement, - UnblindedStealthInputStatement, - UnblindedStealthOutputStatement, -}; +use tari_ootle_wallet_crypto::{memo::Memo, UnblindedStealthInputWitness}; use tari_template_lib::{ constants::XTR, models::{ @@ -28,24 +21,24 @@ use tari_template_lib::{ ComponentAddress, ResourceAddress, StealthTransferStatement, + StealthUnspentOutput, UtxoAddress, VaultId, }, types::Amount, }; -use tari_transaction::{args, Transaction}; +use tari_transaction::{args, Transaction, UnsignedTransaction}; use crate::{ apis::{ accounts::{derive_account_address_from_public_key, AccountsApi, AccountsApiError}, confidential_transfer::ConfidentialTransferInputSelection, config::{ConfigApi, ConfigApiError}, - key_manager::{KeyBranch, KeyManagerApi, KeyManagerApiError}, - stealth_crypto::{StealthCryptoApi, StealthCryptoApiError}, - stealth_outputs::{StealthOutputsApi, StealthOutputsApiError}, + stealth_crypto::StealthCryptoApiError, + stealth_outputs::{StealthOutputsApi, StealthOutputsApiError, TransferStatementParams}, substate::{SubstateApiError, SubstatesApi, ValidatorScanResult}, }, - models::{Account, AccountWithAddress, OutputStatus, StealthOutputModel, WalletLockId}, + models::{Account, AccountWithAddress, InputSpendData, OutputStatus, StealthOutputModel, WalletLockId}, network::WalletNetworkInterface, storage::{WalletStorageError, WalletStore}, }; @@ -53,11 +46,9 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::stealth_transfers"; pub struct StealthTransferApi<'a, TStore, TNetworkInterface> { - key_manager_api: KeyManagerApi<'a, TStore>, accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, outputs_api: StealthOutputsApi<'a, TStore>, substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, - crypto_api: StealthCryptoApi, config_api: ConfigApi<'a, TStore>, } @@ -68,30 +59,26 @@ where TNetworkInterface::Error: IsNotFoundError, { pub fn new( - key_manager_api: KeyManagerApi<'a, TStore>, accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, outputs_api: StealthOutputsApi<'a, TStore>, substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, - crypto_api: StealthCryptoApi, config_api: ConfigApi<'a, TStore>, ) -> Self { Self { - key_manager_api, accounts_api, outputs_api, substate_api, - crypto_api, config_api, } } - fn resolve_fee_inputs( + fn lock_fee_inputs( &self, lock_id: WalletLockId, owner_account: &AccountWithAddress, params: &StealthTransferParams, ) -> Result { - self.resolved_inputs_for_transfer( + self.lock_inputs_for_transfer( lock_id, owner_account.account(), XTR, @@ -101,7 +88,7 @@ where } #[allow(clippy::too_many_lines)] - fn resolved_inputs_for_transfer( + fn lock_inputs_for_transfer( &self, lock_id: WalletLockId, owner_account: &Account, @@ -128,15 +115,12 @@ where match input_selection { ConfidentialTransferInputSelection::ConfidentialOnly => { - let (input_models, total_locked) = self.outputs_api.lock_outputs_for_at_least_amount( + let (inputs, total_locked) = self.outputs_api.lock_outputs_for_at_least_amount( owner_account.component_address(), &resource_address, lock_id, spend_amount, )?; - let inputs = self - .outputs_api - .resolve_output_masks_for_spending(owner_account, input_models)?; info!( target: LOG_TARGET, @@ -146,7 +130,7 @@ where ); Ok(InputsToSpend { - inputs, + inputs: inputs.into_iter().map(|i| i.into_spend_data()).collect(), revealed: Amount::zero(), }) }, @@ -197,7 +181,7 @@ where self.outputs_api.lock_funds_in_vault(lock_id, &src_vault.id, revealed_to_spend) .inspect_err(|_| { // TODO: atomic rollback will help with this - if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { + if let Err(err) = self.outputs_api.release_lock(lock_id) { error!(target: LOG_TARGET, "Failed to release lock outputs for resource {}: {}", resource_address, err); } })?; @@ -230,21 +214,12 @@ where ) .inspect_err(|_| { // TODO: atomic rollback will help with this - if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { - error!(target: LOG_TARGET, "Failed to release lock outputs for resource {}: {}", resource_address, err); - } - })?; - let inputs = self - .outputs_api - .resolve_output_masks_for_spending(owner_account, inputs) - .inspect_err(|_| { - // TODO: atomic rollback will help with this - if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { + if let Err(err) = self.outputs_api.release_lock(lock_id) { error!(target: LOG_TARGET, "Failed to release lock outputs for resource {}: {}", resource_address, err); } })?; - let total_confidential_spent = Amount::sum_from_positive(inputs.iter().map(|i| i.value())) + let total_confidential_spent = Amount::sum_from_positive(inputs.iter().map(|i| i.value)) // The wallet has somehow stored a negative amount, which should not happen. .expect("BUG: an unblinded input amount was negative"); @@ -252,7 +227,7 @@ where self.outputs_api.lock_revealed_funds(lock_id, &src_vault.id, revealed_to_spend) .inspect_err(|_| { // TODO: atomic rollback will help with this - if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { + if let Err(err) = self.outputs_api.release_lock(lock_id) { error!(target: LOG_TARGET, "Failed to release lock outputs for resource {}: {}", resource_address, err); } })?; @@ -270,13 +245,13 @@ where ); Ok(InputsToSpend { - inputs, + inputs: inputs.into_iter().map(|i| i.into_spend_data()).collect(), revealed: revealed_to_spend, }) }, ConfidentialTransferInputSelection::PreferConfidential => { let lock_id = self.outputs_api.create_lock()?; - let (blinded_inputs, blinded_amount_locked) = self.outputs_api.lock_outputs_until_partial_amount( + let (inputs, blinded_amount_locked) = self.outputs_api.lock_outputs_until_partial_amount( owner_account.component_address(), &resource_address, spend_amount, @@ -288,7 +263,7 @@ where .unwrap_or_else(Amount::zero); if available_revealed_funds < revealed_to_spend { - self.outputs_api.release_locked_outputs(lock_id)?; + self.outputs_api.release_lock(lock_id)?; return Err(StealthTransferApiError::InsufficientFunds); } @@ -299,7 +274,7 @@ where .lock_revealed_funds(lock_id, &vault.id, revealed_to_spend)?; }, None => { - if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { + if let Err(err) = self.outputs_api.release_lock(lock_id) { error!(target: LOG_TARGET, "🚨 Failed to release lock outputs for resource {}: {}", resource_address, err); } return Err(StealthTransferApiError::InsufficientRevealedFunds { @@ -315,12 +290,8 @@ where } } - let inputs = self - .outputs_api - .resolve_output_masks_for_spending(owner_account, blinded_inputs)?; - Ok(InputsToSpend { - inputs, + inputs: inputs.into_iter().map(|i| i.into_spend_data()).collect(), revealed: revealed_to_spend, }) }, @@ -336,6 +307,16 @@ where let network = self.config_api.get_network()?; params.validate(network)?; + let Some(owner_key_id) = owner_account.owner_key_id() else { + return Err(StealthTransferApiError::InvalidParameter { + param: "owner_account", + reason: format!( + "Account {} is view only and does not have the required secrets for transfers", + owner_account.component_address() + ), + }); + }; + let destination_account = derive_account_address_from_public_key(params.destination_address.account_public_key()); @@ -347,7 +328,7 @@ where .try_from_byte_type() .map_err(|e| StealthTransferApiError::InvalidParameter { param: "owner_account", - reason: format!("Invalid owner account address: {e}"), + reason: format!("Non-canonical owner account address: {e}"), })?; // add the input for the resource address to be transferred @@ -448,79 +429,51 @@ where .try_from_byte_type() .expect("already validated"); - let output_statement = params - .blinded_output_amount - .is_positive() - .then(|| { - self.create_output_statement( - &destination_address, - params.blinded_output_amount, - ¶ms.resource_address, - resource_view_key.clone(), - params.output_memo.as_ref(), - ) - }) - .transpose()?; - // Resolve fee inputs let lock_id = self.outputs_api.create_lock()?; - let fee_inputs_to_spend = self.resolve_fee_inputs(lock_id, &owner_account, ¶ms)?; + let fee_inputs_to_spend = self.lock_fee_inputs(lock_id, &owner_account, ¶ms)?; // TODO: use single db transaction across calls // --- Any error from here can result in funds staying locked --- - let fee_change = fee_inputs_to_spend + let fee_stealth_change_amt = fee_inputs_to_spend .total_stealth_input_amount() .saturating_sub(params.max_fee.into()); // Generate fee change outputs if required - let fee_change_output_statement = fee_change - .is_positive() - .then(|| self.create_output_statement(&owner_address, fee_change, ¶ms.resource_address, None, None)) - .transpose() - .inspect_err(|e| { - warn!(target: LOG_TARGET, "Unlocking fee fund locks after error: {}", e); - // This is a hack that addresses the case where output creation fails after the fee transaction. - // However, any error after this point do not undo locking. This is a limitation - // of the current design - the db transaction should be passed in and - // automatically rolled back on error. - if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { - error!( - target: LOG_TARGET, - "Failed to release fee inputs for transfer: {}", - err - ); - } - })?; - // Generate fee reveal statement - let fee_transfer_statement = self.crypto_api.generate_transfer_statement( - fee_inputs_to_spend.statements_iter(), - fee_inputs_to_spend.revealed, - fee_change_output_statement.iter(), - Amount::from(params.max_fee), - )?; - - if let Some(ref fee_change) = fee_change_output_statement { + let fee_change_output = Some(OutputToCreate { + owner_address: &owner_address, + amount: fee_stealth_change_amt, + memo: None, + }) + .filter(|o| o.amount.is_positive()); + + // Generate fee transfer statement + let fee_transfer_statement = self.outputs_api.generate_transfer_statement(TransferStatementParams { + spend_key_id: owner_key_id, + view_only_key_id: owner_account.view_only_key_id(), + resource_address: ¶ms.resource_address, + resource_view_key: resource_view_key.clone(), + inputs: &fee_inputs_to_spend.inputs, + input_revealed_amount: fee_inputs_to_spend.revealed, + outputs: fee_change_output.into_iter(), + output_revealed_amount: Amount::from(params.max_fee), + })?; + + // Add the unconfirmed fee change output to the wallet store + if let Some(output) = fee_transfer_statement.outputs_statement.outputs.first() { self.add_unconfirmed_output_from_statement( lock_id, &owner_account, params.resource_address, - fee_change, + output, + fee_stealth_change_amt, None, )?; } - substate_inputs.extend( - fee_inputs_to_spend - .inputs - .iter() - .filter(|i| i.is_on_chain) - .map(|i| &i.statement) - .map(|i| SubstateRequirement::unversioned(to_utxo_address(XTR, &i.mask_and_value))), - ); - // Reserve and lock input funds - let inputs_to_spend = match self.resolved_inputs_for_transfer( + let inputs_to_spend = match self.lock_inputs_for_transfer( lock_id, owner_account.account(), params.resource_address, @@ -534,7 +487,7 @@ where // However, any error after this point do not undo locking. This is a limitation // of the current design - the db transaction should be passed in and // automatically rolled back on error. - if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { + if let Err(err) = self.outputs_api.release_lock(lock_id) { error!( target: LOG_TARGET, "Failed to release fee inputs for transfer: {}", @@ -571,38 +524,65 @@ where } } + // Any change outputs? + let change_amount = inputs_to_spend + .total_amount() + .checked_sub_positive(params.total_output_amount()) + .unwrap_or_else(|| { + // This is a bug because the wallet chooses inputs based on the required outputs. This function should + // not have been called if there are insufficient funds. + error!( + target: LOG_TARGET, + "BUG: total_stealth_input_amount or params.total_amount() are negative after validation" + ); + panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); + }); + + let change_output = Some(OutputToCreate { + owner_address: &owner_address, + amount: change_amount, + memo: None, + }) + .filter(|o| o.amount.is_positive()); + + let transfer_statement = self.outputs_api.generate_transfer_statement(TransferStatementParams { + spend_key_id: owner_key_id, + view_only_key_id: owner_account.view_only_key_id(), + resource_address: ¶ms.resource_address, + resource_view_key, + inputs: &inputs_to_spend.inputs, + input_revealed_amount: inputs_to_spend.revealed, + outputs: Some(OutputToCreate { + amount: params.blinded_output_amount, + owner_address: &destination_address, + memo: params.output_memo.as_ref(), + }) + .into_iter() + .chain(change_output), + output_revealed_amount: params.revealed_output_amount, + })?; + // Add all input UTXO substates to transaction inputs substate_inputs.extend( - inputs_to_spend + fee_inputs_to_spend .inputs .iter() + // If spending XTR, we may lock the fee change UTXO for spending, however since this does not exist yet we do not include it as a tx input .filter(|i| i.is_on_chain) - .map(|i| &i.statement) - .map(|i| SubstateRequirement::unversioned(to_utxo_address(params.resource_address, &i.mask_and_value))), + .map(|i| &i.commitment) + .map(|commitment| UtxoAddress::new(XTR, (*commitment).into())) + .map(SubstateRequirement::unversioned), ); - // Any change outputs? - let maybe_change_statement = self.generate_change_statement( - lock_id, - &owner_account, - params.resource_address, - resource_view_key, - &inputs_to_spend, - params.total_output_amount(), - )?; - - let outputs = output_statement - .filter(|o| o.statement.amount.is_positive()) - .into_iter() - .chain(maybe_change_statement) - .collect::>(); - - let transfer_statement = self.crypto_api.generate_transfer_statement( - inputs_to_spend.statements_iter(), - inputs_to_spend.revealed, - &outputs, - params.revealed_output_amount, - )?; + substate_inputs.extend( + inputs_to_spend + .inputs + .iter() + .filter(|i| i.is_on_chain) + .map(|i| &i.commitment) + .map(|commitment| UtxoAddress::new(params.resource_address, (*commitment).into())) + .map(SubstateRequirement::unversioned), + ); let result = self.generate_transfer_transaction( &owner_account, @@ -614,17 +594,15 @@ where ); match result { - Ok(transaction) => { - let tx_id = transaction.calculate_id(); - self.outputs_api.locks_set_transaction_id(lock_id, tx_id)?; - Ok(TransferOutput { - transaction, - transaction_lock_id: lock_id, - }) - }, + Ok(transaction) => Ok(TransferOutput { + transaction, + lock_id, + fee_inputs: fee_inputs_to_spend, + transfer_inputs: inputs_to_spend, + }), Err(err) => { // Unlock inputs - if let Err(e) = self.outputs_api.release_locked_outputs(lock_id) { + if let Err(e) = self.outputs_api.release_lock(lock_id) { error!(target: LOG_TARGET, "Failed to release inputs lock after error: {}", e); } Err(err) @@ -640,24 +618,9 @@ where fee_transfer_statement: StealthTransferStatement, transfer_statement: StealthTransferStatement, need_to_create_account: bool, - ) -> Result { + ) -> Result { let revealed_input_amount = transfer_statement.inputs_statement.revealed_amount; let revealed_output_amount = transfer_statement.outputs_statement.revealed_output_amount; - let owner_key_id = owner_account - .owner_key_id() - .ok_or_else(|| StealthTransferApiError::InvalidParameter { - param: "owner_account", - reason: "Owner account has no owner key".to_string(), - })?; - - let signer_key = if revealed_input_amount.is_positive() || - fee_transfer_statement.inputs_statement.revealed_amount.is_positive() - { - self.key_manager_api.get_account_owner_key(owner_key_id)? - } else { - // Since we don't require account auth, use a throwaway nonce to sign the transaction - self.key_manager_api.next_key(KeyBranch::Nonce)?.into() - }; let transaction = Transaction::builder() .for_network(params.destination_address.network().as_byte()) @@ -713,87 +676,32 @@ where .with_inputs(inputs) // TODO: remove the need to add this input .add_input(XTR) - .build_and_seal(&signer_key.secret); + .build_unsigned_transaction(); Ok(transaction) } - fn generate_change_statement( - &self, - lock_id: WalletLockId, - account: &AccountWithAddress, - resource_address: ResourceAddress, - resource_view_key: Option, - inputs_to_spend: &InputsToSpend, - total_output_amount: Amount, - ) -> Result, StealthTransferApiError> { - let change_amount = inputs_to_spend - .total_amount() - .checked_sub_positive(total_output_amount) - .unwrap_or_else(|| { - // This is a bug because the wallet chooses inputs based on the required outputs. This function should - // not have been called if there are insufficient funds. - error!( - target: LOG_TARGET, - "BUG: total_stealth_input_amount or params.total_amount() are negative after validation" - ); - panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); - }); - - if change_amount.is_zero() { - return Ok(None); - } - - let change_address = - account - .address - .try_from_byte_type() - .map_err(|e| StealthTransferApiError::InvalidParameter { - param: "owner_account", - reason: format!("Invalid owner account address: {e}"), - })?; - - let change = self.create_output_statement( - &change_address, - change_amount, - &resource_address, - resource_view_key, - None, - )?; - - self.add_unconfirmed_output_from_statement(lock_id, account, resource_address, &change, None)?; - - Ok(Some(change)) - } - fn add_unconfirmed_output_from_statement( &self, lock_id: WalletLockId, account: &AccountWithAddress, resource_address: ResourceAddress, - output: &UnblindedStealthOutputStatement, + output: &StealthUnspentOutput, + value: Amount, memo: Option, ) -> Result<(), StealthTransferApiError> { - let output_value = output.statement.amount; - if output_value.is_zero() { - return Ok(()); - } - self.outputs_api.add_output(&StealthOutputModel { owner_account: *account.component_address(), resource_address, - commitment: output - .statement - .to_commitment() - .expect("BUG: to_commitment negative amount") - .to_byte_type(), - value: output_value, - sender_public_nonce: output.statement.sender_public_nonce.to_byte_type(), + commitment: output.output.commitment, + value, + sender_public_nonce: output.output.sender_public_nonce, view_only_key_id: account.view_only_key_id(), owner_key_id: account.owner_key_id(), - encrypted_data: output.statement.encrypted_data.clone(), + encrypted_data: output.output.encrypted_data.clone(), status: OutputStatus::LockedUnconfirmed, memo, + minimum_value_promise: output.output.minimum_value_promise, tag_byte: output.tag, lock_id: Some(lock_id), is_burnt: false, @@ -802,74 +710,13 @@ where })?; Ok(()) } - - fn create_output_statement( - &self, - destination: &RistrettoOotleAddress, - amount: Amount, - resource_address: &ResourceAddress, - resource_view_key: Option, - memo: Option<&Memo>, - ) -> Result { - if !amount.is_positive() { - return Err(StealthTransferApiError::InvalidParameter { - param: "amount", - reason: format!("Amount must be positive, got {}", amount), - }); - } - - let mask = self.key_manager_api.next_key(KeyBranch::StealthMask)?; - - let (nonce_secret, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); - let encrypted_data = self.crypto_api.encrypt_value_and_mask( - amount - .to_u64_checked() - .ok_or_else(|| StealthTransferApiError::AmountOverflow { - param: "amount", - details: "Stealth amount exceeds u64::MAX. This is currently a limitation due to the format of \ - EncryptedData" - .to_string(), - })?, - &mask.key, - destination.view_only_key(), - &nonce_secret, - memo, - )?; - - // Create stealth address - used during spend time - let output_owner_public_key = self.crypto_api.derive_stealth_owner_public_key( - destination.network(), - destination.account_key(), - &nonce_secret, - ); - - let statement = UnblindedOutputStatement { - amount, - mask: mask.key, - sender_public_nonce: public_nonce, - encrypted_data, - minimum_value_promise: 0, - resource_view_key, - }; - - let derived_tag = self.crypto_api.derive_stealth_output_tag( - destination.network(), - &nonce_secret, - destination.view_only_key(), - resource_address, - ); - - Ok(UnblindedStealthOutputStatement { - statement, - output_owner_public_key, - tag: derived_tag, - }) - } } pub struct TransferOutput { - pub transaction: Transaction, - pub transaction_lock_id: WalletLockId, + pub transaction: UnsignedTransaction, + pub lock_id: WalletLockId, + pub fee_inputs: InputsToSpend, + pub transfer_inputs: InputsToSpend, } #[derive(Debug)] @@ -947,26 +794,32 @@ impl StealthTransferParams { } #[derive(Debug)] -pub struct InputToSpend { - pub statement: UnblindedStealthInputStatement, - pub is_on_chain: bool, +pub struct UnblindedInputToSpend { + pub witness: UnblindedStealthInputWitness, } -impl InputToSpend { +impl UnblindedInputToSpend { pub fn value(&self) -> Amount { - self.statement.mask_and_value.value + self.witness.mask_and_value.value } } +#[derive(Debug, Clone, Copy)] +pub struct OutputToCreate<'a> { + pub owner_address: &'a RistrettoOotleAddress, + pub amount: Amount, + pub memo: Option<&'a Memo>, +} + #[derive(Debug)] pub struct InputsToSpend { - pub inputs: Vec, + pub inputs: Vec, pub revealed: Amount, } impl InputsToSpend { - pub fn statements_iter(&self) -> impl Iterator + '_ { - self.inputs.iter().map(|i| &i.statement) + pub fn inputs_iter(&self) -> impl Iterator + '_ { + self.inputs.iter() } pub fn total_amount(&self) -> Amount { @@ -974,7 +827,7 @@ impl InputsToSpend { } pub fn total_stealth_input_amount(&self) -> Amount { - self.inputs.iter().map(|i| i.statement.mask_and_value.value).sum() + self.inputs.iter().map(|i| i.value).sum() } } @@ -990,8 +843,6 @@ pub enum StealthTransferApiError { SubstateApi(#[from] SubstateApiError), #[error("Insufficient funds")] InsufficientFunds, - #[error("Key manager error: {0}")] - KeyManager(#[from] KeyManagerApiError), #[error("Accounts API error: {0}")] Accounts(#[from] AccountsApiError), #[error("Invalid parameter `{param}`: {reason}")] @@ -1017,14 +868,3 @@ pub struct AccountDetails { pub vaults: Vec, pub exists: bool, } - -fn to_utxo_address(resource_address: ResourceAddress, mask_and_value: &MaskAndValue) -> UtxoAddress { - UtxoAddress::new( - resource_address, - mask_and_value - .to_commitment() - .expect("BUG: value not u64") - .to_byte_type() - .into(), - ) -} diff --git a/crates/wallet/sdk/src/apis/transaction.rs b/crates/wallet/sdk/src/apis/transaction.rs index b4fb0e60de..e0ccb34069 100644 --- a/crates/wallet/sdk/src/apis/transaction.rs +++ b/crates/wallet/sdk/src/apis/transaction.rs @@ -50,7 +50,8 @@ where Ok(transaction) } - pub async fn insert_new_transaction( + /// Inserts a new transaction into the wallet database with status `New`. + pub fn insert_new_transaction( &self, transaction: Transaction, new_account_info: Option, @@ -63,6 +64,11 @@ where Ok(tx_id) } + /// Submits a transaction to the network. The transaction must be in the `New` status. + /// If the submission is successful, the transaction status is updated to `Pending`. + /// If the transaction is rejected, the status is updated to `InvalidTransaction` and the + /// rejection reason is stored. + /// Returns `Ok(true)` if the transaction was successfully submitted, `Ok(false)` if it was rejected pub async fn submit_transaction(&self, transaction_id: TransactionId) -> Result { let transaction = self.store.with_read_tx(|tx| tx.transactions_get(transaction_id))?; diff --git a/crates/wallet/sdk/src/lib.rs b/crates/wallet/sdk/src/lib.rs index dfd2991add..a10685da52 100644 --- a/crates/wallet/sdk/src/lib.rs +++ b/crates/wallet/sdk/src/lib.rs @@ -15,4 +15,9 @@ pub mod network; pub type WalletSecretKey = tari_transaction_components::key_manager::tari_key_manager::DerivedKey; +// Re-export commonly used types +pub use tari_common_types::seeds::seed_words::SeedWords; +pub use tari_ootle_address::*; +pub use tari_ootle_common_types::Network; pub use tari_ootle_wallet_crypto as crypto; +pub use tari_template_lib::constants; diff --git a/crates/wallet/sdk/src/models/key.rs b/crates/wallet/sdk/src/models/key.rs index b1254d9483..17c23afb32 100644 --- a/crates/wallet/sdk/src/models/key.rs +++ b/crates/wallet/sdk/src/models/key.rs @@ -92,6 +92,10 @@ impl Key { &self.secret } + pub fn key_id(&self) -> &KeyId { + &self.key_id + } + pub fn to_public_key(&self) -> RistrettoPublicKey { RistrettoPublicKey::from_secret_key(&self.secret) } diff --git a/crates/wallet/sdk/src/models/stealth_output.rs b/crates/wallet/sdk/src/models/stealth_output.rs index 9dd3b66174..f72eee3679 100644 --- a/crates/wallet/sdk/src/models/stealth_output.rs +++ b/crates/wallet/sdk/src/models/stealth_output.rs @@ -24,6 +24,7 @@ pub struct StealthOutputModel { pub encrypted_data: EncryptedData, pub tag_byte: UtxoTag, pub memo: Option, + pub minimum_value_promise: u64, pub status: OutputStatus, pub is_burnt: bool, pub is_frozen: bool, @@ -35,6 +36,25 @@ impl StealthOutputModel { pub fn to_utxo_address(&self) -> UtxoAddress { UtxoAddress::new(self.resource_address, self.commitment.into()) } + + pub fn into_spend_data(self) -> InputSpendData { + InputSpendData { + commitment: self.commitment, + public_nonce: self.sender_public_nonce, + encrypted_data: self.encrypted_data, + value: self.value, + is_on_chain: self.is_on_chain, + } + } +} + +#[derive(Debug, Clone)] +pub struct InputSpendData { + pub commitment: PedersenCommitmentBytes, + pub public_nonce: RistrettoPublicKeyBytes, + pub encrypted_data: EncryptedData, + pub value: Amount, + pub is_on_chain: bool, } pub struct StealthBalance { diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs index 7e4b814bac..4e0e3ffe63 100644 --- a/crates/wallet/sdk/src/sdk.rs +++ b/crates/wallet/sdk/src/sdk.rs @@ -69,9 +69,17 @@ where config: WalletSdkConfig, ) -> Result, WalletSdkError> { // initialize network - let config_api = ConfigApi::new(&store); - if !config_api.exists(ConfigKey::Network)? { - config_api.set(ConfigKey::Network, config.network.as_key_str())?; + if let Some(network) = Self::get_store_network(&store)? { + if config.network != network { + return Err(WalletSdkError::InvariantError { + details: format!( + "Network mismatch. Config network is {:?} but database network is {:?}", + config.network, network + ), + }); + } + } else { + ConfigApi::new(&store).set(ConfigKey::Network, config.network.as_key_str())?; } Ok(Self { @@ -82,6 +90,12 @@ where }) } + pub fn get_store_network(store: &TStore) -> Result, WalletSdkError> { + let config_api = ConfigApi::new(store); + let network = config_api.get(ConfigKey::Network).optional()?; + Ok(network) + } + /// Initializes the cipher seed for the wallet. Either creating a new cipher seed or recovering it from the provided /// seed words if provided and necessary. Returns true if the cipher seed was recovered from the seed words, /// otherwise false. @@ -201,11 +215,9 @@ where pub fn stealth_transfer_api(&self) -> StealthTransferApi<'_, TStore, TNetworkInterface> { StealthTransferApi::new( - self.key_manager_api(), self.accounts_api(), self.stealth_outputs_api(), self.substate_api(), - self.stealth_crypto_api(), self.config_api(), ) } @@ -273,13 +285,11 @@ where } /// Retrieve the seed words from current cipher seed stored. - pub fn load_seed_words(&mut self) -> Result { + pub fn load_seed_words(&mut self) -> Result, WalletSdkError> { let seed_words = self .load_cipher_seed()? - .ok_or_else(|| WalletSdkError::InvariantError { - details: "call to load_cipher_seed without initializing the cipher seed".to_string(), - })? - .to_mnemonic(MnemonicLanguage::English, None)?; + .map(|s| s.to_mnemonic(MnemonicLanguage::English, None)) + .transpose()?; Ok(seed_words) } } diff --git a/crates/wallet/sdk_services/src/account_monitor/handle.rs b/crates/wallet/sdk_services/src/account_monitor/handle.rs index 3dc39f2cc1..f28297cffa 100644 --- a/crates/wallet/sdk_services/src/account_monitor/handle.rs +++ b/crates/wallet/sdk_services/src/account_monitor/handle.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_template_lib::models::ComponentAddress; +use tari_template_lib::models::{ComponentAddress, ResourceAddress}; use tokio::sync::{mpsc, oneshot}; use crate::{account_monitor::monitor::AccountMonitorError, Reply}; @@ -13,6 +13,11 @@ pub(super) enum AccountMonitorRequest { scan_for_utxos: bool, reply: Reply>, }, + AssociateResource { + account: ComponentAddress, + resource: ResourceAddress, + reply: Reply>, + }, } #[derive(Debug, Clone)] @@ -48,4 +53,21 @@ impl AccountMonitorHandle { .map_err(|_| AccountMonitorError::ServiceShutdown)?; reply_rx.await.map_err(|_| AccountMonitorError::ServiceShutdown)? } + + pub async fn associate_resource( + &self, + account: ComponentAddress, + resource: ResourceAddress, + ) -> Result<(), AccountMonitorError> { + let (reply_tx, reply_rx) = oneshot::channel(); + self.sender + .send(AccountMonitorRequest::AssociateResource { + account, + resource, + reply: reply_tx, + }) + .await + .map_err(|_| AccountMonitorError::ServiceShutdown)?; + reply_rx.await.map_err(|_| AccountMonitorError::ServiceShutdown)? + } } diff --git a/crates/wallet/sdk_services/src/account_monitor/monitor.rs b/crates/wallet/sdk_services/src/account_monitor/monitor.rs index 0c214c2101..9db4dc4e6d 100644 --- a/crates/wallet/sdk_services/src/account_monitor/monitor.rs +++ b/crates/wallet/sdk_services/src/account_monitor/monitor.rs @@ -4,8 +4,8 @@ use std::{collections::HashMap, time::Duration}; use log::*; -use tari_engine_types::indexed_value::IndexedValueError; -use tari_ootle_common_types::optional::IsNotFoundError; +use tari_engine_types::{indexed_value::IndexedValueError, resource::Resource}; +use tari_ootle_common_types::optional::{IsNotFoundError, Optional}; use tari_ootle_wallet_sdk::{ apis::{ accounts::AccountsApiError, @@ -22,7 +22,7 @@ use tari_ootle_wallet_sdk::{ WalletSdk, }; use tari_shutdown::ShutdownSignal; -use tari_template_lib::prelude::ComponentAddress; +use tari_template_lib::{models::ResourceAddress, prelude::ComponentAddress}; use tari_transaction::TransactionId; use tokio::{ sync::{broadcast, mpsc}, @@ -134,9 +134,37 @@ where } => { let _ignore = reply.send(self.refresh_account(account, scan_for_utxos).await); }, + AccountMonitorRequest::AssociateResource { + account, + resource, + reply, + } => { + let _ignore = reply.send(self.associate_resource_with_account(&account, resource).await); + }, } } + async fn associate_resource_with_account( + &self, + account_address: &ComponentAddress, + resource_address: ResourceAddress, + ) -> Result<(), AccountMonitorError> { + let accounts_api = self.wallet_sdk.accounts_api(); + self.fetch_and_cache_resource(&resource_address).await?; + accounts_api.associate_stealth_resource(account_address, resource_address)?; + Ok(()) + } + + async fn fetch_and_cache_resource(&self, resx_addr: &ResourceAddress) -> Result { + if let Some(resx) = self.wallet_sdk.resources_api().get(resx_addr).optional()? { + return Ok(resx); + } + + let resource = self.wallet_sdk.substate_api().fetch_resource(*resx_addr).await?; + self.wallet_sdk.resources_api().upsert_resource(resx_addr, &resource)?; + Ok(resource) + } + async fn on_poll(&self) { if let Err(err) = self.refresh_all_accounts().await { error!(target: LOG_TARGET, "Error refreshing all accounts: {}", err); diff --git a/crates/wallet/sdk_services/src/transaction_service/service.rs b/crates/wallet/sdk_services/src/transaction_service/service.rs index 73faa11577..cefe27fcd6 100644 --- a/crates/wallet/sdk_services/src/transaction_service/service.rs +++ b/crates/wallet/sdk_services/src/transaction_service/service.rs @@ -151,9 +151,7 @@ where new_account_info: Option, ) -> Result { let transaction_api = self.wallet_sdk.transaction_api(); - let transaction_id = transaction_api - .insert_new_transaction(transaction, new_account_info.clone(), false) - .await?; + let transaction_id = transaction_api.insert_new_transaction(transaction, new_account_info.clone(), false)?; if transaction_api.submit_transaction(transaction_id).await? { self.notify.notify(TransactionSubmittedEvent { transaction_id, diff --git a/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql b/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql index 1895ecb7c4..74e52c7dfe 100644 --- a/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql +++ b/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql @@ -232,26 +232,27 @@ CREATE TABLE webauthn_registration_passkeys -- Stealth Outputs CREATE TABLE stealth_outputs ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - owner_account_id INTEGER NOT NULL REFERENCES accounts (id), - resource_address TEXT NOT NULL, - commitment TEXT NOT NULL, - value TEXT NOT NULL, - sender_public_nonce TEXT NOT NULL, + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + owner_account_id INTEGER NOT NULL REFERENCES accounts (id), + resource_address TEXT NOT NULL, + commitment TEXT NOT NULL, + value TEXT NOT NULL, + sender_public_nonce TEXT NOT NULL, -- Status can be "Unspent", "Spent", "Locked", "LockedUnconfirmed", "Invalid" - status TEXT NOT NULL, - locked_at DATETIME NULL, - lock_id INTEGER NULL, - view_only_key_id TEXT NOT NULL, - owner_key_id TEXT NULL, - encrypted_data BLOB NOT NULL DEFAULT '', - tag_byte INTEGER NOT NULL, - memo_json TEXT NULL, - is_burnt BOOLEAN NOT NULL DEFAULT 0, - is_frozen BOOLEAN NOT NULL DEFAULT 0, - is_on_chain BOOLEAN NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + status TEXT NOT NULL, + locked_at DATETIME NULL, + lock_id INTEGER NULL, + view_only_key_id TEXT NOT NULL, + owner_key_id TEXT NULL, + encrypted_data BLOB NOT NULL DEFAULT '', + tag_byte INTEGER NOT NULL, + memo_json TEXT NULL, + minimum_value_promise BIGINT NOT NULL, + is_burnt BOOLEAN NOT NULL DEFAULT 0, + is_frozen BOOLEAN NOT NULL DEFAULT 0, + is_on_chain BOOLEAN NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX stealth_outputs_uniq_resource_addr_commitment ON stealth_outputs (resource_address, commitment); diff --git a/crates/wallet/storage_sqlite/src/models/stealth_output.rs b/crates/wallet/storage_sqlite/src/models/stealth_output.rs index d75f150a94..832dced506 100644 --- a/crates/wallet/storage_sqlite/src/models/stealth_output.rs +++ b/crates/wallet/storage_sqlite/src/models/stealth_output.rs @@ -35,6 +35,7 @@ pub struct StealthOutput { pub encrypted_data: Vec, pub tag_byte: i32, pub memo_json: Option, + pub minimum_value_promise: i64, pub is_burnt: bool, pub is_frozen: bool, pub is_on_chain: bool, @@ -81,6 +82,7 @@ impl StealthOutput { })?, tag_byte: UtxoTag::new(self.tag_byte as u32), memo: self.memo_json.as_ref().map(deserialize_json).transpose()?, + minimum_value_promise: self.minimum_value_promise as u64, status: self.status.parse().map_err(|_| WalletStorageError::DecodingError { operation: "try_into_output", item: "output", diff --git a/crates/wallet/storage_sqlite/src/schema.rs b/crates/wallet/storage_sqlite/src/schema.rs index 083801da62..ac65adb2e0 100644 --- a/crates/wallet/storage_sqlite/src/schema.rs +++ b/crates/wallet/storage_sqlite/src/schema.rs @@ -161,6 +161,7 @@ diesel::table! { encrypted_data -> Binary, tag_byte -> Integer, memo_json -> Nullable, + minimum_value_promise -> BigInt, is_burnt -> Bool, is_frozen -> Bool, is_on_chain -> Bool, diff --git a/crates/wallet/storage_sqlite/src/writer.rs b/crates/wallet/storage_sqlite/src/writer.rs index 66ffdc3719..b4cf14139e 100644 --- a/crates/wallet/storage_sqlite/src/writer.rs +++ b/crates/wallet/storage_sqlite/src/writer.rs @@ -1125,6 +1125,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { stealth_outputs::encrypted_data.eq(output.encrypted_data.as_ref()), stealth_outputs::tag_byte.eq(output.tag_byte.value() as i32), stealth_outputs::memo_json.eq(output.memo.as_ref().map(serialize_json).transpose()?), + stealth_outputs::minimum_value_promise.eq(output.minimum_value_promise as i64), stealth_outputs::is_on_chain.eq(output.is_on_chain), stealth_outputs::status.eq(output.status.as_key_str()), stealth_outputs::is_burnt.eq(output.is_burnt), diff --git a/utilities/tariswap_test_bench/src/runner.rs b/utilities/tariswap_test_bench/src/runner.rs index f31ef8b864..1b3e103197 100644 --- a/utilities/tariswap_test_bench/src/runner.rs +++ b/utilities/tariswap_test_bench/src/runner.rs @@ -49,8 +49,7 @@ impl Runner { let tx_id = self .sdk .transaction_api() - .insert_new_transaction(transaction, None, false) - .await?; + .insert_new_transaction(transaction, None, false)?; self.sdk.transaction_api().submit_transaction(tx_id).await?;