diff --git a/Cargo.lock b/Cargo.lock index 80db10e588..5cc6907940 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,7 +1840,7 @@ dependencies = [ [[package]] name = "consensus_tests" -version = "0.13.1" +version = "0.14.0" dependencies = [ "fern", "futures 0.3.31", @@ -2607,7 +2607,7 @@ dependencies = [ [[package]] name = "db_inspector" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -3746,7 +3746,7 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generate_ristretto_value_lookup" -version = "0.13.1" +version = "0.14.0" dependencies = [ "clap 3.2.25", "human_bytes", @@ -4993,7 +4993,7 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "integration_tests" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "config", @@ -5761,7 +5761,7 @@ dependencies = [ [[package]] name = "libp2p-messaging" -version = "0.13.1" +version = "0.14.0" dependencies = [ "async-trait", "futures-bounded", @@ -5940,7 +5940,7 @@ dependencies = [ [[package]] name = "libp2p-substream" -version = "0.13.1" +version = "0.14.0" dependencies = [ "libp2p", "prometheus-client", @@ -8466,7 +8466,7 @@ dependencies = [ [[package]] name = "proto_builder" -version = "0.13.1" +version = "0.14.0" dependencies = [ "prost-build 0.14.1", "sha2", @@ -10300,7 +10300,7 @@ dependencies = [ [[package]] name = "sqlite_message_logger" -version = "0.13.1" +version = "0.14.0" dependencies = [ "chrono", "diesel", @@ -10341,7 +10341,7 @@ checksum = "e7386b49cb287f6fafbfd3bd604914bccb99fb8d53483f40e1ecfda5d45f3370" [[package]] name = "state_store_tests" -version = "0.13.1" +version = "0.14.0" dependencies = [ "env_logger 0.11.8", "indexmap 2.11.4", @@ -10730,7 +10730,7 @@ dependencies = [ [[package]] name = "tari_base_node_client" -version = "0.13.1" +version = "0.14.0" dependencies = [ "log", "minotari_app_grpc", @@ -10954,7 +10954,7 @@ dependencies = [ [[package]] name = "tari_consensus" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "indexmap 2.11.4", @@ -10978,7 +10978,7 @@ dependencies = [ [[package]] name = "tari_consensus_types" -version = "0.13.1" +version = "0.14.0" dependencies = [ "borsh", "serde", @@ -11080,7 +11080,7 @@ dependencies = [ [[package]] name = "tari_engine" -version = "0.13.1" +version = "0.14.0" dependencies = [ "blake2", "cargo_toml 0.22.3", @@ -11109,7 +11109,7 @@ dependencies = [ [[package]] name = "tari_engine_types" -version = "0.13.1" +version = "0.14.0" dependencies = [ "base64 0.21.7", "bincode 2.0.1", @@ -11136,7 +11136,7 @@ dependencies = [ [[package]] name = "tari_epoch_manager" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "log", @@ -11158,7 +11158,7 @@ dependencies = [ [[package]] name = "tari_epoch_oracles" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "blake2", @@ -11199,7 +11199,7 @@ dependencies = [ [[package]] name = "tari_indexer" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "async-graphql", @@ -11263,7 +11263,7 @@ dependencies = [ [[package]] name = "tari_indexer_client" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "bounded-vec", @@ -11293,7 +11293,7 @@ dependencies = [ [[package]] name = "tari_indexer_lib" -version = "0.13.1" +version = "0.14.0" dependencies = [ "log", "serde", @@ -11369,7 +11369,7 @@ dependencies = [ [[package]] name = "tari_networking" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "async-trait", @@ -11406,7 +11406,7 @@ dependencies = [ [[package]] name = "tari_ootle_address" -version = "0.13.1" +version = "0.14.0" dependencies = [ "bech32", "bincode 2.0.1", @@ -11422,7 +11422,7 @@ dependencies = [ [[package]] name = "tari_ootle_app_utilities" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -11461,7 +11461,7 @@ dependencies = [ [[package]] name = "tari_ootle_common_types" -version = "0.13.1" +version = "0.14.0" dependencies = [ "blake2", "borsh", @@ -11489,7 +11489,7 @@ dependencies = [ [[package]] name = "tari_ootle_p2p" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "prost 0.14.1", @@ -11511,7 +11511,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "bitflags 2.9.2", @@ -11536,7 +11536,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage_sqlite" -version = "0.13.1" +version = "0.14.0" dependencies = [ "diesel", "diesel_migrations", @@ -11556,7 +11556,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_cli" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -11585,7 +11585,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_crypto" -version = "0.13.1" +version = "0.14.0" dependencies = [ "argon2 0.5.3", "blake2", @@ -11609,7 +11609,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "blake2", @@ -11644,7 +11644,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk_services" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "futures 0.3.31", @@ -11669,7 +11669,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_storage_sqlite" -version = "0.13.1" +version = "0.14.0" dependencies = [ "bigdecimal", "diesel", @@ -11693,7 +11693,7 @@ dependencies = [ [[package]] name = "tari_ootle_walletd" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "async-trait", @@ -11778,7 +11778,7 @@ dependencies = [ [[package]] name = "tari_rpc_framework" -version = "0.13.1" +version = "0.14.0" dependencies = [ "async-trait", "bitflags 2.9.2", @@ -11802,7 +11802,7 @@ dependencies = [ [[package]] name = "tari_rpc_macros" -version = "0.13.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -11811,7 +11811,7 @@ dependencies = [ [[package]] name = "tari_rpc_state_sync" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "futures 0.3.31", @@ -11831,7 +11831,7 @@ dependencies = [ [[package]] name = "tari_scaffolder" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -11903,7 +11903,7 @@ dependencies = [ [[package]] name = "tari_signaling_server" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -11929,7 +11929,7 @@ dependencies = [ [[package]] name = "tari_state_store_rocksdb" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -11955,7 +11955,7 @@ dependencies = [ [[package]] name = "tari_state_tree" -version = "0.13.1" +version = "0.14.0" dependencies = [ "indexmap 2.11.4", "log", @@ -11982,7 +11982,7 @@ dependencies = [ [[package]] name = "tari_swarm" -version = "0.13.1" +version = "0.14.0" dependencies = [ "libp2p", "libp2p-messaging", @@ -11992,7 +11992,7 @@ dependencies = [ [[package]] name = "tari_swarm_daemon" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "async-trait", @@ -12048,7 +12048,7 @@ dependencies = [ [[package]] name = "tari_template_builtin" -version = "0.13.1" +version = "0.14.0" dependencies = [ "tari_engine_types", "tari_template_lib", @@ -12100,7 +12100,7 @@ dependencies = [ [[package]] name = "tari_template_manager" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "bytes 1.10.1", @@ -12166,7 +12166,7 @@ dependencies = [ [[package]] name = "tari_transaction" -version = "0.13.1" +version = "0.14.0" dependencies = [ "borsh", "hex", @@ -12254,7 +12254,7 @@ dependencies = [ [[package]] name = "tari_transaction_manifest" -version = "0.13.1" +version = "0.14.0" dependencies = [ "proc-macro2", "serde_json", @@ -12288,7 +12288,7 @@ dependencies = [ [[package]] name = "tari_validator_node" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -12350,7 +12350,7 @@ dependencies = [ [[package]] name = "tari_validator_node_cli" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -12378,7 +12378,7 @@ dependencies = [ [[package]] name = "tari_validator_node_client" -version = "0.13.1" +version = "0.14.0" dependencies = [ "indexmap 2.11.4", "multiaddr 0.18.1", @@ -12401,7 +12401,7 @@ dependencies = [ [[package]] name = "tari_validator_node_rpc" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "prost 0.14.1", @@ -12423,7 +12423,7 @@ dependencies = [ [[package]] name = "tari_wallet_daemon_client" -version = "0.13.1" +version = "0.14.0" dependencies = [ "reqwest 0.11.27", "serde", @@ -12444,7 +12444,7 @@ dependencies = [ [[package]] name = "tari_watcher" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -12472,7 +12472,7 @@ dependencies = [ [[package]] name = "tariswap_bench" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "clap 4.5.48", @@ -13131,7 +13131,7 @@ dependencies = [ [[package]] name = "transaction_generator" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -13151,7 +13151,7 @@ dependencies = [ [[package]] name = "transaction_submitter" -version = "0.13.1" +version = "0.14.0" dependencies = [ "anyhow", "clap 4.5.48", diff --git a/Cargo.toml b/Cargo.toml index 429f9671a0..7cf278eebd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ # NOTE: When editing this version, also edit the versions in template_built_in/templates/account and account_nft [workspace.package] -version = "0.13.1" +version = "0.14.0" edition = "2021" authors = ["The Tari Development Community"] repository = "https://github.com/tari-project/tari-ootle" diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index a1cf2495c9..1429d05421 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -26,7 +26,7 @@ use tari_ootle_wallet_crypto::{ use tari_ootle_wallet_sdk::{ apis::{ confidential_transfer::ConfidentialTransferParams, - stealth_transfer::StealthTransferParams, + stealth_transfer::{StealthTransferParams, TransferOutput}, substate::ValidatorScanResult, }, models::{KeyBranch, NewAccountData}, @@ -970,12 +970,18 @@ pub async fn handle_stealth_transfer( let params = StealthTransferParams { input_selection: req.input_selection, - destination_address: req.destination_address, resource_address: req.resource_address, max_fee: req.max_fee, - blinded_output_amount: req.blinded_output_amount, - revealed_output_amount: req.revealed_output_amount, - output_memo: req.output_memo, + outputs: req + .transfers + .into_iter() + .map(|transfer| TransferOutput { + address: transfer.destination_address, + blinded_amount: transfer.blinded_output_amount, + revealed_amount: transfer.revealed_output_amount, + memo: transfer.output_memo, + }) + .collect(), is_dry_run: req.dry_run, }; if let Err(err) = params.validate(network) { diff --git a/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts b/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts index 12e3bc9b7e..723aea897d 100644 --- a/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts +++ b/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts @@ -154,11 +154,15 @@ export const useAccountsTransfer = () => { owner_account: account, input_selection: params.input_selection, resource_address: params.resource_address, - destination_address: params.destination_address, + transfers: [ + { + destination_address: params.destination_address, + blinded_output_amount: params.output_to_revealed ? 0 : params.amount, + revealed_output_amount: params.output_to_revealed ? params.amount : 0, + output_memo: params.output_memo || null, + }, + ], max_fee, - blinded_output_amount: params.output_to_revealed ? 0 : params.amount, - revealed_output_amount: params.output_to_revealed ? params.amount : 0, - output_memo: params.output_memo || null, dry_run: params.dry_run, }; return accountsStealthTransfer(transferRequest); diff --git a/bindings/package.json b/bindings/package.json index a1abf221ae..54d8d29e69 100644 --- a/bindings/package.json +++ b/bindings/package.json @@ -1,6 +1,6 @@ { "name": "@tari-project/typescript-bindings", - "version": "1.18.2", + "version": "1.19.0", "description": "TypeScript types synchronized to the Tari Ootle Rust codebase", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { diff --git a/bindings/src/types/Instruction.ts b/bindings/src/types/Instruction.ts index f047039b54..6ac4ff3457 100644 --- a/bindings/src/types/Instruction.ts +++ b/bindings/src/types/Instruction.ts @@ -29,6 +29,7 @@ export type Instruction = | { ClaimValidatorFees: { address: string } } | "DropAllProofsInWorkspace" | { AssertBucketContains: { key: WorkspaceOffsetId; resource_address: ResourceAddress; min_amount: Amount } } + | { TakeFromBucket: { input_bucket: WorkspaceOffsetId; amount: Amount; output_bucket: number } } | { PublishTemplate: { binary: Array } } | { AllocateAddress: { allocatable_type: AllocatableAddressType; workspace_id: number } } | { diff --git a/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts b/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts index cd3d5752fb..99db681649 100644 --- a/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts +++ b/bindings/src/types/tari-indexer-client/IndexerGetIdentityResponse.ts @@ -4,5 +4,5 @@ import type { RistrettoPublicKeyBytes } from "../RistrettoPublicKeyBytes"; export type IndexerGetIdentityResponse = { peer_id: string; public_key: RistrettoPublicKeyBytes; - public_addresses: string[]; + public_addresses: Array; }; diff --git a/bindings/src/types/wallet-daemon-client/StealthTransfer.ts b/bindings/src/types/wallet-daemon-client/StealthTransfer.ts new file mode 100644 index 0000000000..47b1e92262 --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/StealthTransfer.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Amount } from "../Amount"; +import type { Memo } from "../Memo"; +import type { OotleAddress } from "../OotleAddress"; + +export type StealthTransfer = { + destination_address: OotleAddress; + blinded_output_amount: Amount; + revealed_output_amount: Amount; + output_memo?: Memo | null; +}; diff --git a/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts b/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts index 12c7197436..5860472d00 100644 --- a/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts +++ b/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts @@ -1,19 +1,14 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Amount } from "../Amount"; import type { ConfidentialTransferInputSelection } from "../ConfidentialTransferInputSelection"; -import type { Memo } from "../Memo"; -import type { OotleAddress } from "../OotleAddress"; import type { ResourceAddress } from "../ResourceAddress"; import type { ComponentAddressOrName } from "./ComponentAddressOrName"; +import type { StealthTransfer } from "./StealthTransfer"; export type StealthTransferRequest = { owner_account: ComponentAddressOrName; input_selection: ConfidentialTransferInputSelection; resource_address: ResourceAddress; - destination_address: OotleAddress; + transfers: Array; max_fee: number; - blinded_output_amount: Amount; - revealed_output_amount: Amount; - output_memo?: Memo | null; dry_run: boolean; }; diff --git a/bindings/src/wallet-daemon-client.ts b/bindings/src/wallet-daemon-client.ts index f7b3751ef7..d9b3f1ddf4 100644 --- a/bindings/src/wallet-daemon-client.ts +++ b/bindings/src/wallet-daemon-client.ts @@ -32,6 +32,7 @@ export * from "./types/wallet-daemon-client/MintFaucetNftRequest"; export * from "./types/wallet-daemon-client/AuthLoginDenyRequest"; export * from "./types/wallet-daemon-client/PublishTemplateRequest"; export * from "./types/wallet-daemon-client/TransactionSubmitResponse"; +export * from "./types/wallet-daemon-client/StealthTransfer"; export * from "./types/wallet-daemon-client/ProofsGenerateResponse"; export * from "./types/wallet-daemon-client/TransactionSubmitManifestResponse"; export * from "./types/wallet-daemon-client/ListNftsResponse"; diff --git a/clients/javascript/wallet_daemon_client/package.json b/clients/javascript/wallet_daemon_client/package.json index 32583197fb..d064f87fb5 100644 --- a/clients/javascript/wallet_daemon_client/package.json +++ b/clients/javascript/wallet_daemon_client/package.json @@ -1,6 +1,6 @@ { "name": "@tari-project/wallet_jrpc_client", - "version": "1.9.3", + "version": "1.10.0", "description": "Tari wallet JSON-RPC client library", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index 5a94a3afbd..174866a52c 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -1061,14 +1061,20 @@ pub struct StealthTransferRequest { pub owner_account: ComponentAddressOrName, pub input_selection: ConfidentialTransferInputSelection, pub resource_address: ResourceAddress, - pub destination_address: OotleAddress, + pub transfers: Vec, #[cfg_attr(feature = "ts", ts(type = "number"))] pub max_fee: u64, + pub dry_run: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct StealthTransfer { + pub destination_address: OotleAddress, pub blinded_output_amount: Amount, pub revealed_output_amount: Amount, #[serde(default, skip_serializing_if = "Option::is_none")] pub output_memo: Option, - pub dry_run: bool, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/crates/engine/src/runtime/impl.rs b/crates/engine/src/runtime/impl.rs index 30bdad6a72..1e9bccab71 100644 --- a/crates/engine/src/runtime/impl.rs +++ b/crates/engine/src/runtime/impl.rs @@ -1950,7 +1950,7 @@ impl> RuntimeInte self.tracker.write_with(|state| { let bucket = state.get_bucket_mut(bucket_id)?; let resource = bucket.take(amount)?; - let bucket_id = state.id_provider()?.new_bucket_id(); + let bucket_id = state.new_bucket_id(); state.new_bucket(bucket_id, resource)?; Ok(InvokeResult::encode(&bucket_id)?) }) @@ -2244,7 +2244,7 @@ impl> RuntimeInte debug!(target: LOG_TARGET, "Workspace invoke: {:?}", action,); match action { - // Basically names an output on the workspace so that you can refer to it as an + // Names an output on the workspace so that you can refer to it as an // Arg::Variable WorkspaceAction::PutLastInstructionOutput => { let key = args.assert_one_arg()?; @@ -2694,6 +2694,16 @@ impl> RuntimeInte }) } + fn put_on_workspace(&self, id: WorkspaceId, value: IndexedValue) -> Result<(), RuntimeError> { + self.invoke_modules_on_runtime_call("put_on_workspace")?; + + self.validate_return_value(&value)?; + + self.tracker + .with_workspace_mut(|workspace| workspace.insert(id, value))?; + Ok(()) + } + fn signature_invoke(&self, action: SignatureAction, args: EngineArgs) -> Result { self.invoke_modules_on_runtime_call("signature_invoke")?; diff --git a/crates/engine/src/runtime/mod.rs b/crates/engine/src/runtime/mod.rs index 413a9e0a7f..e41b08f96f 100644 --- a/crates/engine/src/runtime/mod.rs +++ b/crates/engine/src/runtime/mod.rs @@ -189,6 +189,7 @@ pub trait RuntimeInterface: Send + Sync { fn push_call_frame(&self, frame: PushCallFrame) -> Result<(), RuntimeError>; fn pop_call_frame(&self) -> Result<(), RuntimeError>; fn publish_template(&self, template: Vec) -> Result<(), RuntimeError>; + fn put_on_workspace(&self, id: WorkspaceId, value: IndexedValue) -> Result<(), RuntimeError>; fn signature_invoke(&self, action: SignatureAction, args: EngineArgs) -> Result; diff --git a/crates/engine/src/runtime/working_state.rs b/crates/engine/src/runtime/working_state.rs index 89707dc616..d8199b9e5f 100644 --- a/crates/engine/src/runtime/working_state.rs +++ b/crates/engine/src/runtime/working_state.rs @@ -397,9 +397,10 @@ impl WorkingState { } pub(super) fn validate_finalized(&self) -> Result<(), RuntimeError> { - if !self.buckets.is_empty() { + let dangling_bucket_count = self.buckets.iter().filter(|(_, b)| !b.amount().is_zero()).count(); + if dangling_bucket_count > 0 { return Err(TransactionCommitError::DanglingBuckets { - count: self.buckets.len(), + count: dangling_bucket_count, } .into()); } diff --git a/crates/engine/src/transaction/processor.rs b/crates/engine/src/transaction/processor.rs index 350d0774f7..e28119497d 100644 --- a/crates/engine/src/transaction/processor.rs +++ b/crates/engine/src/transaction/processor.rs @@ -37,7 +37,7 @@ use tari_ootle_common_types::services::template_provider::TemplateProvider; use tari_template_abi::{FunctionDef, Type}; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::{ - args::{AllocateAddressResult, WorkspaceAction}, + args::{AllocateAddressResult, BucketAction, BucketRef, WorkspaceAction}, auth::{ComponentAccessRules, OwnerRule}, invoke_args, models::{Bucket, NonFungibleAddress, StealthTransferStatement}, @@ -316,6 +316,24 @@ impl + 'static> T )?; Ok(InstructionResult::empty()) }, + Instruction::TakeFromBucket { + input_bucket, + amount, + output_bucket, + } => { + let item = runtime + .interface() + .workspace_invoke(WorkspaceAction::Get, invoke_args![input_bucket].into())?; + let bucket = runtime.interface().bucket_invoke( + BucketRef::Ref(item.decode()?), + BucketAction::Take, + invoke_args![amount].into(), + )?; + runtime + .interface() + .put_on_workspace(output_bucket, IndexedValue::from_value(bucket.into_value()?)?)?; + Ok(InstructionResult::empty()) + }, Instruction::PublishTemplate { binary } => Self::publish_template(config, runtime, binary), Instruction::AllocateAddress { allocatable_type: substate_type, diff --git a/crates/engine/tests/account.rs b/crates/engine/tests/account.rs index b328111e06..94f17edf7e 100644 --- a/crates/engine/tests/account.rs +++ b/crates/engine/tests/account.rs @@ -277,3 +277,55 @@ fn custom_access_rules() { vec![user2_account_proof], ); } + +#[test] +fn take_from_bucket() { + let mut test = TemplateTest::new(Vec::<&str>::new()); + + let faucet_template = test.get_template_address("TestFaucet"); + let (alice, _proof, _alice_sk) = test.create_empty_account(); + let (bob, _proof, _bob_sk) = test.create_empty_account(); + + let initial_supply = Amount::from(1_000_000_000_000u64); + test.execute_expect_success( + Transaction::builder() + .allocate_component_address("faucet") + .call_function(faucet_template, "mint_with_opts", args![ + initial_supply, + "faucet".to_string(), + Workspace("faucet"), + ]) + .call_method("faucet", "take_free_coins_custom", args![1000]) + .put_last_instruction_output_on_workspace("free_coins") + .take_from_bucket("free_coins", 100, "foo_bucket") + // Take all to test what happens when free_coins is empty (should not fail due to dangling buckets) + .take_from_bucket("free_coins", 900, "bar_bucket") + .call_method(alice, "deposit", args![Workspace("foo_bucket")]) + .call_method(bob, "deposit", args![Workspace("bar_bucket")]) + .build_and_seal(test.secret_key()), + vec![test.owner_proof()], + ); + + let resources = test.read_only_state_store().get_all_resources().unwrap(); + let faucet_resource = resources + .iter() + .find(|(_, r)| r.token_symbol() == Some("faucet")) + .map(|(addr, _)| *addr) + .unwrap(); + + let alice_acc = test.read_only_state_store().get_account(alice).unwrap(); + let bob_acc = test.read_only_state_store().get_account(bob).unwrap(); + let alice_balance = test + .read_only_state_store() + .get_vault(&alice_acc.get_vault_by_resource(&faucet_resource).unwrap().vault_id()) + .unwrap() + .balance(); + let bob_balance = test + .read_only_state_store() + .get_vault(&bob_acc.get_vault_by_resource(&faucet_resource).unwrap().vault_id()) + .unwrap() + .balance(); + + assert_eq!(alice_balance, 100); + assert_eq!(bob_balance, 900); +} diff --git a/crates/engine/tests/test.rs b/crates/engine/tests/test.rs index fe8f14c745..4943d4d5db 100644 --- a/crates/engine/tests/test.rs +++ b/crates/engine/tests/test.rs @@ -96,7 +96,7 @@ fn state_create_multiple_in_one_call() { let mut count = 0usize; template_test .read_only_state_store() - .with_substates(|s| { + .with_substates(|_, s| { if s.substate_value() .component() .filter(|a| a.template_address == template_address) @@ -1141,113 +1141,6 @@ mod tickets { } } -// mod nft_indexes { -// use super::*; -// -// fn setup() -> ( -// TemplateTest, -// (ComponentAddress, NonFungibleAddress), -// ComponentAddress, -// SubstateId, -// ) { -// let mut template_test = TemplateTest::new(vec!["tests/templates/nft/nft_list"]); -// -// let (account_address, owner_token, _) = template_test.create_funded_account(); -// let nft_component: ComponentAddress = template_test.call_function("SparkleNft", "new", args![], vec![]); -// -// let nft_resx = template_test.get_previous_output_address(SubstateType::Resource); -// -// // TODO: cleanup -// (template_test, (account_address, owner_token), nft_component, nft_resx) -// } -// -// #[test] -// #[allow(clippy::too_many_lines)] -// fn new_nft_index() { -// let (mut template_test, (account_address, owner_proof), nft_component, nft_resx) = setup(); -// -// let vars = vec![ -// ("account", account_address.into()), -// ("nft", nft_component.into()), -// ("nft_resx", nft_resx.clone().into()), -// ]; -// -// let total_supply: Amount = -// template_test.call_method(nft_component, "total_supply", args![], vec![owner_proof.clone()]); -// assert_eq!(total_supply, Amount(0)); -// -// let result = template_test -// .execute_and_commit_manifest( -// r#" -// let account = var!["account"]; -// let sparkle_nft = var!["nft"]; -// -// let nft_bucket = sparkle_nft.mint(); -// account.deposit(nft_bucket); -// "#, -// vars.clone(), -// vec![owner_proof.clone()], -// ) -// .unwrap(); -// -// let diff = result.finalize.result.expect("execution failed"); -// -// // Resource is changed -// assert_eq!(diff.down_iter().filter(|(addr, _)| addr.is_resource()).count(), 1); -// assert_eq!(diff.up_iter().filter(|(addr, _)| addr.is_resource()).count(), 1); -// -// // NFT component changed -// assert_eq!(diff.down_iter().filter(|(addr, _)| addr.is_component()).count(), 1); -// assert_eq!(diff.up_iter().filter(|(addr, _)| addr.is_component()).count(), 1); -// -// // One new vault created -// assert_eq!(diff.down_iter().filter(|(addr, _)| addr.is_vault()).count(), 0); -// assert_eq!(diff.up_iter().filter(|(addr, _)| addr.is_vault()).count(), 1); -// -// // One new NFT minted -// assert_eq!(diff.down_iter().filter(|(addr, _)| addr.is_non_fungible()).count(), 0); -// assert_eq!(diff.up_iter().filter(|(addr, _)| addr.is_non_fungible()).count(), 1); -// -// // One new NFT minted -// assert_eq!(diff.down_iter().filter(|(addr, _)| addr.is_non_fungible()).count(), 0); -// assert_eq!(diff.up_iter().filter(|(addr, _)| addr.is_non_fungible()).count(), 1); -// let (nft_addr, _) = diff.up_iter().find(|(addr, _)| addr.is_non_fungible()).unwrap(); -// -// // One new NFT index -// assert_eq!( -// diff.down_iter() -// .filter(|(addr, _)| addr.is_non_fungible_index()) -// .count(), -// 0 -// ); -// assert_eq!( -// diff.up_iter().filter(|(addr, _)| addr.is_non_fungible_index()).count(), -// 1 -// ); -// let (index_addr, index) = diff.up_iter().find(|(addr, _)| addr.is_non_fungible_index()).unwrap(); -// // The nft index address is composed of the resource address -// assert_eq!( -// nft_resx.as_resource_address().unwrap(), -// index_addr -// .as_non_fungible_index_address() -// .unwrap() -// .resource_address() -// .to_owned(), -// ); -// // The index references the newly minted nft -// let referenced_address = index -// .substate_value() -// .non_fungible_index() -// .unwrap() -// .referenced_address(); -// assert_eq!(nft_addr.to_address_string(), referenced_address.to_string()); -// -// // The total supply of the resource is increased -// let total_supply: Amount = template_test.call_method(nft_component, "total_supply", args![], -// vec![owner_proof]); assert_eq!(total_supply, Amount(1)); -// } -// } - #[test] fn test_builtin_templates() { let mut template_test = TemplateTest::new(vec!["tests/templates/builtin_templates"]); diff --git a/crates/p2p/proto/transaction.proto b/crates/p2p/proto/transaction.proto index 33210bba3b..090b49d6ba 100644 --- a/crates/p2p/proto/transaction.proto +++ b/crates/p2p/proto/transaction.proto @@ -32,10 +32,11 @@ message Instruction { bool drop_all_proofs_in_workspace = 7; CreateAccount create_account = 8; AssertBucketContains assert_bucket_contains = 9; - PublishTemplate publish_template = 10; - AllocateAddress allocate_address = 11; - StealthTransfer stealth_transfer = 12; - PayFee pay_fee = 13; + TakeFromBucket take_from_bucket = 10; + PublishTemplate publish_template = 11; + AllocateAddress allocate_address = 12; + StealthTransfer stealth_transfer = 13; + PayFee pay_fee = 14; } } @@ -61,6 +62,12 @@ message ClaimBurn { AbridgedKernel kernel = 7; } +message TakeFromBucket { + WorkspaceOffsetId input_bucket = 1; + tari.ootle.common.Amount amount = 2; + uint32 output_bucket = 3; +} + message AbridgedKernel { uint32 version = 1; uint64 fee = 2; diff --git a/crates/p2p/src/conversions/transaction.rs b/crates/p2p/src/conversions/transaction.rs index 073e043df4..186d489d66 100644 --- a/crates/p2p/src/conversions/transaction.rs +++ b/crates/p2p/src/conversions/transaction.rs @@ -233,6 +233,15 @@ impl TryFrom for Instruction { min_amount: assert_contains.min_amount.unwrap_or_default().into(), }) }, + Some(TakeFromBucket(take_from_bucket)) => Ok(Instruction::TakeFromBucket { + input_bucket: take_from_bucket + .input_bucket + .ok_or_else(|| anyhow!("take_from_bucket_input_bucket not provided"))? + .try_into()?, + amount: take_from_bucket.amount.unwrap_or_default().into(), + output_bucket: u16::try_from(take_from_bucket.output_bucket) + .context("take_from_bucket_output_bucket overflowed")?, + }), Some(PublishTemplate(publish_template)) => Ok(Instruction::PublishTemplate { binary: publish_template.binary, }), @@ -375,6 +384,20 @@ impl From for proto::transaction::Instruction { }, )), }, + + Instruction::TakeFromBucket { + input_bucket, + amount, + output_bucket, + } => proto::transaction::Instruction { + instruction: Some(proto::transaction::instruction::Instruction::TakeFromBucket( + proto::transaction::TakeFromBucket { + input_bucket: Some(input_bucket.into()), + amount: Some(amount.into()), + output_bucket: u32::from(output_bucket), + }, + )), + }, Instruction::PublishTemplate { binary } => proto::transaction::Instruction { instruction: Some(proto::transaction::instruction::Instruction::PublishTemplate( proto::transaction::PublishTemplate { binary }, diff --git a/crates/template_lib/src/component/instance.rs b/crates/template_lib/src/component/instance.rs index 825f1fe377..6020ee1d89 100644 --- a/crates/template_lib/src/component/instance.rs +++ b/crates/template_lib/src/component/instance.rs @@ -31,9 +31,16 @@ impl ComponentBuilder { } /// Use an allocated address for the component. - pub fn with_address_allocation(mut self, allocation: ComponentAddressAllocation) -> Self { - assert!(self.address_allocation.is_none(), "Address allocation already set"); - self.address_allocation = Some(allocation); + pub fn with_address_allocation(self, allocation: ComponentAddressAllocation) -> Self { + self.with_address_allocation_opt(Some(allocation)) + } + + /// Use an allocated address for the component, if `Some`. + pub fn with_address_allocation_opt(mut self, allocation: Option) -> Self { + if let Some(allocation) = allocation { + assert!(self.address_allocation.is_none(), "Address allocation already set"); + self.address_allocation = Some(allocation); + } self } diff --git a/crates/template_lib/src/resource/builder/confidential.rs b/crates/template_lib/src/resource/builder/confidential.rs index e43b646370..f8b0a38bf5 100644 --- a/crates/template_lib/src/resource/builder/confidential.rs +++ b/crates/template_lib/src/resource/builder/confidential.rs @@ -76,8 +76,13 @@ impl ConfidentialResourceBuilder { } /// Sets the already allocated address for the resource - pub fn with_address_allocation(mut self, address: ResourceAddressAllocation) -> Self { - self.address_allocation = Some(address); + pub fn with_address_allocation(self, address: ResourceAddressAllocation) -> Self { + self.with_address_allocation_opt(Some(address)) + } + + /// Sets the already allocated address for the resource, optionally + pub fn with_address_allocation_opt(mut self, address: Option) -> Self { + self.address_allocation = address; self } diff --git a/crates/template_lib/src/resource/builder/fungible.rs b/crates/template_lib/src/resource/builder/fungible.rs index 0995f63b3d..c760b7366d 100644 --- a/crates/template_lib/src/resource/builder/fungible.rs +++ b/crates/template_lib/src/resource/builder/fungible.rs @@ -117,8 +117,13 @@ impl FungibleResourceBuilder { } /// Sets the already allocated address for the resource - pub fn with_address_allocation(mut self, address: ResourceAddressAllocation) -> Self { - self.address_allocation = Some(address); + pub fn with_address_allocation(self, address: ResourceAddressAllocation) -> Self { + self.with_address_allocation_opt(Some(address)) + } + + /// Sets the already allocated address for the resource, optionally + pub fn with_address_allocation_opt(mut self, address: Option) -> Self { + self.address_allocation = address; self } diff --git a/crates/template_lib/src/resource/builder/non_fungible.rs b/crates/template_lib/src/resource/builder/non_fungible.rs index 035e634e35..0f788dbfe0 100644 --- a/crates/template_lib/src/resource/builder/non_fungible.rs +++ b/crates/template_lib/src/resource/builder/non_fungible.rs @@ -74,8 +74,13 @@ impl NonFungibleResourceBuilder { } /// Sets the already allocated address for the resource - pub fn with_address_allocation(mut self, address: ResourceAddressAllocation) -> Self { - self.address_allocation = Some(address); + pub fn with_address_allocation(self, address: ResourceAddressAllocation) -> Self { + self.with_address_allocation_opt(Some(address)) + } + + /// Sets the already allocated address for the resource, optionally + pub fn with_address_allocation_opt(mut self, address: Option) -> Self { + self.address_allocation = address; self } diff --git a/crates/template_lib/src/resource/builder/stealth.rs b/crates/template_lib/src/resource/builder/stealth.rs index ae21f9a380..e8b28200b7 100644 --- a/crates/template_lib/src/resource/builder/stealth.rs +++ b/crates/template_lib/src/resource/builder/stealth.rs @@ -77,8 +77,13 @@ impl StealthResourceBuilder { } /// Sets the already allocated address for the resource - pub fn with_address_allocation(mut self, address: ResourceAddressAllocation) -> Self { - self.address_allocation = Some(address); + pub fn with_address_allocation(self, address: ResourceAddressAllocation) -> Self { + self.with_address_allocation_opt(Some(address)) + } + + /// Sets the already allocated address for the resource, optionally + pub fn with_address_allocation_opt(mut self, address: Option) -> Self { + self.address_allocation = address; self } diff --git a/crates/template_test_tooling/src/read_only_state_store.rs b/crates/template_test_tooling/src/read_only_state_store.rs index 853b397259..1a32baedf8 100644 --- a/crates/template_test_tooling/src/read_only_state_store.rs +++ b/crates/template_test_tooling/src/read_only_state_store.rs @@ -51,6 +51,17 @@ impl<'a> ReadOnlyStateStore<'a> { Ok(substate.into_substate_value().into_resource().unwrap()) } + pub fn get_all_resources(&self) -> Result, StateStoreError> { + let mut resources = HashMap::new(); + self.with_substates(|id, substate| { + if let SubstateId::Resource(resource_address) = id { + let resource = substate.substate_value().as_resource().unwrap(); + resources.insert(*resource_address, resource.clone()); + } + })?; + Ok(resources) + } + pub fn get_vault(&self, vault_id: &VaultId) -> Result { let substate = self.get_substate(&SubstateId::Vault(*vault_id))?; Ok(substate.into_substate_value().into_vault().unwrap()) @@ -77,8 +88,8 @@ impl<'a> ReadOnlyStateStore<'a> { } pub fn with_substates(&self, mut f: F) -> Result<(), StateStoreError> - where F: FnMut(Substate) { - self.store.iter().for_each(|(_, substate)| f(substate.clone())); + where F: FnMut(&SubstateId, &Substate) { + self.store.iter().for_each(|(id, substate)| f(id, substate)); Ok(()) } } diff --git a/crates/template_test_tooling/templates/faucet/src/lib.rs b/crates/template_test_tooling/templates/faucet/src/lib.rs index 9289be742d..9e49b7459a 100644 --- a/crates/template_test_tooling/templates/faucet/src/lib.rs +++ b/crates/template_test_tooling/templates/faucet/src/lib.rs @@ -36,6 +36,14 @@ mod faucet_template { } pub fn mint_with_symbol(initial_supply: Amount, symbol: String) -> Component { + Self::mint_with_opts(initial_supply, symbol, None) + } + + pub fn mint_with_opts( + initial_supply: Amount, + symbol: String, + address_alloc: Option, + ) -> Component { let coins = ResourceBuilder::public_fungible() .with_token_symbol(symbol) .initial_supply(initial_supply); @@ -43,6 +51,7 @@ mod faucet_template { Component::new(Self { vault: Vault::from_bucket(coins), }) + .with_address_allocation_opt(address_alloc) .with_access_rules(AccessRules::allow_all()) .create() } diff --git a/crates/transaction/src/builder/mod.rs b/crates/transaction/src/builder/mod.rs index 93983f876b..15d19f84c3 100644 --- a/crates/transaction/src/builder/mod.rs +++ b/crates/transaction/src/builder/mod.rs @@ -256,6 +256,21 @@ impl TransactionBuilder { self.add_instruction(Instruction::PutLastInstructionOutputOnWorkspace { key }) } + pub fn take_from_bucket, A: Into>( + mut self, + label: T, + amount: A, + output_label: T, + ) -> Self { + let key = self.get_workspace_offset_id_from_named_arg(label.into()); + let output_key = self.workspace_ids.insert(output_label.into()); + self.add_instruction(Instruction::TakeFromBucket { + input_bucket: key, + amount: amount.into(), + output_bucket: output_key, + }) + } + pub fn assert_bucket_contains, A: Into>( self, label: T, diff --git a/crates/transaction/src/v1/instruction.rs b/crates/transaction/src/v1/instruction.rs index 304de1dbdf..b8dc14535f 100644 --- a/crates/transaction/src/v1/instruction.rs +++ b/crates/transaction/src/v1/instruction.rs @@ -72,6 +72,11 @@ pub enum Instruction { resource_address: ResourceAddress, min_amount: Amount, }, + TakeFromBucket { + input_bucket: WorkspaceOffsetId, + amount: Amount, + output_bucket: WorkspaceId, + }, PublishTemplate { binary: Vec, }, @@ -181,6 +186,18 @@ impl Display for Instruction { key, resource_address, min_amount ) }, + + Self::TakeFromBucket { + input_bucket, + amount, + output_bucket, + } => { + write!( + f, + "TakeFromBucket {{ input_bucket: {}, amount: {}, output_bucket: {} }}", + input_bucket, amount, output_bucket + ) + }, Self::PublishTemplate { .. } => { write!(f, "PublishTemplate") }, diff --git a/crates/transaction/src/v1/transaction.rs b/crates/transaction/src/v1/transaction.rs index 56a123cf3c..6b0b4f7b9f 100644 --- a/crates/transaction/src/v1/transaction.rs +++ b/crates/transaction/src/v1/transaction.rs @@ -188,6 +188,7 @@ fn calc_instruction_weight(instruction: &Instruction) -> u64 { Instruction::ClaimValidatorFees { .. } => 1, Instruction::DropAllProofsInWorkspace => 1, Instruction::AssertBucketContains { .. } => 1, + Instruction::TakeFromBucket { .. } => 1, Instruction::PublishTemplate { binary } => binary.len() as u64 / BINARY_WEIGHT_DIVISOR, Instruction::AllocateAddress { .. } => 1, Instruction::StealthTransfer { statement, .. } => calc_stealth_statement_weight(statement), @@ -197,7 +198,7 @@ fn calc_instruction_weight(instruction: &Instruction) -> u64 { fn calc_stealth_statement_weight(statement: &StealthTransferStatement) -> u64 { // TODO: weight inputs and outputs accordingly - currently outputs cost 2x inputs - statement.inputs_statement.inputs.len() as u64 + (statement.outputs_statement.outputs.len() as u64 * 2) + 100 + statement.inputs_statement.inputs.len() as u64 + (statement.outputs_statement.outputs.len() as u64 * 2) } fn calc_args_weight(args: &[InstructionArg]) -> u64 { diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs index 3a4454dd84..acf7d82af8 100644 --- a/crates/wallet/sdk/src/apis/stealth_outputs.rs +++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs @@ -41,7 +41,7 @@ use crate::{ config::{ConfigApi, ConfigApiError}, key_manager::{KeyManagerApi, KeyManagerApiError}, stealth_crypto::{StealthCryptoApi, StealthCryptoApiError}, - stealth_transfer::{OutputToCreate, UnblindedInputToSpend}, + stealth_transfer::{StealthOutputToCreate, UnblindedInputToSpend}, }, models::{ AccountAndViewKeys, @@ -664,7 +664,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { params: TransferStatementParams<'_, I>, ) -> Result where - I: IntoIterator>, + I: IntoIterator>, { let unblinded_inputs = self.resolve_output_masks_for_spending( params.spend_key_branch, @@ -677,7 +677,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { .into_iter() .map(|output| { self.create_output_witness( - output.owner_address, + &output.owner_address, output.amount, params.resource_address, params.resource_view_key.clone(), diff --git a/crates/wallet/sdk/src/apis/stealth_transfer.rs b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs similarity index 67% rename from crates/wallet/sdk/src/apis/stealth_transfer.rs rename to crates/wallet/sdk/src/apis/stealth_transfer/api.rs index ef5538a0c2..4994ea177b 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs @@ -1,19 +1,19 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::cmp; +use std::{cmp, collections::HashSet}; use log::*; use tari_crypto::ristretto::RistrettoPublicKey; use tari_engine_types::{substate::SubstateId, ConvertFromByteType, FromByteType, ToByteType}; -use tari_ootle_address::{OotleAddress, RistrettoOotleAddress}; +use tari_ootle_address::RistrettoOotleAddress; use tari_ootle_common_types::{ displayable::Displayable, optional::{IsNotFoundError, Optional}, Network, SubstateRequirement, }; -use tari_ootle_wallet_crypto::{memo::Memo, UnblindedStealthInputWitness}; +use tari_ootle_wallet_crypto::memo::Memo; use tari_template_lib::{ constants::XTR, models::{ @@ -23,35 +23,30 @@ use tari_template_lib::{ StealthTransferStatement, StealthUnspentOutput, UtxoAddress, - VaultId, }, types::Amount, }; use tari_transaction::{args, Transaction, UnsignedTransaction}; use tokio::sync::Semaphore; +use super::{ + error::StealthTransferApiError, + params::StealthTransferParams, + types::{InputsToSpend, StealthOutputToCreate, StealthTransferOutput}, + TransferOutput, +}; use crate::{ apis::{ - accounts::{derive_account_address_from_public_key, AccountsApi, AccountsApiError}, + accounts::{derive_account_address_from_public_key, AccountsApi}, confidential_transfer::ConfidentialTransferInputSelection, - config::{ConfigApi, ConfigApiError}, - key_manager::{KeyManagerApi, KeyManagerApiError}, - stealth_crypto::StealthCryptoApiError, - stealth_outputs::{StealthOutputsApi, StealthOutputsApiError, TransferStatementParams}, - substate::{SubstateApiError, SubstatesApi, ValidatorScanResult}, - }, - models::{ - AccountWithAddress, - InputSpendData, - KeyBranch, - KeyId, - OutputStatus, - StealthOutputModel, - WalletLockId, - WalletPublicKey, + config::ConfigApi, + key_manager::KeyManagerApi, + stealth_outputs::{StealthOutputsApi, TransferStatementParams}, + substate::{SubstatesApi, ValidatorScanResult}, }, + models::{AccountWithAddress, KeyBranch, KeyId, OutputStatus, StealthOutputModel, WalletLockId}, network::WalletNetworkInterface, - storage::{WalletStorageError, WalletStore}, + storage::WalletStore, }; const LOG_TARGET: &str = "tari::ootle::wallet_sdk::apis::stealth_transfers"; @@ -293,7 +288,7 @@ where &self, owner_account: AccountWithAddress, params: StealthTransferParams, - ) -> Result { + ) -> Result { let network = self.config_api.get_network()?; params.validate(network)?; @@ -307,12 +302,9 @@ where }); }; - let destination_account = - derive_account_address_from_public_key(params.destination_address.account_public_key()); - // Determine Transaction Inputs let mut substate_inputs = Vec::new(); - let owner_address = + let owner_address: RistrettoOotleAddress = owner_account .address() .try_from_byte_type() @@ -324,83 +316,16 @@ where // add the input for the resource address to be transferred substate_inputs.push(SubstateRequirement::unversioned(params.resource_address)); - let need_to_create_dest_account = if params.revealed_output_amount.is_positive() { - match self - .accounts_api - .get_account_by_address(&destination_account) - .optional()? - { - Some(local_account) => { - if local_account.is_confirmed_on_chain() { - substate_inputs.push(SubstateRequirement::unversioned(destination_account)); - if let Some(vault) = self - .accounts_api - .get_vault_by_resource(local_account.component_address(), ¶ms.resource_address) - .optional()? - { - substate_inputs.push(SubstateRequirement::unversioned(vault.id)); - } - - false - } else { - true - } - }, - None => { - // TODO: we're just determining if the account exists - symptom of a larger problem/missing - // 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 - .fetch_substate_from_network(&SubstateId::Component(destination_account), None) - .await - .optional()?; - - if let Some(ValidatorScanResult { id: address, substate }) = to_account_substate { - substate_inputs.push(SubstateRequirement::unversioned(destination_account)); - - let account = - substate - .component() - .ok_or_else(|| StealthTransferApiError::UnexpectedIndexerResponse { - details: format!( - "Expected indexer to return component for address {}. It returned {}", - destination_account, address - ), - })?; - let dest_account = BuiltinAccount::from_value(account.state()).map_err(|e| { - StealthTransferApiError::UnexpectedIndexerResponse { - details: format!("Failed to convert component substate to account: {e}"), - } - })?; - // If they have an existing vault, we need to add it as an input - if let Some(vault) = dest_account.get_vault_by_resource(¶ms.resource_address) { - debug!( - target: LOG_TARGET, - "Found existing vault {} for resource {} in destination account {}", - vault.vault_id(), - params.resource_address, - destination_account - ); - substate_inputs.push(SubstateRequirement::unversioned(vault.vault_id())); - } else { - debug!( - target: LOG_TARGET, - "No existing vault found for resource {} in destination account {}. It will be created.", - params.resource_address, - destination_account - ); - } - false - } else { - // If the account does not exist, we need to create it - true - } - }, + let mut accounts_to_create = HashSet::new(); + for output in ¶ms.outputs { + let need_to_create_dest_account = self + .determine_destination_account_inputs(output, ¶ms.resource_address, &mut substate_inputs) + .await?; + if need_to_create_dest_account { + let dest_account = derive_account_address_from_public_key(output.address.account_public_key()); + accounts_to_create.insert(dest_account); } - } else { - false - }; + } // We need to fetch the resource substate to check if there is a view key present. let resource = self.substate_api.fetch_resource(params.resource_address).await?; @@ -414,12 +339,8 @@ where param: "resource_view_key", reason: format!("Invalid resource view key: {e}"), })?; - let destination_address = params - .destination_address - .try_from_byte_type() - .expect("already validated"); - // Critical section + // Critical section - TODO: use a DB transaction let _permit = self.semaphore.acquire().await.expect("semaphore is never closed"); let lock_id = self.outputs_api.create_lock()?; @@ -430,22 +351,19 @@ where self.lock_fee_inputs(lock_id, &owner_account, params.max_fee, params.input_selection), )?; - // TODO: use single db transaction across calls - // --- Any error from here can result in funds staying locked --- - 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 = Some(OutputToCreate { - owner_address: &owner_address, + let fee_change_output = Some(StealthOutputToCreate { + owner_address: owner_address.clone(), amount: fee_stealth_change_amt, memo: None, }) .filter(|o| o.amount.is_positive()); - // Figure out which signing key to use - if there are no revealed funds, which necessitate using a account + // Figure out which signing key to use - if there are no revealed funds, which necessitate using an account // withdraw auth signature, then we can use a nonce key. 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 { @@ -570,12 +488,18 @@ where panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); }); - let change_output = Some(OutputToCreate { - owner_address: &owner_address, + let change_output = Some(StealthOutputToCreate { + owner_address, amount: change_amount, memo: None, }); + let outputs_to_create = params + .outputs + .iter() + .map(TryInto::try_into) + .collect::, StealthTransferApiError>>()?; + let transfer_statement = self.unlock_on_failure( lock_id, self.outputs_api.generate_transfer_statement(TransferStatementParams { @@ -586,15 +510,11 @@ where 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) - .filter(|o| o.amount.is_positive()), - output_revealed_amount: params.revealed_output_amount, + outputs: outputs_to_create + .into_iter() + .chain(change_output) + .filter(|o| o.amount.is_positive()), + output_revealed_amount: params.total_revealed_output_amount(), required_signer: required_signer_pk, }), )?; @@ -602,38 +522,33 @@ 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!( + if change_amount.is_positive() { + if let Some(output) = transfer_statement.outputs_statement.outputs.last() { + 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( + ); + self.unlock_on_failure( lock_id, - &owner_account, - params.resource_address, - output, - change_amount, - None, - ), - )?; + 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 + .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) @@ -655,16 +570,17 @@ where let transaction = self.unlock_on_failure( lock_id, self.generate_transfer_transaction( + network, &owner_account, params, substate_inputs, fee_transfer_statement, transfer_statement, - need_to_create_dest_account, + &accounts_to_create, ), )?; - Ok(TransferOutput { + Ok(StealthTransferOutput { transaction, lock_id, fee_inputs: fee_inputs_to_spend, @@ -674,6 +590,95 @@ where }) } + async fn determine_destination_account_inputs( + &self, + output: &TransferOutput, + resource_address: &ResourceAddress, + substate_inputs: &mut Vec, + ) -> Result { + // No revealed outputs, no need to use the account + if !output.revealed_amount.is_positive() { + return Ok(false); + } + + let destination_account = derive_account_address_from_public_key(output.address.account_public_key()); + + // Local account? (Saves a call to the indexer) + match self + .accounts_api + .get_account_by_address(&destination_account) + .optional()? + { + Some(local_account) => { + if local_account.is_confirmed_on_chain() { + substate_inputs.push(SubstateRequirement::unversioned(destination_account)); + if let Some(vault) = self + .accounts_api + .get_vault_by_resource(local_account.component_address(), resource_address) + .optional()? + { + substate_inputs.push(SubstateRequirement::unversioned(vault.id)); + } + + Ok(false) + } else { + Ok(true) + } + }, + None => { + // TODO: we're just determining if the account exists - symptom of a larger problem/missing + // 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 + .fetch_substate_from_network(&SubstateId::Component(destination_account), None) + .await + .optional()?; + + if let Some(ValidatorScanResult { id: address, substate }) = to_account_substate { + substate_inputs.push(SubstateRequirement::unversioned(destination_account)); + + let account = + substate + .component() + .ok_or_else(|| StealthTransferApiError::UnexpectedIndexerResponse { + details: format!( + "Expected indexer to return component for address {}. It returned {}", + destination_account, address + ), + })?; + let dest_account = BuiltinAccount::from_value(account.state()).map_err(|e| { + StealthTransferApiError::UnexpectedIndexerResponse { + details: format!("Failed to convert component substate to account: {e}"), + } + })?; + // If they have an existing vault, we need to add it as an input + if let Some(vault) = dest_account.get_vault_by_resource(resource_address) { + debug!( + target: LOG_TARGET, + "Found existing vault {} for resource {} in destination account {}", + vault.vault_id(), + resource_address, + destination_account + ); + substate_inputs.push(SubstateRequirement::unversioned(vault.vault_id())); + } else { + debug!( + target: LOG_TARGET, + "No existing vault found for resource {} in destination account {}. It will be created.", + resource_address, + destination_account + ); + } + Ok(false) + } else { + // If the account does not exist, we need to create it + Ok(true) + } + }, + } + } + pub fn unlock_on_failure(&self, lock_id: WalletLockId, result: Result) -> Result { match result { Ok(value) => Ok(value), @@ -688,18 +693,19 @@ where fn generate_transfer_transaction( &self, + network: Network, owner_account: &AccountWithAddress, params: StealthTransferParams, inputs: Vec, fee_transfer_statement: StealthTransferStatement, transfer_statement: StealthTransferStatement, - need_to_create_account: bool, + accounts_to_create: &HashSet, ) -> Result { let revealed_input_amount = transfer_statement.inputs_statement.revealed_amount; let revealed_output_amount = transfer_statement.outputs_statement.revealed_output_amount; let transaction = Transaction::builder() - .for_network(params.destination_address.network().as_byte()) + .for_network(network.as_byte()) .with_dry_run(params.is_dry_run) .with_fee_instructions_builder(|builder| { if fee_transfer_statement.inputs_statement.revealed_amount.is_positive() { @@ -737,16 +743,29 @@ where builder .put_last_instruction_output_on_workspace("output_bucket") .then(|builder| { - if need_to_create_account { - builder.create_account_with_bucket( - *params.destination_address.account_public_key(), - "output_bucket", - ) - } else { - builder.call_method(params.derived_destination_account(), "deposit", args![Workspace( - "output_bucket" - )]) - } + params.outputs.iter().enumerate().fold(builder, |builder, (i, output)| { + if !output.revealed_amount.is_positive() { + return builder; + } + + let dest_account = derive_account_address_from_public_key(output.address.account_public_key()); + let need_to_create_account = accounts_to_create.contains(&dest_account); + let sub_bucket_name = format!("output-sub-bucket-{i}"); + if need_to_create_account { + builder + .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) + .create_account_with_bucket( + *output.address.account_public_key(), + sub_bucket_name + ) + } else { + builder + .take_from_bucket("output_bucket", output.revealed_amount, &sub_bucket_name) + .call_method(dest_account, "deposit", args![Workspace( + sub_bucket_name + )]) + } + }) }) }) .with_inputs(inputs) @@ -787,164 +806,3 @@ where Ok(()) } } - -pub struct TransferOutput { - pub transaction: UnsignedTransaction, - pub lock_id: WalletLockId, - pub fee_inputs: InputsToSpend, - pub transfer_inputs: InputsToSpend, - pub additional_signer: Option, - pub main_signer: WalletPublicKey, -} - -#[derive(Debug)] -pub struct StealthTransferParams { - /// Strategy for input selection - pub input_selection: ConfidentialTransferInputSelection, - /// Amount of the inputs to spend to a blinded output - pub blinded_output_amount: Amount, - /// Amount of the inputs to spend to a revealed output - pub revealed_output_amount: Amount, - /// Optional memo to include a memo in the output. This memo is encrypted and can only be read by the recipient. - pub output_memo: Option, - /// Destination address used to derive the UTXO encryption keys, owner signature and the account in which to - /// deposit revealed funds - pub destination_address: OotleAddress, - /// Address of the resource to transfer - pub resource_address: ResourceAddress, - /// Fee to lock for the transaction - pub max_fee: u64, - /// Run as a dry run, no funds will be transferred if true - pub is_dry_run: bool, -} - -impl StealthTransferParams { - pub fn validate(&self, network: Network) -> Result<(), StealthTransferApiError> { - if self.blinded_output_amount.is_negative() { - return Err(StealthTransferApiError::InvalidParameter { - param: "blinded_output_amount", - reason: "Blinded output amount must be non-negative".to_string(), - }); - } - - if self.revealed_output_amount.is_negative() { - return Err(StealthTransferApiError::InvalidParameter { - param: "revealed_output_amount", - reason: "Revealed output amount must be non-negative".to_string(), - }); - } - - if self.blinded_output_amount.is_zero() && self.revealed_output_amount.is_zero() { - return Err(StealthTransferApiError::InvalidParameter { - param: "blinded_output_amount and revealed_output_amount", - reason: "At least one of the amounts must be greater than zero".to_string(), - }); - } - - if self.destination_address.network() != network { - return Err(StealthTransferApiError::InvalidParameter { - param: "destination_address", - reason: format!( - "Destination address network ({}) does not match wallet network ({})", - self.destination_address.network(), - network - ), - }); - } - - self.destination_address - .validate() - .map_err(|e| StealthTransferApiError::InvalidParameter { - param: "destination_address", - reason: format!("Invalid destination address: {}", e), - })?; - - Ok(()) - } - - pub fn total_output_amount(&self) -> Amount { - self.blinded_output_amount + self.revealed_output_amount - } - - pub fn derived_destination_account(&self) -> ComponentAddress { - derive_account_address_from_public_key(self.destination_address.account_public_key()) - } -} - -#[derive(Debug)] -pub struct UnblindedInputToSpend { - pub witness: UnblindedStealthInputWitness, -} - -impl UnblindedInputToSpend { - pub fn value(&self) -> Amount { - 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 revealed: Amount, -} - -impl InputsToSpend { - pub fn inputs_iter(&self) -> impl Iterator + '_ { - self.inputs.iter() - } - - pub fn total_amount(&self) -> Amount { - self.total_stealth_input_amount() + self.revealed - } - - pub fn total_stealth_input_amount(&self) -> Amount { - self.inputs.iter().map(|i| i.value).sum() - } -} - -#[derive(Debug, thiserror::Error)] -pub enum StealthTransferApiError { - #[error("Store error: {0}")] - StoreError(#[from] WalletStorageError), - #[error("Confidential crypto error: {0}")] - Crypto(#[from] StealthCryptoApiError), - #[error("Stealth outputs error: {0}")] - OutputsApi(#[from] StealthOutputsApiError), - #[error("Substate API error: {0}")] - SubstateApi(#[from] SubstateApiError), - #[error("Key manager error: {0}")] - KeyManagerApi(#[from] KeyManagerApiError), - #[error("Insufficient funds")] - InsufficientFunds, - #[error("Accounts API error: {0}")] - Accounts(#[from] AccountsApiError), - #[error("Invalid parameter `{param}`: {reason}")] - InvalidParameter { param: &'static str, reason: String }, - #[error("Unexpected indexer response: {details}")] - UnexpectedIndexerResponse { details: String }, - #[error("Config API error: {0}")] - ConfigApi(#[from] ConfigApiError), - #[error("Amount overflow for parameter `{param}`: {details}")] - AmountOverflow { param: &'static str, details: String }, - #[error("Insufficient revealed funds: {details}")] - InsufficientRevealedFunds { details: String }, -} - -impl IsNotFoundError for StealthTransferApiError { - fn is_not_found_error(&self) -> bool { - matches!(self, Self::StoreError(e) if e.is_not_found_error() ) - } -} - -pub struct AccountDetails { - pub address: ComponentAddress, - pub vaults: Vec, - pub exists: bool, -} diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs new file mode 100644 index 0000000000..663233874f --- /dev/null +++ b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs @@ -0,0 +1,50 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_ootle_common_types::optional::IsNotFoundError; + +use crate::{ + apis::{ + accounts::AccountsApiError, + config::ConfigApiError, + key_manager::KeyManagerApiError, + stealth_crypto::StealthCryptoApiError, + stealth_outputs::StealthOutputsApiError, + substate::SubstateApiError, + }, + storage::WalletStorageError, +}; + +#[derive(Debug, thiserror::Error)] +pub enum StealthTransferApiError { + #[error("Store error: {0}")] + StoreError(#[from] WalletStorageError), + #[error("Confidential crypto error: {0}")] + Crypto(#[from] StealthCryptoApiError), + #[error("Stealth outputs error: {0}")] + OutputsApi(#[from] StealthOutputsApiError), + #[error("Substate API error: {0}")] + SubstateApi(#[from] SubstateApiError), + #[error("Key manager error: {0}")] + KeyManagerApi(#[from] KeyManagerApiError), + #[error("Insufficient funds")] + InsufficientFunds, + #[error("Accounts API error: {0}")] + Accounts(#[from] AccountsApiError), + #[error("Invalid parameter `{param}`: {reason}")] + InvalidParameter { param: &'static str, reason: String }, + #[error("Unexpected indexer response: {details}")] + UnexpectedIndexerResponse { details: String }, + #[error("Config API error: {0}")] + ConfigApi(#[from] ConfigApiError), + #[error("Amount overflow for parameter `{param}`: {details}")] + AmountOverflow { param: &'static str, details: String }, + #[error("Insufficient revealed funds: {details}")] + InsufficientRevealedFunds { details: String }, +} + +impl IsNotFoundError for StealthTransferApiError { + fn is_not_found_error(&self) -> bool { + matches!(self, Self::StoreError(e) if e.is_not_found_error() ) + } +} diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/mod.rs b/crates/wallet/sdk/src/apis/stealth_transfer/mod.rs new file mode 100644 index 0000000000..c4dbc31788 --- /dev/null +++ b/crates/wallet/sdk/src/apis/stealth_transfer/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2023 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +mod api; +mod error; +mod params; +mod types; + +pub use api::*; +pub use error::*; +pub use params::*; +pub use types::*; diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/params.rs b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs new file mode 100644 index 0000000000..13315f0049 --- /dev/null +++ b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs @@ -0,0 +1,136 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_engine_types::{crypto::MAX_LAZY_BP_AGG_FACTORS, FromByteType}; +use tari_ootle_address::OotleAddress; +use tari_ootle_common_types::Network; +use tari_ootle_wallet_crypto::memo::Memo; +use tari_template_lib::{models::ResourceAddress, prelude::Amount}; + +use crate::apis::{ + confidential_transfer::ConfidentialTransferInputSelection, + stealth_transfer::{StealthOutputToCreate, StealthTransferApiError}, +}; + +#[derive(Debug)] +pub struct StealthTransferParams { + /// Strategy for input selection + pub input_selection: ConfidentialTransferInputSelection, + pub outputs: Vec, + /// Address of the resource to transfer + pub resource_address: ResourceAddress, + /// Fee to lock for the transaction + pub max_fee: u64, + /// Run as a dry run, no funds will be transferred if true + pub is_dry_run: bool, +} + +impl StealthTransferParams { + pub fn validate(&self, network: Network) -> Result<(), StealthTransferApiError> { + if self.outputs.is_empty() { + return Err(StealthTransferApiError::InvalidParameter { + param: "outputs", + reason: "At least one output must be specified".to_string(), + }); + } + + let blinded_count = self.outputs.iter().filter(|o| o.blinded_amount.is_positive()).count(); + if blinded_count > MAX_LAZY_BP_AGG_FACTORS { + return Err(StealthTransferApiError::InvalidParameter { + param: "outputs", + reason: format!( + "Number of outputs ({}) exceeds maximum allowed ({})", + blinded_count, MAX_LAZY_BP_AGG_FACTORS + ), + }); + } + + for output in &self.outputs { + if output.blinded_amount.is_negative() { + return Err(StealthTransferApiError::InvalidParameter { + param: "blinded_output_amount", + reason: "Blinded output amount must be non-negative".to_string(), + }); + } + + if output.revealed_amount.is_negative() { + return Err(StealthTransferApiError::InvalidParameter { + param: "revealed_output_amount", + reason: "Revealed output amount must be non-negative".to_string(), + }); + } + + if output.blinded_amount.is_zero() && output.revealed_amount.is_zero() { + return Err(StealthTransferApiError::InvalidParameter { + param: "blinded_output_amount and revealed_output_amount", + reason: "At least one of the amounts must be greater than zero".to_string(), + }); + } + + if output.address.network() != network { + return Err(StealthTransferApiError::InvalidParameter { + param: "destination_address", + reason: format!( + "Destination address network ({}) does not match wallet network ({})", + output.address.network(), + network + ), + }); + } + + output + .address + .validate() + .map_err(|e| StealthTransferApiError::InvalidParameter { + param: "destination_address", + reason: format!("Invalid destination address: {}", e), + })?; + } + + Ok(()) + } + + pub fn total_output_amount(&self) -> Amount { + self.outputs.iter().map(|o| o.total_output_amount()).sum() + } + + pub fn total_revealed_output_amount(&self) -> Amount { + self.outputs.iter().map(|o| o.revealed_amount).sum() + } +} + +#[derive(Debug, Clone)] +pub struct TransferOutput { + /// Destination address used to derive the UTXO encryption keys, owner signature and the account in which to + /// deposit revealed funds + pub address: OotleAddress, + /// Amount to spend to a revealed output + pub revealed_amount: Amount, + /// Amount to spend to a blinded output + pub blinded_amount: Amount, + /// Optional memo to include a memo in the output. This memo is encrypted and can only be read by the recipient. + pub memo: Option, +} + +impl TransferOutput { + pub fn total_output_amount(&self) -> Amount { + self.revealed_amount + self.blinded_amount + } +} + +impl<'a> TryFrom<&'a TransferOutput> for StealthOutputToCreate<'a> { + type Error = StealthTransferApiError; + + fn try_from(value: &'a TransferOutput) -> Result { + Ok(Self { + owner_address: value.address.try_from_byte_type().map_err(|e| { + StealthTransferApiError::InvalidParameter { + param: "destination_address", + reason: format!("Invalid destination address: {}", e), + } + })?, + amount: value.blinded_amount, + memo: value.memo.as_ref(), + }) + } +} diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/types.rs b/crates/wallet/sdk/src/apis/stealth_transfer/types.rs new file mode 100644 index 0000000000..2585bb39b1 --- /dev/null +++ b/crates/wallet/sdk/src/apis/stealth_transfer/types.rs @@ -0,0 +1,65 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use tari_ootle_address::RistrettoOotleAddress; +use tari_ootle_wallet_crypto::{memo::Memo, UnblindedStealthInputWitness}; +use tari_template_lib::{ + models::{ComponentAddress, VaultId}, + prelude::Amount, +}; +use tari_transaction::UnsignedTransaction; + +use crate::models::{InputSpendData, WalletLockId, WalletPublicKey}; + +pub struct StealthTransferOutput { + pub transaction: UnsignedTransaction, + pub lock_id: WalletLockId, + pub fee_inputs: InputsToSpend, + pub transfer_inputs: InputsToSpend, + pub additional_signer: Option, + pub main_signer: WalletPublicKey, +} + +#[derive(Debug)] +pub struct UnblindedInputToSpend { + pub witness: UnblindedStealthInputWitness, +} + +impl UnblindedInputToSpend { + pub fn value(&self) -> Amount { + self.witness.mask_and_value.value + } +} + +#[derive(Debug, Clone)] +pub struct StealthOutputToCreate<'a> { + pub owner_address: RistrettoOotleAddress, + pub amount: Amount, + pub memo: Option<&'a Memo>, +} + +#[derive(Debug)] +pub struct InputsToSpend { + pub inputs: Vec, + pub revealed: Amount, +} + +impl InputsToSpend { + pub fn inputs_iter(&self) -> impl Iterator + '_ { + self.inputs.iter() + } + + pub fn total_amount(&self) -> Amount { + self.total_stealth_input_amount() + self.revealed + } + + pub fn total_stealth_input_amount(&self) -> Amount { + self.inputs.iter().map(|i| i.value).sum() + } +} + +pub struct AccountDetails { + pub address: ComponentAddress, + pub vaults: Vec, + pub exists: bool, +} diff --git a/integration_tests/src/wallet_daemon_client.rs b/integration_tests/src/wallet_daemon_client.rs index ffaa62549b..eefc482578 100644 --- a/integration_tests/src/wallet_daemon_client.rs +++ b/integration_tests/src/wallet_daemon_client.rs @@ -52,6 +52,7 @@ use tari_wallet_daemon_client::{ ConfidentialTransferRequest, ListNftsRequest, MintFaucetNftRequest, + StealthTransfer, StealthTransferRequest, TransactionSubmitRequest, TransactionWaitResultRequest, @@ -122,11 +123,13 @@ pub async fn transfer_stealth( owner_account: source_account_name, input_selection: ConfidentialTransferInputSelection::PreferRevealed, resource_address, - destination_address: dest_account.address, + transfers: vec![StealthTransfer { + destination_address: dest_account.address, + blinded_output_amount: amount, + revealed_output_amount: Default::default(), + output_memo: None, + }], max_fee: 2000, - blinded_output_amount: amount, - revealed_output_amount: Default::default(), - output_memo: None, dry_run: false, }) .await