diff --git a/Cargo.lock b/Cargo.lock
index 7ecb5ef96e..993d2b71a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11636,6 +11636,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.17",
"time",
+ "tokio",
"ts-rs",
"webauthn-rs",
"zeroize",
diff --git a/applications/tari_indexer/src/lib.rs b/applications/tari_indexer/src/lib.rs
index 8c27970aa4..94e5b5e1a9 100644
--- a/applications/tari_indexer/src/lib.rs
+++ b/applications/tari_indexer/src/lib.rs
@@ -190,7 +190,7 @@ pub async fn run_indexer(config: ApplicationConfig, mut shutdown_signal: Shutdow
},
_ = shutdown_signal.wait() => {
- dbg!("Shutting down run_substate_polling");
+ debug!(target: LOG_TARGET, "Shutting down run_substate_polling");
break;
},
}
diff --git a/applications/tari_indexer/src/network_state_sync/worker.rs b/applications/tari_indexer/src/network_state_sync/worker.rs
index 601ce8a55e..121301932c 100644
--- a/applications/tari_indexer/src/network_state_sync/worker.rs
+++ b/applications/tari_indexer/src/network_state_sync/worker.rs
@@ -503,12 +503,17 @@ fn extend_bufs_from_substate_update(
value: create.substate.value().clone(),
});
},
- Some(_) => {},
+ Some(_) => {
+ warn!(target: LOG_TARGET, "⚠️ NEVER HAPPEN: Received unexpected substate value for created substate: {}", create.substate.substate_id());
+ },
None => {
let id = create.substate.substate_id();
- if id.is_template() || id.is_transaction_receipt() || id.is_utxo() {
+ if id.is_template() || id.is_transaction_receipt() {
warn!(target: LOG_TARGET, "⚠️ NEVER HAPPEN: Received substate {id} update with no value");
}
+ if let Some(addr) = id.as_utxo_address() {
+ debug!(target: LOG_TARGET, "🌍️ Received UTXO substate {addr} creation with no value. Ignoring as this means it is spent later.");
+ }
},
},
SubstateUpdateProof::Destroy(destroy) => match &destroy.substate_id {
diff --git a/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx b/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx
index 882501864c..fe529219cd 100644
--- a/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx
+++ b/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx
@@ -59,6 +59,7 @@ const SUBSTATE_TYPES = [
"TransactionReceipt",
"ValidatorFeePool",
"Template",
+ "Utxo",
] as const;
type ExtendedSubstateItem = ListSubstateItem & { id: string; show?: boolean };
@@ -90,7 +91,7 @@ function SubstatesLayout() {
const extendedSubstates = useMemo(
() => substates.map((substate) => ({ ...substate, id: substateIdToString(substate.substate_id) })),
- [substates]
+ [substates],
);
const visibleSubstates = filteredSubstates.filter((substate) => substate.show !== false);
diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs
index 90230ac987..a1cf2495c9 100644
--- a/applications/tari_walletd/src/handlers/accounts.rs
+++ b/applications/tari_walletd/src/handlers/accounts.rs
@@ -3,6 +3,7 @@
use std::{array, collections::HashSet};
+use anyhow::Context;
use axum_extra::headers::authorization::Bearer;
use indexmap::IndexMap;
use log::*;
@@ -36,7 +37,7 @@ use tari_template_lib::{
constants::{STEALTH_TARI_RESOURCE_ADDRESS, XTR, XTR_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_VAULT_ADDRESS},
types::{Amount, ResourceType},
};
-use tari_transaction::args;
+use tari_transaction::{args, TransactionSignature};
use tari_wallet_daemon_client::{
permissions::JrpcPermission,
types::{
@@ -284,7 +285,7 @@ pub async fn handle_get_balances(
let vaults = sdk.accounts_api().get_vaults_by_account(account.component_address())?;
let stealth_outputs = sdk
.stealth_outputs_api()
- .get_unspent_outputs_by_account(account.component_address())?;
+ .get_unspent_outputs_by_account(account.component_address(), false)?;
let mut balances = Vec::with_capacity(vaults.len());
let mut vaulted_resources = HashSet::new();
@@ -683,6 +684,7 @@ pub async fn handle_create_free_test_coins(
account.is_confirmed_on_chain().then(|| NewAccountData {
address: *account.component_address(),
}),
+ None,
)
.await?;
@@ -987,13 +989,27 @@ pub async fn handle_stealth_transfer(
task::spawn(async move {
let transfer = sdk.stealth_transfer_api().transfer(owner_account, params).await?;
- let transaction = transfer.transaction.authorized_sealed_signer().build();
+ let transaction = transfer.transaction.authorized_sealed_signer();
+ let main_pk = transfer.main_signer.public_key().to_byte_type();
+ // Add additional signature if needed
+ let additional_sig = transfer
+ .additional_signer
+ .as_ref()
+ .map(|s| {
+ sdk.local_signer_api()
+ .get_signature(s.branch, s.key_id, &main_pk, &transaction)
+ })
+ .transpose()?
+ .map(|sig| TransactionSignature::new(sig.public_key.to_byte_type(), sig.signature.to_byte_type()));
+
+ let transaction = transaction.build_with_signatures(additional_sig.into_iter().collect());
+
+ // Sign and seal the final transaction
let transaction =
sdk.local_signer_api()
- .sign(transfer.signing_key_branch, transfer.signing_key_id, transaction)?;
+ .sign(transfer.main_signer.branch, transfer.main_signer.key_id, transaction)?;
- // TODO: if submitting fails we need to unlock the inputs again
if req.dry_run {
// 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
@@ -1009,46 +1025,25 @@ pub async fn handle_stealth_transfer(
Ok(res) => Ok(StealthTransferResponse {
transaction_id: res.finalize.transaction_hash.into(),
}),
- Err(e) => {
- 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
- );
- }
-
- Err(anyhow::anyhow!("Dry run transaction failed: {}", e))
- },
+ Err(e) => Err(anyhow::anyhow!("Dry run transaction failed: {}", e)),
};
}
- // 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 {
- transaction_id: tx_id,
- new_account: None,
- });
-
- Ok(StealthTransferResponse { transaction_id: tx_id })
- },
- Err(e) => {
- 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
- );
- }
-
- Err(anyhow::anyhow!("Transaction submission failed: {}", e))
- },
- }
+ let tx_id = sdk
+ .stealth_transfer_api()
+ .unlock_on_failure(
+ transfer.lock_id,
+ transaction_service
+ .submit_transaction_with_opts(transaction, None, Some(transfer.lock_id))
+ .await,
+ )
+ .context("Transaction failed to submit")?;
+ notifier.notify(TransactionSubmittedEvent {
+ transaction_id: tx_id,
+ new_account: None,
+ });
+
+ Ok(StealthTransferResponse { transaction_id: tx_id })
})
.await?
}
diff --git a/applications/tari_walletd/src/handlers/auth/jwt.rs b/applications/tari_walletd/src/handlers/auth/jwt.rs
index d88d74bd22..aff94c2397 100644
--- a/applications/tari_walletd/src/handlers/auth/jwt.rs
+++ b/applications/tari_walletd/src/handlers/auth/jwt.rs
@@ -7,7 +7,13 @@ use axum_extra::headers::authorization::Bearer;
use jsonwebtoken::{errors, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use tari_crypto::tari_utilities::SafePassword;
-use tari_ootle_wallet_sdk::storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter};
+use tari_ootle_wallet_sdk::storage::{
+ CommitableStore,
+ WalletStorageError,
+ WalletStore,
+ WalletStoreReader,
+ WalletStoreWriter,
+};
use tari_wallet_daemon_client::{
permissions::{Claims, JrpcPermission, JrpcPermissions},
types::EncodedJwtString,
diff --git a/applications/tari_walletd/src/services/webauthn.rs b/applications/tari_walletd/src/services/webauthn.rs
index baeba8ee46..d14db6f8d8 100644
--- a/applications/tari_walletd/src/services/webauthn.rs
+++ b/applications/tari_walletd/src/services/webauthn.rs
@@ -3,7 +3,13 @@
use std::time::{Duration, Instant};
-use tari_ootle_wallet_sdk::storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter};
+use tari_ootle_wallet_sdk::storage::{
+ CommitableStore,
+ WalletStorageError,
+ WalletStore,
+ WalletStoreReader,
+ WalletStoreWriter,
+};
use thiserror::Error;
use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration};
diff --git a/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx b/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx
index d329c33e9c..1fb116dce3 100644
--- a/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx
+++ b/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx
@@ -64,13 +64,12 @@ function StealthUtxoList({ account }: { account: Account }) {
);
const columnWidths = {
- 1: "10%",
- 2: "15%",
- 3: "20%",
- 4: "25%",
+ 1: "15%",
+ 2: "20%",
+ 3: "15%",
+ 4: "30%",
5: "10%",
6: "10%",
- 7: "10%",
};
return (
@@ -93,7 +92,6 @@ function StealthUtxoList({ account }: { account: Account }) {
Memo
Burnt
Frozen
- On Chain
@@ -116,7 +114,6 @@ function StealthUtxoList({ account }: { account: Account }) {
{utxo.is_burnt ? "Yes" : "No"}
{utxo.is_frozen ? "Yes" : "No"}
- {utxo.is_on_chain ? "Yes" : "No"}
))}
{emptyRows(page, rowsPerPage, data.utxos) > 0 && (
diff --git a/crates/engine_types/src/commit_result.rs b/crates/engine_types/src/commit_result.rs
index 052089ac2a..df78dbb724 100644
--- a/crates/engine_types/src/commit_result.rs
+++ b/crates/engine_types/src/commit_result.rs
@@ -197,6 +197,10 @@ impl FinalizeResult {
self.result.any_reject()
}
+ pub fn reject(&self) -> Option<&RejectReason> {
+ self.result.reject()
+ }
+
pub fn fee_accept_transaction_reject(&self) -> Option<(&SubstateDiff, &RejectReason)> {
self.result.fee_accept_transaction_reject()
}
@@ -288,6 +292,13 @@ impl TransactionResult {
}
}
+ pub fn reject(&self) -> Option<&RejectReason> {
+ match self {
+ Self::Reject(reject_result) => Some(reject_result),
+ _ => None,
+ }
+ }
+
pub fn expect(self, msg: &str) -> SubstateDiff {
match self {
Self::Accept(substate_diff) => substate_diff,
diff --git a/crates/engine_types/src/resource_container.rs b/crates/engine_types/src/resource_container.rs
index 79a177cd76..abee2eb87e 100644
--- a/crates/engine_types/src/resource_container.rs
+++ b/crates/engine_types/src/resource_container.rs
@@ -330,7 +330,7 @@ impl ResourceContainer {
if withdraw_amt > *revealed_amount {
return Err(ResourceError::InsufficientBalance {
details: format!(
- "Bucket contained insufficient revealed funds. Required: {}, Available: {}",
+ "Bucket or vault contained insufficient revealed funds. Required: {}, Available: {}",
withdraw_amt, revealed_amount
),
});
@@ -342,7 +342,7 @@ impl ResourceContainer {
if withdraw_amt > *revealed_amount {
return Err(ResourceError::InsufficientBalance {
details: format!(
- "Bucket contained insufficient revealed funds. Required: {}, Available: {}",
+ "Bucket or vault contained insufficient revealed funds. Required: {}, Available: {}",
withdraw_amt, revealed_amount
),
});
diff --git a/crates/wallet/crypto/src/memo.rs b/crates/wallet/crypto/src/memo.rs
index 6f7acf6e48..21d5e12ea4 100644
--- a/crates/wallet/crypto/src/memo.rs
+++ b/crates/wallet/crypto/src/memo.rs
@@ -51,13 +51,6 @@ impl Memo {
Some(Self::Bytes(b))
}
- pub fn as_bytes(&self) -> &[u8] {
- match self {
- Memo::Message(s) => s.as_bytes(),
- Memo::Bytes(b) => b.as_ref(),
- }
- }
-
pub fn len(&self) -> usize {
match self {
Memo::Message(s) => s.len(),
diff --git a/crates/wallet/sdk/Cargo.toml b/crates/wallet/sdk/Cargo.toml
index 4f54e5f4fc..7b3a4c4943 100644
--- a/crates/wallet/sdk/Cargo.toml
+++ b/crates/wallet/sdk/Cargo.toml
@@ -35,6 +35,7 @@ webauthn-rs = { workspace = true }
keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "sync-secret-service"] }
passwords = "3.1.16"
zeroize = { workspace = true, features = ["serde", "simd"] }
+tokio = { workspace = true, default-features = false, features = ["sync"] }
[dev-dependencies]
tari_ootle_wallet_storage_sqlite = { workspace = true }
diff --git a/crates/wallet/sdk/src/apis/accounts.rs b/crates/wallet/sdk/src/apis/accounts.rs
index e6900094e8..6769e88f92 100644
--- a/crates/wallet/sdk/src/apis/accounts.rs
+++ b/crates/wallet/sdk/src/apis/accounts.rs
@@ -39,7 +39,7 @@ use crate::{
WalletOotleAddressWithKeyIds,
},
network::WalletNetworkInterface,
- storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
+ storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
};
pub struct AccountsApi<'a, TStore, TNetworkInterface> {
diff --git a/crates/wallet/sdk/src/apis/confidential_outputs.rs b/crates/wallet/sdk/src/apis/confidential_outputs.rs
index 998991048d..18734e7828 100644
--- a/crates/wallet/sdk/src/apis/confidential_outputs.rs
+++ b/crates/wallet/sdk/src/apis/confidential_outputs.rs
@@ -16,7 +16,7 @@ use crate::{
key_manager::{KeyManagerApi, KeyManagerApiError},
},
models::{Account, ConfidentialOutputModel, OutputStatus, WalletLockId, WalletSecretKey},
- storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
+ storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
};
const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::confidential_outputs";
diff --git a/crates/wallet/sdk/src/apis/config.rs b/crates/wallet/sdk/src/apis/config.rs
index d8888e8c05..921337de03 100644
--- a/crates/wallet/sdk/src/apis/config.rs
+++ b/crates/wallet/sdk/src/apis/config.rs
@@ -6,7 +6,7 @@ use std::{str::FromStr, sync::OnceLock};
use serde::{de::DeserializeOwned, Serialize};
use tari_ootle_common_types::{optional::IsNotFoundError, Network};
-use crate::storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter};
+use crate::storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter};
#[derive(Debug, Clone)]
pub struct ConfigApi<'a, TStore> {
diff --git a/crates/wallet/sdk/src/apis/key_manager.rs b/crates/wallet/sdk/src/apis/key_manager.rs
index a2baaf91b2..729b52e543 100644
--- a/crates/wallet/sdk/src/apis/key_manager.rs
+++ b/crates/wallet/sdk/src/apis/key_manager.rs
@@ -34,7 +34,7 @@ use crate::{
WalletPublicKey,
WalletSecretKey,
},
- storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
+ storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
};
pub type WalletKeyManager = TariKeyManager>;
@@ -76,6 +76,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> {
key_id: KeyId::derived(index),
public_key: pk,
secret_key: key,
+ branch,
is_active: active,
});
}
@@ -137,6 +138,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> {
let imported_key = self.get_imported_key(local_key_id)?;
Ok(WalletPublicKey {
public_key: imported_key.to_public_key(),
+ branch,
key_id,
})
},
@@ -144,6 +146,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> {
let derived_key = self.derive_key(branch, index)?;
Ok(WalletPublicKey {
public_key: derived_key.to_public_key(),
+ branch,
key_id,
})
},
@@ -168,6 +171,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> {
.map_err(|e| KeyManagerApiError::KeyStoreError { source: e.into() })?;
Ok(DerivedWalletKey {
key: secret,
+ branch,
key_index: index,
})
}
@@ -254,6 +258,7 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> {
let key = self.derive_key(branch, next_key_id)?;
Ok(WalletPublicKey {
public_key: key.to_public_key(),
+ branch,
key_id: key.as_key_id(),
})
}
diff --git a/crates/wallet/sdk/src/apis/non_fungible_tokens.rs b/crates/wallet/sdk/src/apis/non_fungible_tokens.rs
index 196c1feb34..1eb18a8e15 100644
--- a/crates/wallet/sdk/src/apis/non_fungible_tokens.rs
+++ b/crates/wallet/sdk/src/apis/non_fungible_tokens.rs
@@ -12,7 +12,7 @@ use thiserror::Error;
use crate::{
models::NonFungibleToken,
- storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
+ storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
};
pub struct NonFungibleTokensApi<'a, TStore> {
diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs
index 7d0fe25023..3a4454dd84 100644
--- a/crates/wallet/sdk/src/apis/stealth_outputs.rs
+++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs
@@ -9,6 +9,7 @@ use tari_crypto::{
};
use tari_engine_types::{
component::derive_component_address_from_public_key,
+ substate::SubstateDiff,
FromByteType,
ToByteType,
Utxo,
@@ -32,7 +33,6 @@ use tari_template_lib::{
prelude::{PedersenCommitmentBytes, RistrettoPublicKeyBytes},
types::{Amount, EncryptedData},
};
-use tari_transaction::TransactionId;
use crate::{
apis::{
@@ -53,7 +53,7 @@ use crate::{
StealthOutputModel,
WalletLockId,
},
- storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
+ storage::{CommitableStore, WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
};
const LOG_TARGET: &str = "tari::ootle::wallet::apis::stealth_outputs";
@@ -108,16 +108,6 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> {
})
}
- pub fn locks_set_transaction_id(
- &self,
- lock_id: WalletLockId,
- transaction_id: TransactionId,
- ) -> Result<(), StealthOutputsApiError> {
- self.store
- .with_write_tx(|tx| tx.locks_link_transaction(lock_id, transaction_id))?;
- Ok(())
- }
-
/// Locks as many outputs required to reach at least the specified amount. If there are insufficient funds, all
/// available outputs will be locked and returned along with the total amount locked.
pub fn lock_outputs_until_partial_amount(
@@ -195,31 +185,16 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> {
}
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.vaults_release_lock_revealed_funds(lock_id).optional()?;
- tx.locks_delete(lock_id)
- })?;
+ self.store.with_write_tx(|tx| tx.locks_release(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()?;
+ pub fn finalize_lock(&self, lock_id: WalletLockId, diff: &SubstateDiff) -> Result<(), ConfidentialOutputsApiError> {
+ self.store
+ .with_write_tx(|tx| tx.locks_unlock_finalized(lock_id, diff))?;
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,
@@ -283,10 +258,11 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> {
pub fn get_unspent_outputs_by_account(
&self,
account_address: &ComponentAddress,
+ exclude_locked: bool,
) -> Result, StealthOutputsApiError> {
let balance = self
.store
- .with_read_tx(|tx| tx.stealth_outputs_get_unspent_by_account(account_address))?;
+ .with_read_tx(|tx| tx.stealth_outputs_get_unspent_by_account(account_address, exclude_locked))?;
Ok(balance)
}
@@ -314,13 +290,18 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> {
pub fn upsert_utxo(&self, utxo: &StealthOutputModel) -> Result<(), StealthOutputsApiError> {
self.store.with_write_tx(|tx| {
// TODO(perf): consider a dedicated exists query
- let exists = tx
+ let maybe_utxo = tx
.stealth_outputs_get_by_commitment(&utxo.resource_address, &utxo.commitment)
- .optional()?
- .is_some();
- if exists {
+ .optional()?;
+ if let Some(prev_utxo) = maybe_utxo {
+ let new_status = match prev_utxo.status {
+ OutputStatus::Unspent => Some(utxo.status),
+ // If not unspent, don't allow status to be changed.
+ // EDGE-CASE: scanning picks up a local UTXO that we know was spent
+ _ => None,
+ };
let address = utxo.to_utxo_address();
- tx.stealth_outputs_update(&address, Some(utxo.is_burnt), Some(utxo.status), Some(utxo.is_frozen))
+ tx.stealth_outputs_update(&address, Some(utxo.is_burnt), new_status, Some(utxo.is_frozen))
} else {
tx.stealth_outputs_insert(utxo)
}
diff --git a/crates/wallet/sdk/src/apis/stealth_transfer.rs b/crates/wallet/sdk/src/apis/stealth_transfer.rs
index cd5bca3db4..ef5538a0c2 100644
--- a/crates/wallet/sdk/src/apis/stealth_transfer.rs
+++ b/crates/wallet/sdk/src/apis/stealth_transfer.rs
@@ -28,6 +28,7 @@ use tari_template_lib::{
types::Amount,
};
use tari_transaction::{args, Transaction, UnsignedTransaction};
+use tokio::sync::Semaphore;
use crate::{
apis::{
@@ -39,7 +40,16 @@ use crate::{
stealth_outputs::{StealthOutputsApi, StealthOutputsApiError, TransferStatementParams},
substate::{SubstateApiError, SubstatesApi, ValidatorScanResult},
},
- models::{AccountWithAddress, InputSpendData, KeyBranch, KeyId, OutputStatus, StealthOutputModel, WalletLockId},
+ models::{
+ AccountWithAddress,
+ InputSpendData,
+ KeyBranch,
+ KeyId,
+ OutputStatus,
+ StealthOutputModel,
+ WalletLockId,
+ WalletPublicKey,
+ },
network::WalletNetworkInterface,
storage::{WalletStorageError, WalletStore},
};
@@ -52,6 +62,7 @@ pub struct StealthTransferApi<'a, TStore, TNetworkInterface> {
substate_api: SubstatesApi<'a, TStore, TNetworkInterface>,
key_manager_api: KeyManagerApi<'a, TStore>,
config_api: ConfigApi<'a, TStore>,
+ semaphore: Semaphore,
}
impl<'a, TStore, TNetworkInterface> StealthTransferApi<'a, TStore, TNetworkInterface>
@@ -73,24 +84,10 @@ where
substate_api,
key_manager_api,
config_api,
+ semaphore: Semaphore::new(1),
}
}
- fn lock_fee_inputs(
- &self,
- lock_id: WalletLockId,
- owner_account: &AccountWithAddress,
- params: &StealthTransferParams,
- ) -> Result {
- self.lock_inputs_for_transfer(
- lock_id,
- owner_account.account().component_address(),
- XTR,
- params.max_fee.into(),
- params.input_selection,
- )
- }
-
#[allow(clippy::too_many_lines)]
pub fn lock_inputs_for_transfer(
&self,
@@ -275,6 +272,22 @@ where
}
}
+ fn lock_fee_inputs>(
+ &self,
+ lock_id: WalletLockId,
+ owner_account: &AccountWithAddress,
+ max_fee: A,
+ input_selection: ConfidentialTransferInputSelection,
+ ) -> Result {
+ self.lock_inputs_for_transfer(
+ lock_id,
+ owner_account.account().component_address(),
+ XTR,
+ max_fee.into(),
+ input_selection,
+ )
+ }
+
#[allow(clippy::too_many_lines)]
pub async fn transfer(
&self,
@@ -335,7 +348,7 @@ where
},
None => {
// TODO: we're just determining if the account exists - symptom of a larger problem/missing
- // feature where account is created as needed by the execution layer instead of having to be
+ // feature: the account should be created as needed by the execution layer, instead of having to be
// determined by the client side
let to_account_substate = self
.substate_api
@@ -406,21 +419,15 @@ where
.try_from_byte_type()
.expect("already validated");
+ // Critical section
+ let _permit = self.semaphore.acquire().await.expect("semaphore is never closed");
+
let lock_id = self.outputs_api.create_lock()?;
// Lock up funds for fees and transfer
- let fee_inputs_to_spend =
- self.unlock_on_failure(lock_id, self.lock_fee_inputs(lock_id, &owner_account, ¶ms))?;
-
- let inputs_to_spend = self.unlock_on_failure(
+ let fee_inputs_to_spend = self.unlock_on_failure(
lock_id,
- self.lock_inputs_for_transfer(
- lock_id,
- owner_account.account().component_address(),
- params.resource_address,
- params.total_output_amount(),
- params.input_selection,
- ),
+ self.lock_fee_inputs(lock_id, &owner_account, params.max_fee, params.input_selection),
)?;
// TODO: use single db transaction across calls
@@ -440,8 +447,7 @@ where
// Figure out which signing key to use - if there are no revealed funds, which necessitate using a account
// withdraw auth signature, then we can use a nonce key.
- let must_sign_with_account_key =
- fee_inputs_to_spend.revealed.is_positive() || inputs_to_spend.revealed.is_positive();
+ let must_sign_with_account_key = fee_inputs_to_spend.revealed.is_positive();
let (signing_key_branch, signing_key_id) = if must_sign_with_account_key {
(KeyBranch::Account, owner_key_id)
} else {
@@ -453,6 +459,7 @@ where
.key_manager_api
.get_public_key(signing_key_branch, signing_key_id)?;
let required_signer_pk = required_signer.public_key.to_byte_type();
+ let fee_signer = required_signer;
// Generate fee transfer statement
let fee_transfer_statement = self.unlock_on_failure(
@@ -473,12 +480,19 @@ where
// Add the unconfirmed fee change output to the wallet store
if let Some(output) = fee_transfer_statement.outputs_statement.outputs.first() {
+ debug!(
+ target: LOG_TARGET,
+ "Adding FEE unconfirmed output with commitment {} for amount {} to account {}",
+ output.output.commitment,
+ fee_stealth_change_amt,
+ owner_account.component_address()
+ );
self.unlock_on_failure(
lock_id,
self.add_unconfirmed_output_from_statement(
lock_id,
&owner_account,
- params.resource_address,
+ XTR,
output,
fee_stealth_change_amt,
None,
@@ -486,6 +500,38 @@ where
)?;
}
+ // NOTE: important to add this after we add the fee change, because this allows us to spend the fee change
+ // UTXO (XTR case)
+ let inputs_to_spend = self.unlock_on_failure(
+ lock_id,
+ self.lock_inputs_for_transfer(
+ lock_id,
+ owner_account.account().component_address(),
+ params.resource_address,
+ params.total_output_amount(),
+ params.input_selection,
+ ),
+ )?;
+
+ // Signing key for main transfer intent
+ let must_sign_with_account_key = inputs_to_spend.revealed.is_positive();
+ let (signing_key_branch, signing_key_id) = if must_sign_with_account_key {
+ (KeyBranch::Account, owner_key_id)
+ } else {
+ let next_index =
+ self.unlock_on_failure(lock_id, self.key_manager_api.next_derived_key_index(KeyBranch::Nonce))?;
+ (KeyBranch::Nonce, KeyId::derived(next_index))
+ };
+ let main_signer = if signing_key_branch == fee_signer.branch && signing_key_id == fee_signer.key_id {
+ None
+ } else {
+ let required_signer = self
+ .key_manager_api
+ .get_public_key(signing_key_branch, signing_key_id)?;
+ Some(required_signer)
+ };
+ let required_signer_pk = main_signer.as_ref().unwrap_or(&fee_signer).public_key().to_byte_type();
+
// If we're spending from the owner account, add the inputs
if inputs_to_spend.revealed.is_positive() || fee_inputs_to_spend.revealed.is_positive() {
substate_inputs.push(SubstateRequirement::unversioned(*owner_account.component_address()));
@@ -553,12 +599,43 @@ where
}),
)?;
+ // Add the unconfirmed change output to the wallet store
+ // NOTE: we can get the nth element because outputs are guaranteed to be in the order we pass them to
+ // generate_transfer_statement
+ let index = if params.blinded_output_amount.is_positive() {
+ // Change output is second element
+ 1
+ } else {
+ // otherwise, it's the first element
+ 0
+ };
+ if let Some(output) = transfer_statement.outputs_statement.outputs.get(index) {
+ debug!(
+ target: LOG_TARGET,
+ "Adding TRANSFER unconfirmed output with commitment {} for amount {} to account {}",
+ output.output.commitment,
+ change_amount,
+ owner_account.component_address()
+ );
+ self.unlock_on_failure(
+ lock_id,
+ self.add_unconfirmed_output_from_statement(
+ lock_id,
+ &owner_account,
+ params.resource_address,
+ output,
+ change_amount,
+ None,
+ ),
+ )?;
+ }
+
// Add all input UTXO substates to transaction inputs
substate_inputs.extend(
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
+ // 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.commitment)
.map(|commitment| UtxoAddress::new(XTR, (*commitment).into()))
@@ -592,12 +669,12 @@ where
lock_id,
fee_inputs: fee_inputs_to_spend,
transfer_inputs: inputs_to_spend,
- signing_key_branch,
- signing_key_id,
+ additional_signer: main_signer,
+ main_signer: fee_signer,
})
}
- fn unlock_on_failure(&self, lock_id: WalletLockId, result: Result) -> Result {
+ pub fn unlock_on_failure(&self, lock_id: WalletLockId, result: Result) -> Result {
match result {
Ok(value) => Ok(value),
Err(e) => {
@@ -716,8 +793,8 @@ pub struct TransferOutput {
pub lock_id: WalletLockId,
pub fee_inputs: InputsToSpend,
pub transfer_inputs: InputsToSpend,
- pub signing_key_branch: KeyBranch,
- pub signing_key_id: KeyId,
+ pub additional_signer: Option,
+ pub main_signer: WalletPublicKey,
}
#[derive(Debug)]
diff --git a/crates/wallet/sdk/src/apis/transaction.rs b/crates/wallet/sdk/src/apis/transaction.rs
index e0ccb34069..fa5a28a48f 100644
--- a/crates/wallet/sdk/src/apis/transaction.rs
+++ b/crates/wallet/sdk/src/apis/transaction.rs
@@ -19,9 +19,9 @@ use tari_template_lib::{
use tari_transaction::{Transaction, TransactionId};
use crate::{
- models::{NewAccountData, TransactionStatus, WalletTransaction, WalletTransactionUpdate},
+ models::{NewAccountData, TransactionStatus, WalletLockId, WalletTransaction, WalletTransactionUpdate},
network::{StatusResponseError, TransactionFinalizedResult, WalletNetworkInterface, WalletQueryErrorStatus},
- storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter},
+ storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore},
};
const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::transaction";
@@ -89,21 +89,21 @@ where
)
})?;
},
- Err(err) => match err.get_status() {
- WalletQueryErrorStatus::TransactionRejected { message } => {
- warn!(target: LOG_TARGET, "Invalid transaction submission: {transaction_id} {message}");
- self.store.with_write_tx(|tx| {
- tx.transactions_update(
- WalletTransactionUpdate::new(transaction_id)
- .with_new_status(TransactionStatus::InvalidTransaction)
- .with_invalid_reason(&message),
- )
- })?;
- return Ok(false);
- },
- _ => {
- return Err(err.into());
- },
+ Err(err) => {
+ return match err.get_status() {
+ WalletQueryErrorStatus::TransactionRejected { message } => {
+ warn!(target: LOG_TARGET, "Invalid transaction submission: {transaction_id} {message}");
+ self.store.with_write_tx(|tx| {
+ tx.transactions_update(
+ WalletTransactionUpdate::new(transaction_id)
+ .with_new_status(TransactionStatus::InvalidTransaction)
+ .with_invalid_reason(&message),
+ )
+ })?;
+ Ok(false)
+ },
+ _ => Err(err.into()),
+ }
},
}
@@ -256,27 +256,25 @@ where
.with_finalized_time(finalized_time),
)?;
- // if the transaction being processed is confidential,
- // we should make sure that the account's locked outputs
- // are either set to spent or released, depending if the
+
+ // Make sure that any locked outputs are either set to spent or released, depending on if the
// transaction was finalized or rejected. Always release for dry runs.
- if transaction.is_dry_run ||
- !matches!(
- new_status,
- TransactionStatus::Accepted | TransactionStatus::OnlyFeeAccepted
- )
- {
+ if transaction.is_dry_run {
self.release_all_locks_for_transaction_internal(tx, transaction_id)?;
} else {
- // TODO: it becomes more complicated if the transaction is Fee accepted, we'll need to finalize
- // spends relating to fees and release the rest
- let lock_ids = tx.locks_get_by_transaction_id(transaction_id)?;
- info!(target: LOG_TARGET, "Finalizing locked outputs for transaction {}: {:?}", transaction_id, lock_ids);
- for lock_id in lock_ids {
- tx.confidential_outputs_finalize_by_lock_id(lock_id)?;
- tx.stealth_outputs_finalize_by_lock_id(lock_id)?;
- tx.vaults_finalized_locked_revealed_funds(lock_id).optional()?;
- tx.locks_delete(lock_id)?;
+ let maybe_diff = execution_result
+ .as_ref()
+ .and_then(|e| e.finalize.result.any_accept());
+ match maybe_diff {
+ Some(diff) => {
+ if let Some(lock_id) = tx.locks_get_by_transaction_id(transaction_id).optional()? {
+ info!(target: LOG_TARGET, "Finalizing locked outputs for transaction {}: {}", transaction_id, lock_id);
+ tx.locks_unlock_finalized(lock_id, diff)?;
+ }
+ }
+ None => {
+ self.release_all_locks_for_transaction_internal(tx, transaction_id)?;
+ }
}
}
@@ -294,21 +292,24 @@ where
.with_write_tx(|tx| self.release_all_locks_for_transaction_internal(tx, transaction_id))
}
+ pub fn locks_set_transaction_id(
+ &self,
+ lock_id: WalletLockId,
+ transaction_id: TransactionId,
+ ) -> Result<(), TransactionApiError> {
+ self.store
+ .with_write_tx(|tx| tx.locks_link_transaction(lock_id, transaction_id))?;
+ Ok(())
+ }
+
fn release_all_locks_for_transaction_internal(
&self,
- tx: &mut ::WriteTransaction<'_>,
+ tx: &mut ::WriteTransaction<'_>,
transaction_id: TransactionId,
) -> Result<(), TransactionApiError> {
- let lock_ids = tx.locks_get_by_transaction_id(transaction_id)?;
-
- debug!(target: LOG_TARGET, "Releasing {} locks (and associated outputs) for transaction {} that was not committed", lock_ids.len(), transaction_id);
- for lock_id in lock_ids {
- // Lock could be for confidential outputs or stealth outputs
- tx.confidential_outputs_release_by_lock_id(lock_id)?;
- tx.stealth_outputs_release_by_lock_id(lock_id)?;
- // If the lock locks a vault, we need to release the revealed funds
- tx.vaults_release_lock_revealed_funds(lock_id).optional()?;
- tx.locks_delete(lock_id)?;
+ if let Some(lock_id) = tx.locks_get_by_transaction_id(transaction_id).optional()? {
+ debug!(target: LOG_TARGET, "Releasing lock {} (and associated outputs) for transaction {} that was not committed", lock_id, transaction_id);
+ tx.locks_release(lock_id)?;
}
Ok(())
@@ -352,7 +353,7 @@ where
)?;
for owned_id in indexed.referenced_substates() {
- if let Some(pos) = other_substates.iter().position(|(addr, _)| addr == &owned_id) {
+ if let Some(pos) = other_substates.iter().position(|(addr, _)| *addr == owned_id) {
let (_, child) = other_substates.swap_remove(pos);
// If there was a previous parent for this substate, we keep it as is.
let parent = downed_substates_with_parents
diff --git a/crates/wallet/sdk/src/models/key.rs b/crates/wallet/sdk/src/models/key.rs
index 174186fa46..6c5ff07a29 100644
--- a/crates/wallet/sdk/src/models/key.rs
+++ b/crates/wallet/sdk/src/models/key.rs
@@ -58,6 +58,7 @@ pub struct WalletKeyRecord {
pub(crate) key_id: KeyId,
pub(crate) public_key: RistrettoPublicKey,
pub(crate) secret_key: RistrettoSecretKey,
+ pub(crate) branch: KeyBranch,
pub(crate) is_active: bool,
}
@@ -73,6 +74,10 @@ impl WalletKeyRecord {
pub fn public_key(&self) -> &RistrettoPublicKey {
&self.public_key
}
+
+ pub fn branch(&self) -> KeyBranch {
+ self.branch
+ }
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
@@ -101,6 +106,7 @@ impl ImportedWalletKey {
#[derive(Clone)]
pub struct DerivedWalletKey {
pub key: RistrettoSecretKey,
+ pub branch: KeyBranch,
pub key_index: DerivedKeyIndex,
}
@@ -114,18 +120,10 @@ impl DerivedWalletKey {
}
}
-impl From for DerivedWalletKey {
- fn from(key: tari_transaction_components::key_manager::tari_key_manager::DerivedKey) -> Self {
- Self {
- key: key.key,
- key_index: key.key_index,
- }
- }
-}
-
#[derive(Clone)]
pub struct WalletPublicKey {
pub public_key: RistrettoPublicKey,
+ pub branch: KeyBranch,
pub key_id: KeyId,
}
@@ -143,6 +141,7 @@ impl From for WalletPublicKey {
fn from(derived: DerivedWalletKey) -> Self {
Self {
key_id: derived.as_key_id(),
+ branch: derived.branch,
public_key: derived.to_public_key(),
}
}
diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs
index 074b904a99..15bce85617 100644
--- a/crates/wallet/sdk/src/sdk.rs
+++ b/crates/wallet/sdk/src/sdk.rs
@@ -95,6 +95,45 @@ where
})
}
+ // pub fn create_read_context(&self) -> Result, WalletSdkError> {
+ // let read_tx = self.store.create_read_tx()?;
+ // Ok(SdkReadContext::new(read_tx))
+ // }
+ //
+ // pub fn with_read_context(&self, f: F) -> Result
+ // where
+ // F: FnOnce(&mut SdkReadContext) -> Result,
+ // E: From,
+ // {
+ // let mut ctx = self.create_read_context()?;
+ // let ret = f(&mut ctx)?;
+ // Ok(ret)
+ // }
+ //
+ // pub fn create_write_context(&self) -> Result, WalletSdkError> {
+ // let write_tx = self.store.create_write_tx()?;
+ // Ok(SdkWriteContext::new(write_tx))
+ // }
+ //
+ // pub fn with_write_context(&self, f: F) -> Result
+ // where
+ // F: FnOnce(&mut SdkWriteContext) -> Result,
+ // E: From,
+ // {
+ // let mut ctx = self.create_write_context()?;
+ // match f(&mut ctx) {
+ // Ok(r) => {
+ // ctx.commit()?;
+ // Ok(r)
+ // },
+ // Err(e) => {
+ // warn!(target: LOG_TARGET, "Transaction failed! rollback");
+ // ctx.rollback()?;
+ // Err(e.into())
+ // },
+ // }
+ // }
+
pub fn get_store_network(store: &TStore) -> Result