diff --git a/Cargo.lock b/Cargo.lock index f66b7e7b00..735e9a3f6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,6 +1154,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "119771309b95163ec7aaf79810da82f7cd0599c19722d48b9c03894dca833966" dependencies = [ "borsh", + "num-integer", + "num-traits", ] [[package]] @@ -1840,7 +1842,7 @@ dependencies = [ [[package]] name = "consensus_tests" -version = "0.15.1" +version = "0.16.0" dependencies = [ "fern", "futures 0.3.31", @@ -2628,7 +2630,7 @@ dependencies = [ [[package]] name = "db_inspector" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -3780,7 +3782,7 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generate_ristretto_value_lookup" -version = "0.15.1" +version = "0.16.0" dependencies = [ "clap 3.2.25", "futures 0.3.31", @@ -5030,7 +5032,7 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "integration_tests" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "config", @@ -5809,7 +5811,7 @@ dependencies = [ [[package]] name = "libp2p-messaging" -version = "0.15.1" +version = "0.16.0" dependencies = [ "async-trait", "futures-bounded", @@ -5988,7 +5990,7 @@ dependencies = [ [[package]] name = "libp2p-substream" -version = "0.15.1" +version = "0.16.0" dependencies = [ "libp2p", "prometheus-client", @@ -8514,7 +8516,7 @@ dependencies = [ [[package]] name = "proto_builder" -version = "0.15.1" +version = "0.16.0" dependencies = [ "prost-build 0.14.1", "sha2", @@ -10348,7 +10350,7 @@ dependencies = [ [[package]] name = "sqlite_message_logger" -version = "0.15.1" +version = "0.16.0" dependencies = [ "chrono", "diesel", @@ -10389,7 +10391,7 @@ checksum = "e7386b49cb287f6fafbfd3bd604914bccb99fb8d53483f40e1ecfda5d45f3370" [[package]] name = "state_store_tests" -version = "0.15.1" +version = "0.16.0" dependencies = [ "env_logger 0.11.8", "indexmap 2.11.4", @@ -10778,7 +10780,7 @@ dependencies = [ [[package]] name = "tari_base_node_client" -version = "0.15.1" +version = "0.16.0" dependencies = [ "log", "minotari_app_grpc", @@ -11002,7 +11004,7 @@ dependencies = [ [[package]] name = "tari_consensus" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "indexmap 2.11.4", @@ -11026,7 +11028,7 @@ dependencies = [ [[package]] name = "tari_consensus_types" -version = "0.15.1" +version = "0.16.0" dependencies = [ "borsh", "serde", @@ -11128,7 +11130,7 @@ dependencies = [ [[package]] name = "tari_engine" -version = "0.15.1" +version = "0.16.0" dependencies = [ "blake2", "cargo_toml 0.22.3", @@ -11157,7 +11159,7 @@ dependencies = [ [[package]] name = "tari_engine_types" -version = "0.15.1" +version = "0.16.0" dependencies = [ "base64 0.21.7", "bincode 2.0.1", @@ -11184,7 +11186,7 @@ dependencies = [ [[package]] name = "tari_epoch_manager" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "log", @@ -11206,7 +11208,7 @@ dependencies = [ [[package]] name = "tari_epoch_oracles" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "blake2", @@ -11249,7 +11251,7 @@ dependencies = [ [[package]] name = "tari_indexer" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "async-graphql", @@ -11313,7 +11315,7 @@ dependencies = [ [[package]] name = "tari_indexer_client" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "bounded-vec", @@ -11343,7 +11345,7 @@ dependencies = [ [[package]] name = "tari_indexer_lib" -version = "0.15.1" +version = "0.16.0" dependencies = [ "log", "serde", @@ -11419,7 +11421,7 @@ dependencies = [ [[package]] name = "tari_networking" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "async-trait", @@ -11456,7 +11458,7 @@ dependencies = [ [[package]] name = "tari_ootle_address" -version = "0.15.1" +version = "0.16.0" dependencies = [ "bech32", "bincode 2.0.1", @@ -11472,7 +11474,7 @@ dependencies = [ [[package]] name = "tari_ootle_app_utilities" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -11511,7 +11513,7 @@ dependencies = [ [[package]] name = "tari_ootle_common_types" -version = "0.15.1" +version = "0.16.0" dependencies = [ "blake2", "borsh", @@ -11539,7 +11541,7 @@ dependencies = [ [[package]] name = "tari_ootle_p2p" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "prost 0.14.1", @@ -11561,7 +11563,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "bitflags 2.9.2", @@ -11586,7 +11588,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage_sqlite" -version = "0.15.1" +version = "0.16.0" dependencies = [ "diesel", "diesel_migrations", @@ -11606,7 +11608,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_cli" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -11635,7 +11637,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_crypto" -version = "0.15.1" +version = "0.16.0" dependencies = [ "argon2 0.5.3", "blake2", @@ -11659,7 +11661,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "blake2", @@ -11694,7 +11696,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk_services" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "futures 0.3.31", @@ -11719,7 +11721,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_storage_sqlite" -version = "0.15.1" +version = "0.16.0" dependencies = [ "bigdecimal", "diesel", @@ -11743,7 +11745,7 @@ dependencies = [ [[package]] name = "tari_ootle_walletd" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "async-trait", @@ -11828,7 +11830,7 @@ dependencies = [ [[package]] name = "tari_rpc_framework" -version = "0.15.1" +version = "0.16.0" dependencies = [ "async-trait", "bitflags 2.9.2", @@ -11852,7 +11854,7 @@ dependencies = [ [[package]] name = "tari_rpc_macros" -version = "0.15.1" +version = "0.16.0" dependencies = [ "proc-macro2", "quote", @@ -11861,7 +11863,7 @@ dependencies = [ [[package]] name = "tari_rpc_state_sync" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "futures 0.3.31", @@ -11881,7 +11883,7 @@ dependencies = [ [[package]] name = "tari_scaffolder" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -11953,7 +11955,7 @@ dependencies = [ [[package]] name = "tari_signaling_server" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -11979,7 +11981,7 @@ dependencies = [ [[package]] name = "tari_state_store_rocksdb" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -12005,7 +12007,7 @@ dependencies = [ [[package]] name = "tari_state_tree" -version = "0.15.1" +version = "0.16.0" dependencies = [ "indexmap 2.11.4", "log", @@ -12032,7 +12034,7 @@ dependencies = [ [[package]] name = "tari_swarm" -version = "0.15.1" +version = "0.16.0" dependencies = [ "libp2p", "libp2p-messaging", @@ -12042,7 +12044,7 @@ dependencies = [ [[package]] name = "tari_swarm_daemon" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "async-trait", @@ -12098,7 +12100,7 @@ dependencies = [ [[package]] name = "tari_template_builtin" -version = "0.15.1" +version = "0.16.0" dependencies = [ "tari_engine_types", "tari_template_lib", @@ -12128,6 +12130,7 @@ dependencies = [ "bnum", "borsh", "newtype-ops", + "num-integer", "serde", "serde_json", "tari_bor", @@ -12150,7 +12153,7 @@ dependencies = [ [[package]] name = "tari_template_manager" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "bytes 1.10.1", @@ -12216,7 +12219,7 @@ dependencies = [ [[package]] name = "tari_transaction" -version = "0.15.1" +version = "0.16.0" dependencies = [ "borsh", "hex", @@ -12304,7 +12307,7 @@ dependencies = [ [[package]] name = "tari_transaction_manifest" -version = "0.15.1" +version = "0.16.0" dependencies = [ "proc-macro2", "serde_json", @@ -12338,7 +12341,7 @@ dependencies = [ [[package]] name = "tari_validator_node" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -12400,7 +12403,7 @@ dependencies = [ [[package]] name = "tari_validator_node_cli" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -12428,7 +12431,7 @@ dependencies = [ [[package]] name = "tari_validator_node_client" -version = "0.15.1" +version = "0.16.0" dependencies = [ "indexmap 2.11.4", "multiaddr 0.18.1", @@ -12451,7 +12454,7 @@ dependencies = [ [[package]] name = "tari_validator_node_rpc" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "prost 0.14.1", @@ -12473,7 +12476,7 @@ dependencies = [ [[package]] name = "tari_wallet_daemon_client" -version = "0.15.1" +version = "0.16.0" dependencies = [ "reqwest 0.11.27", "serde", @@ -12494,7 +12497,7 @@ dependencies = [ [[package]] name = "tari_watcher" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -12522,7 +12525,7 @@ dependencies = [ [[package]] name = "tariswap_bench" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "clap 4.5.48", @@ -13181,7 +13184,7 @@ dependencies = [ [[package]] name = "traffic-sim" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "clap 4.5.48", @@ -13203,7 +13206,7 @@ dependencies = [ [[package]] name = "transaction_generator" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -13223,7 +13226,7 @@ dependencies = [ [[package]] name = "transaction_submitter" -version = "0.15.1" +version = "0.16.0" dependencies = [ "anyhow", "clap 4.5.48", diff --git a/Cargo.toml b/Cargo.toml index 8ff3cf3ede..13f2f6ee3d 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.15.1" +version = "0.16.0" edition = "2021" authors = ["The Tari Development Community"] repository = "https://github.com/tari-project/tari-ootle" @@ -124,10 +124,7 @@ transaction_generator = { path = "utilities/transaction_generator" } # external minotari/tari dependencies minotari_app_grpc = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } minotari_app_utilities = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } -minotari_console_wallet = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } -minotari_node = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } minotari_node_grpc_client = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } -minotari_wallet = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } minotari_wallet_grpc_client = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } tari_common = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } tari_common_sqlite = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } @@ -213,6 +210,7 @@ mini-moka = "0.10.0" multiaddr = { git = "https://github.com/tari-project/rust-libp2p.git", rev = "debc01257dd979418d7793f2dc91d25471de5c38" } #multiaddr = "0.18" newtype-ops = "0.1.4" +num-integer = { version = "0.1.46", default-features = false } once_cell = "1.18.0" pin-project = "1.1" proc-macro2 = "1.0" diff --git a/applications/tari_indexer/README.md b/applications/tari_indexer/README.md index 15917e0959..62bb2709b7 100644 --- a/applications/tari_indexer/README.md +++ b/applications/tari_indexer/README.md @@ -53,16 +53,62 @@ The indexer uses configuration files and environment variables. Key configuratio ### Command Line Options -- `-r, --api-listen-address `: Bind address for REST API server -- `-g, --node-grpc `: Minotari node gRPC URL for epoch oracle -- `-s, --peer-seeds `: P2P peer seeds (space-separated) -- `--listener-port `: P2P listening port -- `-i, --scanning-interval `: Block scanning interval -- `--web-ui-public-api-url `: Public API URL for web UI -- `--web-ui-public-graphql-url `: Public GraphQL URL for web UI -- `--reachability `: P2P reachability mode (reachable/unreachable) +The following options are available (use `tari_indexer --help` for the most up-to-date list): + +#### Core Options +- `-h, --help`: Print help information +- `-V, --version`: Print version information + +#### Configuration +- `-c, --config `: Path to configuration file (default: `config/config.toml`) +- `-b, --base-path `: Directory to store files [env: `TARI_BASE_DIR`] (default: `~/.tari`) +- `--network `: Override network configuration [env: `TARI_NETWORK`] +- `-p `: Override config properties using fully qualified keys + +#### Logging +- `-l, --log-config `: Path to log configuration file +- `--log-path `: Directory for log storage + +#### API Server +- `-r, --api-listen-address `: Bind address for REST API server +- `--web-ui-public-api-url `: Public API URL for web UI [env: `TARI_INDEXER_WEB_UI_PUBLIC_API_URL`] +- `--web-ui-public-graphql-url `: Public GraphQL URL for web UI [env: `TARI_INDEXER_WEB_UI_PUBLIC_GRAPHQL_URL`] + +#### Blockchain Scanning +- `-a, --address
...`: Substate addresses to track (can be specified multiple times) +- `-i, --scanning-interval `: Block scanning interval in seconds + +#### P2P Networking +- `-s, --peer-seeds `: P2P peer seeds (can be specified multiple times) +- `--listener-port `: Port to listen on for P2P connections +- `--reachability `: P2P reachability mode (reachable/unreachable) - `--disable-mdns`: Disable mDNS peer discovery +#### Epoch Oracle +- `-g, --epoch-oracle-minotari-node-grpc-url `: Minotari node gRPC URL [env: `TARI_INDEXER_MINOTARI_NODE_GRPC_URL`] +- `--epoch-oracle-config `: Path to epoch oracle configuration file + +### Example Usage + +```bash +# Run with custom API address and peer seeds +./target/release/tari_indexer \ + --api-listen-address 0.0.0.0:8080 \ + --peer-seeds "peer1::address1" \ + --peer-seeds "peer2::address2" \ + --scanning-interval 10 + +# Run with environment variables +TARI_INDEXER_WEB_UI_PUBLIC_API_URL=http://localhost:8080 \ +TARI_INDEXER_MINOTARI_NODE_GRPC_URL=http://localhost:18142 \ +./target/release/tari_indexer + +# Run with custom configuration +./target/release/tari_indexer \ + --config /path/to/custom/config.toml \ + --base-path /custom/data/directory +``` + ## APIs ### REST API diff --git a/applications/tari_validator_node/src/genesis_state.rs b/applications/tari_validator_node/src/genesis_state.rs index 7be2a786e7..f8879b580e 100644 --- a/applications/tari_validator_node/src/genesis_state.rs +++ b/applications/tari_validator_node/src/genesis_state.rs @@ -38,7 +38,7 @@ use tari_template_lib::{ XTR_FAUCET_VAULT_ADDRESS, }, models::Metadata, - prelude::{ResourceManager, ResourceType}, + prelude::ResourceType, resource::TOKEN_SYMBOL, rule, types::EntityId, @@ -123,6 +123,15 @@ where TTx::Target: StateStoreReadTransaction, TTx::Addr: NodeAddressable + Serialize, { + let value = Vault::new(ResourceContainer::Stealth { + address: STEALTH_TARI_RESOURCE_ADDRESS, + // just under 18.5 trillion tXTR + revealed_amount: u64::MAX.into(), + locked_amount: Default::default(), + }); + + create_substate(tx, num_preshards, XTR_FAUCET_VAULT_ADDRESS, value)?; + let value = ComponentHeader { template_address: tari_template_builtin::XTR_FAUCET_TEMPLATE_ADDRESS, module_name: "XtrFaucet".to_string(), @@ -133,21 +142,12 @@ where body: ComponentBody { state: cbor!({ "vault" => XTR_FAUCET_VAULT_ADDRESS, - "resource_manager" => ResourceManager::get(STEALTH_TARI_RESOURCE_ADDRESS) }) .unwrap(), }, }; create_substate(tx, num_preshards, XTR_FAUCET_COMPONENT_ADDRESS, value)?; - let value = Vault::new(ResourceContainer::Stealth { - address: STEALTH_TARI_RESOURCE_ADDRESS, - // just under 18.5 trillion tXTR - revealed_amount: u64::MAX.into(), - locked_amount: Default::default(), - }); - - create_substate(tx, num_preshards, XTR_FAUCET_VAULT_ADDRESS, value)?; Ok(()) } diff --git a/crates/engine/src/runtime/fee_state.rs b/crates/engine/src/runtime/fee_state.rs index 75f19e6217..de96f2f07d 100644 --- a/crates/engine/src/runtime/fee_state.rs +++ b/crates/engine/src/runtime/fee_state.rs @@ -38,9 +38,9 @@ impl FeeState { }); } - let Some(amount) = resource_container.amount().to_u64_checked() else { + let Some(amount) = resource_container.unlocked_amount().to_u64_checked() else { return Err(RuntimeError::InvalidAmount { - amount: resource_container.amount(), + amount: resource_container.unlocked_amount(), reason: "Payed an invalid amount. Amount must be positive and not overflow".to_string(), }); }; @@ -48,7 +48,7 @@ impl FeeState { Some(new_total) => self.running_total = new_total, None => { return Err(RuntimeError::InvalidAmount { - amount: resource_container.amount(), + amount: resource_container.unlocked_amount(), reason: "Payed an invalid amount. Amount overflowed".to_string(), }); }, diff --git a/crates/engine/src/runtime/impl.rs b/crates/engine/src/runtime/impl.rs index 1e9bccab71..625e72f963 100644 --- a/crates/engine/src/runtime/impl.rs +++ b/crates/engine/src/runtime/impl.rs @@ -55,6 +55,7 @@ use tari_template_lib::{ AddressAllocationInvokeArg, AllocateAddressResult, BucketAction, + BucketGetAmountArg, BucketRef, BuiltinTemplateAction, BurnStealthUtxoArg, @@ -117,6 +118,7 @@ use tari_template_lib::{ engine_args::{SignatureAction, SignatureVerifyArg}, Amount, EntityId, + ResourceInfo, TemplateAddress, }, }; @@ -862,7 +864,7 @@ impl> RuntimeInte Ok(InvokeResult::encode(&total_supply)?) }) }, - ResourceAction::GetResourceType => { + ResourceAction::GetResourceInfo => { let resource_address = resource_ref .as_resource_address() @@ -877,8 +879,12 @@ impl> RuntimeInte let locked = state.read_lock_substate(&SubstateId::Resource(resource_address))?; let resource = state.get_resource(&locked)?; let resource_type = resource.resource_type(); + let divisibility = resource.divisibility(); state.unlock_substate(locked)?; - Ok(InvokeResult::encode(&resource_type)?) + Ok(InvokeResult::encode(&ResourceInfo { + resource_type, + divisibility, + })?) }) }, ResourceAction::Mint => { @@ -918,7 +924,7 @@ impl> RuntimeInte let payload = Metadata::from_iter([ ("resource_type", resource.resource_type().to_string()), - ("amount", resource.amount().to_string()), + ("amount", resource.unlocked_amount().to_string()), ]); self.emit_std_event("resource", "mint", resource_address, payload, state_mut)?; @@ -1375,7 +1381,7 @@ impl> RuntimeInte let resource_type = state.get_resource(&resource_lock)?.resource_type(); let vault_id = state.id_provider()?.new_vault_id()?; let resource = match resource_type { - ResourceType::Fungible => ResourceContainer::fungible(*resource_address, Amount::zero()), + ResourceType::Fungible => ResourceContainer::public_fungible(*resource_address, Amount::zero()), ResourceType::NonFungible => { ResourceContainer::non_fungible(*resource_address, Default::default()) }, @@ -1458,7 +1464,7 @@ impl> RuntimeInte let payload = Metadata::from_iter([ ("resource_address", bucket.resource_address().to_string()), ("resource_type", bucket.resource_type().to_string()), - ("amount", bucket.amount().to_string()), + ("amount", bucket.unlocked_amount().to_string()), ]); self.emit_std_event("vault", "deposit", vault_id, payload, state_mut)?; @@ -1691,7 +1697,7 @@ impl> RuntimeInte container.deposit(revealed)?; } } - if container.amount().is_zero() { + if container.unlocked_amount().is_zero() { return Err(RuntimeError::InvalidArgument { argument: "TakeFeesArg", reason: "Fee payment has zero value".to_string(), @@ -1702,7 +1708,7 @@ impl> RuntimeInte "vault", "pay_fee", vault_id, - Metadata::from_iter([("amount", container.amount().to_string())]), + Metadata::from_iter([("amount", container.unlocked_amount().to_string())]), state_mut, )?; @@ -1934,10 +1940,36 @@ impl> RuntimeInte reason: "GetAmount bucket action requires a bucket id".to_string(), })?; - args.assert_no_args("Bucket::GetAmount")?; + let arg: BucketGetAmountArg = args.assert_one_arg()?; self.tracker.read_with(|state| { let bucket = state.get_bucket(bucket_id)?; - Ok(InvokeResult::encode(&bucket.amount())?) + match arg { + BucketGetAmountArg::AmountOnly => Ok(InvokeResult::encode(&bucket.unlocked_amount())?), + BucketGetAmountArg::LockedOnly => Ok(InvokeResult::encode(&bucket.locked_amount())?), + BucketGetAmountArg::AmountAndLocked => { + let amount = bucket + .unlocked_amount() + .checked_add(bucket.locked_amount()) + .ok_or_else(|| RuntimeError::InvariantError { + function: "BucketAction::GetAmount", + details: "Total amount overflowed".to_string(), + })?; + Ok(InvokeResult::encode(&amount)?) + }, + BucketGetAmountArg::Everything => { + let amount = bucket + .unlocked_amount() + .checked_add(bucket.locked_amount()) + .and_then(|a| { + a.checked_add(Amount::new(bucket.number_of_confidential_commitments().into())) + }) + .ok_or_else(|| RuntimeError::InvariantError { + function: "BucketAction::GetAmount", + details: "Total amount overflowed".to_string(), + })?; + Ok(InvokeResult::encode(&amount)?) + }, + } }) }, BucketAction::Take => { @@ -2028,7 +2060,7 @@ impl> RuntimeInte self.tracker.write_with(|state| { let bucket = state.take_bucket(bucket_id)?; - let burnt_amount = bucket.amount(); + let burnt_amount = bucket.unlocked_amount(); state.burn_bucket(bucket)?; let resource_mut = state.get_resource_mut(&resource_lock)?; @@ -2125,6 +2157,25 @@ impl> RuntimeInte Ok(InvokeResult::encode(&bucket.number_of_confidential_commitments())?) }) }, + BucketAction::DropEmpty => { + let bucket_id = bucket_ref.bucket_id().ok_or_else(|| RuntimeError::InvalidArgument { + argument: "bucket_ref", + reason: "DropEmpty bucket action requires a bucket id".to_string(), + })?; + args.assert_no_args("Bucket::DropEmpty")?; + + self.tracker.write_with(|state| { + let bucket = state.take_bucket(bucket_id)?; + if !bucket.is_empty() { + return Err(RuntimeError::InvalidArgument { + argument: "bucket_ref", + reason: "Cannot drop a non-empty bucket".to_string(), + }); + } + // Drop + Ok(InvokeResult::unit()) + }) + }, } } @@ -2320,10 +2371,10 @@ impl> RuntimeInte } // validate the bucket amount - if bucket.amount() < min_amount { + if bucket.unlocked_amount() < min_amount { return Err(RuntimeError::AssertError(AssertError::InvalidAmount { expected: min_amount, - got: bucket.amount(), + got: bucket.unlocked_amount(), })); } diff --git a/crates/engine/src/runtime/working_state.rs b/crates/engine/src/runtime/working_state.rs index b792de9034..d0fe63288d 100644 --- a/crates/engine/src/runtime/working_state.rs +++ b/crates/engine/src/runtime/working_state.rs @@ -397,10 +397,9 @@ impl WorkingState { } pub(super) fn validate_finalized(&self) -> Result<(), RuntimeError> { - let dangling_bucket_count = self.buckets.iter().filter(|(_, b)| !b.amount().is_zero()).count(); - if dangling_bucket_count > 0 { + if self.buckets.iter().any(|(_, b)| !b.is_empty()) { return Err(TransactionCommitError::DanglingBuckets { - count: dangling_bucket_count, + count: self.buckets.len(), } .into()); } @@ -494,7 +493,7 @@ impl WorkingState { } pub fn burn_bucket(&mut self, bucket: Bucket) -> Result<(), RuntimeError> { - if bucket.amount().is_zero() { + if bucket.unlocked_amount().is_zero() { return Ok(()); } let resource_address = *bucket.resource_address(); @@ -593,7 +592,7 @@ impl WorkingState { "Minting {} fungible tokens on resource: {}", amount, resource_address ); - ResourceContainer::fungible(resource_address, amount) + ResourceContainer::public_fungible(resource_address, amount) }, MintArg::NonFungible { tokens } => { debug!( @@ -654,13 +653,13 @@ impl WorkingState { if is_total_supply_tracking_enabled { let resource_mut = self.get_resource_mut(locked_resource)?; // Increase the total supply of the resource - if !resource_mut.increase_total_supply(resource_container.amount()) { + if !resource_mut.increase_total_supply(resource_container.unlocked_amount()) { return Err(RuntimeError::ResourceSupplyWouldOverflow { resource_address, current_supply: resource_mut .total_supply() .expect("Resource supply tracking is enabled"), - amount: resource_container.amount(), + amount: resource_container.unlocked_amount(), }); } } @@ -1334,15 +1333,18 @@ impl WorkingState { // First collect fees that cannot be refunded (we have to take all fees even if they exceed the required amount) for resx in self.fee_state.non_refundable_fee_payments_mut_iter() { // PANIC: this is checked by FeeState - let paid_amount = resx.amount().to_u64_checked().expect("invalid fee entry in fee state"); + let paid_amount = resx + .unlocked_amount() + .to_u64_checked() + .expect("invalid fee entry in fee state"); debug!( target: LOG_TARGET, - "Collecting {} of non-refundable fees", resx.amount() + "Collecting {} of non-refundable fees", resx.unlocked_amount() ); // If there is no refund vault, we must take the entire amount to avoid destroying funds - fee_resource.deposit(resx.withdraw(resx.amount())?)?; + fee_resource.deposit(resx.withdraw(resx.unlocked_amount())?)?; if remaining_fees < paid_amount { total_fee_overcharge += paid_amount - remaining_fees; } @@ -1358,11 +1360,14 @@ impl WorkingState { debug!( target: LOG_TARGET, - "Collecting {} of refundable fees", resx.amount() + "Collecting {} of refundable fees", resx.unlocked_amount() ); // PANIC: this is checked by FeeState - let paid_amount = resx.amount().to_u64_checked().expect("invalid fee entry in fee state"); + let paid_amount = resx + .unlocked_amount() + .to_u64_checked() + .expect("invalid fee entry in fee state"); // Withdraw only what is needed let amount_to_withdraw = cmp::min(paid_amount, remaining_fees); @@ -1373,14 +1378,14 @@ impl WorkingState { // Refund the remaining refundable payments if any for (mut resx, refund_vault) in self.fee_state.drain_refundable_fee_payments() { - if resx.amount().is_zero() { - debug_assert!(!resx.amount().is_negative()); + if resx.unlocked_amount().is_zero() { + debug_assert!(!resx.unlocked_amount().is_negative()); continue; } debug!( target: LOG_TARGET, - "Refunding {} of fees to vault {}", resx.amount(), refund_vault + "Refunding {} of fees to vault {}", resx.unlocked_amount(), refund_vault ); let vault_mut = substates_to_persist .get_mut(&SubstateId::Vault(refund_vault)) @@ -1391,7 +1396,7 @@ impl WorkingState { } let total_fees_paid = fee_resource - .amount() + .unlocked_amount() .to_u64_checked() .expect("FeeState guarantees that the total fee payments fit in an u64"); @@ -1520,13 +1525,13 @@ impl WorkingState { match revealed_funds_bucket { Some(ref bucket) => { - if bucket.amount() != statement.inputs_statement.revealed_amount { + if bucket.unlocked_amount() != statement.inputs_statement.revealed_amount { return Err(RuntimeError::InvalidArgument { argument: "revealed_funds_bucket", reason: format!( "Revealed funds bucket amount ({}) does not match the statement's revealed input amount \ ({})", - bucket.amount(), + bucket.unlocked_amount(), statement.inputs_statement.revealed_amount ), }); diff --git a/crates/engine/src/transaction/processor.rs b/crates/engine/src/transaction/processor.rs index e28119497d..76120a0f33 100644 --- a/crates/engine/src/transaction/processor.rs +++ b/crates/engine/src/transaction/processor.rs @@ -37,12 +37,12 @@ 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, BucketAction, BucketRef, WorkspaceAction}, + args::{AllocateAddressResult, BucketAction, BucketGetAmountArg, BucketRef, WorkspaceAction}, auth::{ComponentAccessRules, OwnerRule}, invoke_args, models::{Bucket, NonFungibleAddress, StealthTransferStatement}, prelude::STEALTH_TARI_RESOURCE_ADDRESS, - types::{crypto::RistrettoPublicKeyBytes, TemplateAddress}, + types::{crypto::RistrettoPublicKeyBytes, Amount, TemplateAddress}, }; use tari_transaction::{ args::{InstructionArg, WorkspaceId, WorkspaceOffsetId}, @@ -324,11 +324,27 @@ impl + 'static> T 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(), - )?; + + let bucket_ref = BucketRef::Ref(item.decode()?); + let bucket = + runtime + .interface() + .bucket_invoke(bucket_ref, BucketAction::Take, invoke_args![amount].into())?; + let prev_bucket_val = runtime + .interface() + .bucket_invoke( + bucket_ref, + BucketAction::GetAmount, + invoke_args![BucketGetAmountArg::Everything].into(), + )? + .decode::()?; + if prev_bucket_val.is_zero() { + // Drop the bucket to prevent a dangling (empty) bucket + runtime + .interface() + .bucket_invoke(bucket_ref, BucketAction::DropEmpty, invoke_args![].into())?; + } + runtime .interface() .put_on_workspace(output_bucket, IndexedValue::from_value(bucket.into_value()?)?)?; diff --git a/crates/engine/src/wasm/module.rs b/crates/engine/src/wasm/module.rs index 157637f5dd..4ce946948c 100644 --- a/crates/engine/src/wasm/module.rs +++ b/crates/engine/src/wasm/module.rs @@ -54,12 +54,12 @@ use crate::{ pub type MainFunction = TypedFunction<(WasmPtr, u32), WasmPtr>; #[derive(Debug, Clone)] pub struct WasmModule { - code: Vec, + code: Box<[u8]>, } impl WasmModule { - pub fn from_code(code: Vec) -> Self { - Self { code } + pub fn from_code(code: impl Into>) -> Self { + Self { code: code.into() } } pub fn load_template_from_code(code: &[u8]) -> Result { @@ -103,7 +103,7 @@ impl WasmModule { &self.code } - pub fn into_code(self) -> Vec { + pub fn into_code(self) -> Box<[u8]> { self.code } diff --git a/crates/engine/src/wasm/process.rs b/crates/engine/src/wasm/process.rs index 2640e0938d..681ad1b11f 100644 --- a/crates/engine/src/wasm/process.rs +++ b/crates/engine/src/wasm/process.rs @@ -276,12 +276,12 @@ impl WasmProcess { pub fn validate_template_tari_version(template_def: &TemplateDef) -> Result<(), WasmExecutionError> { let template_tari_version = template_def.tari_version(); - if are_versions_compatible(template_tari_version, version::MINIMUM_SUPPORTED_TEMPLATE_LIB_VERSION)? { - log::debug!(target: LOG_TARGET, "The Tari version in the template WASM (\"{}\") is compatible with the one used in the engine", template_tari_version); + if are_versions_compatible(template_tari_version, version::MINIMUM_SUPPORTED_WASM_ABI_VERSION)? { + log::debug!(target: LOG_TARGET, "The WASM ABI version (\"{}\") is compatible with the one used in the engine", template_tari_version); } else { - log::error!(target: LOG_TARGET, "The Tari version in the template WASM (\"{}\") is incompatible with the one used in the engine (\"{}\")", template_tari_version, version::MINIMUM_SUPPORTED_TEMPLATE_LIB_VERSION); + log::error!(target: LOG_TARGET, "The WASM ABI version (\"{}\") is incompatible with the one used in the engine (\"{}\")", template_tari_version, version::MINIMUM_SUPPORTED_WASM_ABI_VERSION); return Err(WasmExecutionError::TemplateVersionMismatch { - engine_version: version::MINIMUM_SUPPORTED_TEMPLATE_LIB_VERSION.to_owned(), + engine_version: version::MINIMUM_SUPPORTED_WASM_ABI_VERSION.to_owned(), template_version: template_tari_version.to_owned(), }); } diff --git a/crates/engine/tests/access_rules.rs b/crates/engine/tests/access_rules.rs index 83f0474e9d..9ff3fc0ce6 100644 --- a/crates/engine/tests/access_rules.rs +++ b/crates/engine/tests/access_rules.rs @@ -787,7 +787,7 @@ mod resource_access_rules { ) .put_last_instruction_output_on_workspace("tokens") .call_method(user_account, "deposit", args![Workspace("tokens")]) - // Deposit before dropping the proof + // Deposit before dropping the proof - this step should error .call_method(user_account, "deposit", args![Workspace("badges")]) .drop_all_proofs_in_workspace() .build_and_seal(&owner_key), diff --git a/crates/engine/tests/account.rs b/crates/engine/tests/account.rs index 94f17edf7e..39e7e2cb55 100644 --- a/crates/engine/tests/account.rs +++ b/crates/engine/tests/account.rs @@ -13,7 +13,7 @@ use tari_template_lib::{ }; use tari_template_test_tooling::{ support::assert_error::{assert_access_denied_for_action, assert_reject_reason}, - test_faucet_component, + xtr_faucet_component, TemplateTest, }; use tari_transaction::{args, call_args, Instruction, Transaction}; @@ -22,27 +22,7 @@ use tari_transaction::{args, call_args, Instruction, Transaction}; fn basic_faucet_transfer() { let mut template_test = TemplateTest::new(Vec::<&str>::new()); - let faucet_template = template_test.get_template_address("TestFaucet"); - - let initial_supply = Amount::from(1_000_000_000_000u64); - let result = template_test - .execute_and_commit( - vec![Instruction::CallFunction { - address: faucet_template, - function: "mint".to_string(), - args: call_args![initial_supply], - }], - vec![template_test.owner_proof()], - ) - .unwrap(); - let faucet_component: ComponentAddress = result.finalize.execution_results[0].decode().unwrap(); - let faucet_resource = result - .finalize - .result - .expect("Faucet mint failed") - .up_iter() - .find_map(|(address, _)| address.as_resource_address()) - .unwrap(); + let (faucet_component, faucet_resource) = template_test.create_test_faucet_component(1_000_000_000_000u64); // Create sender and receiver accounts let (sender_address, sender_proof, _) = template_test.create_funded_account(); @@ -249,7 +229,7 @@ fn custom_access_rules() { let result = template_test.execute_expect_success( Transaction::builder() - .call_method(test_faucet_component(), "take_free_coins", args![]) + .call_method(xtr_faucet_component(), "take", args![1000]) .put_last_instruction_output_on_workspace("bucket") // Create component with the same ID .create_account_with_custom_rules( diff --git a/crates/engine/tests/composability.rs b/crates/engine/tests/composability.rs index 1f11a380ee..b86f875794 100644 --- a/crates/engine/tests/composability.rs +++ b/crates/engine/tests/composability.rs @@ -227,7 +227,7 @@ fn it_allows_method_to_function_calls() { fn it_fails_on_invalid_calls() { let mut test = setup(); let components = initialize_composability(&mut test); - let (_, _, private_key) = test.template_test.create_funded_account(); + let (_, _, private_key) = test.template_test.create_empty_account(); // the "invalid_state_call" method tries to call a non-existent method in the inner state component let result = test @@ -315,7 +315,7 @@ fn it_allows_multiple_recursion_levels() { #[test] fn it_fails_when_surpassing_recursion_limit() { let mut test = setup(); - let (_, _, private_key) = test.template_test.create_funded_account(); + let (_, _, private_key) = test.template_test.create_empty_account(); let max_call_depth = MAX_CALL_DEPTH; // innermost composability component diff --git a/crates/engine/tests/events.rs b/crates/engine/tests/events.rs index 3e7e882dd4..d440436fad 100644 --- a/crates/engine/tests/events.rs +++ b/crates/engine/tests/events.rs @@ -115,8 +115,8 @@ fn builtin_vault_events() { *event.payload().get("resource_address").unwrap(), faucet_resource.to_string() ); - assert_eq!(*event.payload().get("resource_type").unwrap(), "Fungible"); - assert_eq!(*event.payload().get("amount").unwrap(), amount.to_string()); + assert_eq!(event.payload().get("resource_type").unwrap(), "Fungible"); + assert_eq!(event.payload().get("amount").unwrap(), amount.to_string()); // a standard event for the deposit must have been emmitted let event = result @@ -128,9 +128,9 @@ fn builtin_vault_events() { assert_eq!(event.template_address(), ACCOUNT_TEMPLATE_ADDRESS); // assert_eq!(event.component_address().unwrap(), receiver_address); assert_eq!( - *event.payload().get("resource_address").unwrap(), + event.payload().get("resource_address").unwrap(), faucet_resource.to_string() ); - assert_eq!(*event.payload().get("resource_type").unwrap(), "Fungible"); - assert_eq!(*event.payload().get("amount").unwrap(), amount.to_string()); + assert_eq!(event.payload().get("resource_type").unwrap(), "Fungible"); + assert_eq!(event.payload().get("amount").unwrap(), amount.to_string()); } diff --git a/crates/engine/tests/fees.rs b/crates/engine/tests/fees.rs index 779ced007c..9fef6ad50f 100644 --- a/crates/engine/tests/fees.rs +++ b/crates/engine/tests/fees.rs @@ -5,7 +5,7 @@ use std::iter; use tari_engine_types::commit_result::RejectReason; use tari_template_lib::{constants::STEALTH_TARI_RESOURCE_ADDRESS, models::ComponentAddress, types::Amount}; -use tari_template_test_tooling::{support::assert_error::assert_reject_reason, test_faucet_component, TemplateTest}; +use tari_template_test_tooling::{support::assert_error::assert_reject_reason, xtr_faucet_component, TemplateTest}; use tari_transaction::{args, call_args, Transaction}; #[test] @@ -71,12 +71,13 @@ fn deposit_from_faucet_then_pay() { let (account, owner_token, private_key) = test.create_empty_account(); test.enable_fees(); + let result = test.execute_expect_success( Transaction::builder() .with_fee_instructions_builder(|builder| { builder // Faucet deposits free coins into the account - .call_method(test_faucet_component(), "take_free_coins", args![]) + .call_method(xtr_faucet_component(), "take", args![100000]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) .call_method(account, "pay_fee", args![1000]) @@ -90,10 +91,7 @@ fn deposit_from_faucet_then_pay() { let payment = result.finalize.fee_receipt; let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); - assert_eq!( - new_balance, - payment.total_allocated_fee_payments() - payment.total_fees_charged() - ); + assert_eq!(new_balance, 100000 - payment.total_fees_paid()); } #[test] @@ -118,7 +116,7 @@ fn another_account_pays_partially_for_fees() { .fee_transaction_pay_from_component(account_fee, Amount::from(200)) // Account pays the rest .fee_transaction_pay_from_component(account_fee2, Amount::from(1000)) - .call_method(test_faucet_component(), "take_free_coins", args![]) + .call_method(xtr_faucet_component(), "take", args![1000]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) // NOTE: the test harness provides the virtual proofs as provided, so the transaction signer does not matter diff --git a/crates/engine/tests/templates/buggy/src/lib.rs b/crates/engine/tests/templates/buggy/src/lib.rs index 88daddaf0b..6ae8ba1e6e 100644 --- a/crates/engine/tests/templates/buggy/src/lib.rs +++ b/crates/engine/tests/templates/buggy/src/lib.rs @@ -45,7 +45,7 @@ pub static _ABI_TEMPLATE_DEF: [u8; 4] = [0, 0, 0, 0]; #[no_mangle] pub static _ABI_TEMPLATE_DEF: [u8; 60] = [ 56, 0, 0, 0, 161, 98, 86, 49, 163, 109, 116, 101, 109, 112, 108, 97, 116, 101, 95, 110, 97, 109, 101, 101, 66, 117, - 103, 103, 121, 108, 116, 97, 114, 105, 95, 118, 101, 114, 115, 105, 111, 110, 102, 48, 46, 49, 52, 46, 48, 105, + 103, 103, 121, 108, 116, 97, 114, 105, 95, 118, 101, 114, 115, 105, 111, 110, 102, 48, 46, 49, 53, 46, 48, 105, 102, 117, 110, 99, 116, 105, 111, 110, 115, 128, ]; diff --git a/crates/engine/tests/test.rs b/crates/engine/tests/test.rs index 4943d4d5db..f90024d510 100644 --- a/crates/engine/tests/test.rs +++ b/crates/engine/tests/test.rs @@ -34,8 +34,9 @@ use tari_engine_types::{ use tari_ootle_common_types::substate_type::SubstateType; use tari_template_builtin::{ACCOUNT_TEMPLATE_ADDRESS, NFT_FAUCET_TEMPLATE_ADDRESS}; use tari_template_lib::{ + constants::XTR, models::{ComponentAddress, NonFungible, NonFungibleAddress, ResourceAddress}, - types::{crypto::RistrettoPublicKeyBytes, TemplateAddress}, + types::{crypto::RistrettoPublicKeyBytes, Amount, TemplateAddress}, }; use tari_template_test_tooling::{support::assert_error::assert_reject_reason, TemplateTest}; use tari_transaction::{args, call_args, Transaction}; @@ -152,7 +153,7 @@ fn test_buggy_template() { // Uncomment the following lines to print the ABI bytes // let bytes = tari_bor::encode_with_len(&tari_template_abi::TemplateDef::V1(tari_template_abi::TemplateDefV1 { // template_name: "Buggy".to_string(), - // tari_version: tari_template_abi::version::MINIMUM_SUPPORTED_TEMPLATE_LIB_VERSION, + // tari_version: tari_template_abi::version::MINIMUM_SUPPORTED_WASM_ABI_VERSION.to_string(), // functions: vec![], // })); // println!("pub static _ABI_TEMPLATE_DEF: [u8; {}] = [", bytes.len()); @@ -1024,11 +1025,6 @@ mod emoji_id { } mod tickets { - use tari_template_lib::{ - constants::{XTR, XTR_FAUCET_COMPONENT_ADDRESS}, - types::Amount, - }; - use super::*; #[test] @@ -1063,16 +1059,6 @@ mod tickets { let total_supply: Amount = test.call_method(ticket_seller, "total_supply", args![], vec![]); assert_eq!(total_supply, initial_supply); - // get some funds into the account - test.execute_expect_success( - Transaction::builder() - .call_method(XTR_FAUCET_COMPONENT_ADDRESS, "take_free_coins", args![]) - .put_last_instruction_output_on_workspace("coins") - .call_method(account_address, "deposit", args![Workspace("coins")]) - .build_and_seal(&secret), - vec![], - ); - // buy a ticket test.execute_expect_success( Transaction::builder() diff --git a/crates/engine_types/src/bucket.rs b/crates/engine_types/src/bucket.rs index 43b47af65e..316a4543e5 100644 --- a/crates/engine_types/src/bucket.rs +++ b/crates/engine_types/src/bucket.rs @@ -48,8 +48,12 @@ impl Bucket { } } - pub fn amount(&self) -> Amount { - self.resource_container.amount() + pub fn is_empty(&self) -> bool { + self.resource_container.is_empty() + } + + pub fn unlocked_amount(&self) -> Amount { + self.resource_container.unlocked_amount() } pub fn number_of_confidential_commitments(&self) -> usize { diff --git a/crates/engine_types/src/commit_result.rs b/crates/engine_types/src/commit_result.rs index 77b5dc2a51..a581e7e490 100644 --- a/crates/engine_types/src/commit_result.rs +++ b/crates/engine_types/src/commit_result.rs @@ -59,12 +59,14 @@ impl ExecuteResult { } /// Returns the ExecuteResult if successful, or panic if the transaction was not successful. + #[track_caller] pub fn unwrap_success(self) -> Self { self.expect_success(); self } /// Returns the SubstateDiff if the transaction was successful, or panic if the transaction was not successful. + #[track_caller] pub fn expect_success(&self) -> &SubstateDiff { let diff = self.expect_finalization_success(); @@ -76,6 +78,7 @@ impl ExecuteResult { } /// Returns the RejectReason if the transaction failed, or panic if the transaction was successful. + #[track_caller] pub fn expect_failure(&self) -> &RejectReason { if let Some(reason) = self.finalize.result.any_reject() { reason @@ -84,6 +87,7 @@ impl ExecuteResult { } } + #[track_caller] pub fn expect_finalization_failure(&self) -> &RejectReason { match self.finalize.result { TransactionResult::Accept(_) => panic!("Expected transaction to fail but it succeeded"), @@ -92,6 +96,7 @@ impl ExecuteResult { } } + #[track_caller] pub fn expect_fee_accept_transaction_reject(&self) -> (&SubstateDiff, &RejectReason) { match self.finalize.result { TransactionResult::AcceptFeeRejectRest(ref diff, ref reason) => (diff, reason), @@ -102,6 +107,7 @@ impl ExecuteResult { } } + #[track_caller] pub fn expect_transaction_failure(&self) -> &RejectReason { if let Some(reason) = self.finalize.any_reject() { reason @@ -110,6 +116,7 @@ impl ExecuteResult { } } + #[track_caller] pub fn expect_finalization_success(&self) -> &SubstateDiff { match self.finalize.result { TransactionResult::Accept(ref diff) => diff, @@ -118,12 +125,14 @@ impl ExecuteResult { } } + #[track_caller] pub fn expect_fees_paid_in_full(&self) -> &FeeReceipt { let receipt = &self.finalize.fee_receipt; assert!(receipt.is_paid_in_full(), "Fees not paid in full"); receipt } + #[track_caller] pub fn expect_return(&self, index: usize) -> T { self.finalize .execution_results diff --git a/crates/engine_types/src/events.rs b/crates/engine_types/src/events.rs index b666dd4bbb..2250e4a731 100644 --- a/crates/engine_types/src/events.rs +++ b/crates/engine_types/src/events.rs @@ -113,7 +113,7 @@ impl Event { } pub fn get_payload(&self, key: &str) -> Option<&str> { - self.payload.get(key).map(|s| s.as_str()) + self.payload.get(key) } pub fn payload(&self) -> &Metadata { diff --git a/crates/engine_types/src/proof.rs b/crates/engine_types/src/proof.rs index 6f912b237e..be8e5c6cda 100644 --- a/crates/engine_types/src/proof.rs +++ b/crates/engine_types/src/proof.rs @@ -82,7 +82,7 @@ impl LockedResource { } pub fn amount(&self) -> Amount { - self.locked.amount() + self.locked.unlocked_amount() } pub fn resource_address(&self) -> &ResourceAddress { diff --git a/crates/engine_types/src/resource.rs b/crates/engine_types/src/resource.rs index 60da6f0d5a..818f0f93e1 100644 --- a/crates/engine_types/src/resource.rs +++ b/crates/engine_types/src/resource.rs @@ -205,7 +205,7 @@ impl Resource { } pub fn token_symbol(&self) -> Option<&str> { - self.metadata.get(TOKEN_SYMBOL).map(|s| s.as_str()) + self.metadata.get(TOKEN_SYMBOL) } pub fn divisibility(&self) -> u8 { diff --git a/crates/engine_types/src/resource_container.rs b/crates/engine_types/src/resource_container.rs index abee2eb87e..8dad58ef18 100644 --- a/crates/engine_types/src/resource_container.rs +++ b/crates/engine_types/src/resource_container.rs @@ -52,7 +52,7 @@ pub enum ResourceContainer { } impl ResourceContainer { - pub fn fungible>(address: ResourceAddress, amount: T) -> Self { + pub fn public_fungible>(address: ResourceAddress, amount: T) -> Self { let amount = amount.into(); assert!(amount.is_non_negative(), "amount must be non-negative"); Self::Fungible { @@ -131,7 +131,7 @@ impl ResourceContainer { }) } - pub fn amount(&self) -> Amount { + pub fn unlocked_amount(&self) -> Amount { match self { Self::Fungible { amount, .. } => *amount, Self::NonFungible { token_ids, .. } => Amount::new(token_ids.len().into()), @@ -286,9 +286,9 @@ impl ResourceContainer { } pub fn withdraw(&mut self, withdraw_amt: Amount) -> Result { - if !withdraw_amt.is_positive() { + if !withdraw_amt.is_non_negative() { return Err(ResourceError::InvariantError(format!( - "Amount must be positive (greater than 0). Got :{withdraw_amt}" + "Amount must be non-negative (>= 0). Got :{withdraw_amt}" ))); } match self { @@ -302,7 +302,7 @@ impl ResourceContainer { }); } *amount -= withdraw_amt; - Ok(Self::fungible(*self.resource_address(), withdraw_amt)) + Ok(Self::public_fungible(*self.resource_address(), withdraw_amt)) }, Self::NonFungible { token_ids, .. } => { if withdraw_amt > token_ids.len() { @@ -355,7 +355,9 @@ impl ResourceContainer { pub fn withdraw_all(&mut self) -> Result { match self { - Self::Fungible { .. } | Self::NonFungible { .. } | Self::Stealth { .. } => self.withdraw(self.amount()), + Self::Fungible { .. } | Self::NonFungible { .. } | Self::Stealth { .. } => { + self.withdraw(self.unlocked_amount()) + }, Self::Confidential { commitments, revealed_amount, @@ -565,7 +567,7 @@ impl ResourceContainer { // Sets to zero and returns the amount let newly_locked_amount = mem::take(amount); *locked_amount += newly_locked_amount; - Ok(Self::fungible(resource_address, newly_locked_amount)) + Ok(Self::public_fungible(resource_address, newly_locked_amount)) }, Self::NonFungible { token_ids, @@ -619,7 +621,7 @@ impl ResourceContainer { // Sets to zero and returns the amount let newly_locked_amount = mem::take(revealed_amount); *locked_amount += newly_locked_amount; - Ok(Self::fungible(resource_address, newly_locked_amount)) + Ok(Self::public_fungible(resource_address, newly_locked_amount)) }, } } @@ -644,18 +646,18 @@ impl ResourceContainer { Self::Fungible { amount, locked_amount, .. } => { - if *locked_amount < container.amount() { + if *locked_amount < container.unlocked_amount() { return Err(ResourceError::InsufficientBalance { details: format!( "unlock: resource container did not contain enough locked funds. Required: {}, Available: \ {}", - container.amount(), + container.unlocked_amount(), locked_amount ), }); } - *amount += container.amount(); - *locked_amount -= container.amount(); + *amount += container.unlocked_amount(); + *locked_amount -= container.unlocked_amount(); }, Self::NonFungible { token_ids, @@ -699,11 +701,11 @@ impl ResourceContainer { }); } - if *locked_revealed_amount < container.amount() { + if *locked_revealed_amount < container.unlocked_amount() { return Err(ResourceError::InvariantError(format!( "unlock: resource container did not contain enough locked revealed amount. Required: {}, \ Available: {}", - container.amount(), + container.unlocked_amount(), locked_revealed_amount ))); } @@ -720,26 +722,26 @@ impl ResourceContainer { )); } } - *revealed_amount += container.amount(); - *locked_revealed_amount -= container.amount(); + *revealed_amount += container.unlocked_amount(); + *locked_revealed_amount -= container.unlocked_amount(); }, Self::Stealth { revealed_amount, locked_amount, .. } => { - if *locked_amount < container.amount() { + if *locked_amount < container.unlocked_amount() { return Err(ResourceError::InsufficientBalance { details: format!( "unlock: resource container did not contain enough locked funds. Required: {}, Available: \ {}", - container.amount(), + container.unlocked_amount(), locked_amount ), }); } - *revealed_amount += container.amount(); - *locked_amount -= container.amount(); + *revealed_amount += container.unlocked_amount(); + *locked_amount -= container.unlocked_amount(); }, } @@ -794,7 +796,7 @@ impl ResourceContainer { } *available_amount -= amount; *locked_amount += amount; - Ok(Self::fungible(*self.resource_address(), amount)) + Ok(Self::public_fungible(*self.resource_address(), amount)) }, Self::NonFungible { token_ids, @@ -862,6 +864,12 @@ impl ResourceContainer { }, } } + + pub fn is_empty(&self) -> bool { + self.locked_amount().is_zero() && + self.unlocked_amount().is_zero() && + self.number_of_confidential_commitments() == 0 + } } #[derive(Debug, thiserror::Error)] diff --git a/crates/engine_types/src/vault.rs b/crates/engine_types/src/vault.rs index 45f739ed7f..01204f5b5b 100644 --- a/crates/engine_types/src/vault.rs +++ b/crates/engine_types/src/vault.rs @@ -103,7 +103,7 @@ impl Vault { } pub fn balance(&self) -> Amount { - self.resource_container.amount() + self.resource_container.unlocked_amount() } pub fn locked_balance(&self) -> Amount { diff --git a/crates/template_abi/src/version.rs b/crates/template_abi/src/version.rs index 41699ee823..d373115ae7 100644 --- a/crates/template_abi/src/version.rs +++ b/crates/template_abi/src/version.rs @@ -2,6 +2,6 @@ // SPDX-License-Identifier: BSD-3-Clause /// The version that will be compiled into WASM templates (those using template_macros). -pub const LATEST_TEMPLATE_VERSION: &str = "0.14.0"; -/// The minimum supported version of the template_lib crate in the WASM. -pub const MINIMUM_SUPPORTED_TEMPLATE_LIB_VERSION: &str = "0.14.0"; +pub const LATEST_TEMPLATE_VERSION: &str = "0.15.0"; +/// The minimum supported version ABI version +pub const MINIMUM_SUPPORTED_WASM_ABI_VERSION: &str = "0.15.0"; diff --git a/crates/template_builtin/build.rs b/crates/template_builtin/build.rs index 099dc473f7..106460e0e3 100644 --- a/crates/template_builtin/build.rs +++ b/crates/template_builtin/build.rs @@ -10,7 +10,12 @@ use std::{ process::Command, }; -const TEMPLATE_BUILTINS: &[&str] = &["templates/account", "templates/nft_faucet", "templates/faucet"]; +const TEMPLATE_BUILTINS: &[&str] = &[ + "templates/account", + "templates/nft_faucet", + "templates/faucet", + "templates/liquidity_pool", +]; fn main() -> Result<(), Box> { // Rebuild templates if abi or lib changes (only if they exist in the build context) diff --git a/crates/template_builtin/src/lib.rs b/crates/template_builtin/src/lib.rs index 11e4c1cbf0..1b9a344461 100644 --- a/crates/template_builtin/src/lib.rs +++ b/crates/template_builtin/src/lib.rs @@ -36,6 +36,10 @@ pub const XTR_FAUCET_TEMPLATE_ADDRESS: TemplateAddress = TemplateAddress::from_a 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); +// pub const LIQUIDITY_POOL_TEMPLATE_ADDRESS: TemplateAddress = TemplateAddress::from_array([ +// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, +// ]); + pub fn get_template_builtin(address: &TemplateAddress) -> &'static [u8] { try_get_template_builtin(address).unwrap_or_else(|| panic!("Unknown builtin template address {address}")) } @@ -58,6 +62,11 @@ pub fn all_builtin_templates() -> impl Iterator, + ) -> Component { + // check that the resource pair is correct + assert_ne!(a_addr, b_addr, "The resources of the pair must be different"); + Self::check_resource_is_fungible(a_addr); + Self::check_resource_is_fungible(b_addr); + + // Set token symbol to "LP" if not provided + metadata.get_or_insert(TOKEN_SYMBOL, "LP"); + // create the lp resource + let lp_resource = ResourceBuilder::public_fungible() + .with_divisibility(0) + .with_access_rules( + ResourceAccessRules::new() + .mintable(pool_token_rules.clone()) + .burnable(pool_token_rules.clone()), + ) + .with_owner_rule(owner_rule.clone()) + .with_metadata(metadata) + .build(); + + let vault_a = Vault::new_empty(a_addr); + let vault_b = Vault::new_empty(b_addr); + + Component::new(Self { + vault_a, + vault_b, + lp_resource: ResourceManager::get(lp_resource), + }) + .with_owner_rule(owner_rule) + .with_address_allocation_opt(address_allocation) + .with_access_rules( + ComponentAccessRules::new() + .add_method_rule("add_liquidity", pool_token_rules.clone()) + .add_method_rule("remove_liquidity", pool_token_rules.clone()) + .add_method_rule("contribute", pool_token_rules) + .default(AccessRule::AllowAll), + ) + .create() + } + + pub fn contribute(&mut self, mut bucket_a: Bucket, mut bucket_b: Bucket) -> Bucket { + // Potentially saves binary space + const OVERFLOW_MSG: &str = "Overflow when calculating LP token mint amount"; + + // check that the buckets are correct + let resource_a = bucket_a.resource_address(); + let resource_b = bucket_b.resource_address(); + assert_ne!(resource_a, resource_b, "The resources must be different"); + + // contribution amounts + let a_amount = bucket_a.amount(); + let b_amount = bucket_b.amount(); + + if a_amount.is_zero() || b_amount.is_zero() { + panic!("Cannot contribute zero amount of liquidity"); + } + + let pool_a = self.get_pool_from_resource(resource_a); + let pool_b = self.get_pool_from_resource(resource_b); + + // Get the reserves for each pool + let reserve_a = self.get_pool_vault(pool_a).balance(); + let reserve_b = self.get_pool_vault(pool_b).balance(); + + let lp_supply = self.lp_total_supply(); + + let (lp_mint_amount, a_contribution, b_contribution) = match ( + lp_supply.is_positive(), + reserve_a.is_positive(), + reserve_b.is_positive(), + ) { + // Reserve pools are empty, mint initial lp tokens + (false, false, false) => { + // initial liquidity provision, mint lp tokens equal to the geometric mean `sqrt(c_1 * c_2)` of the + // contributions + + ( + // TODO: lost of precision + a_amount + .checked_mul(b_amount) + .and_then(|c1_c2| c1_c2.checked_sqrt()) + .expect(OVERFLOW_MSG), + a_amount, + b_amount, + ) + }, + (false, _, _) => { + // No LP tokens currently exist but reserves are non-zero + // Calculate the geometric mean of the ratios of the contributions to the reserves + // i.e. sqrt(c_1 + r_1) * sqrt(c_2 + r_2) + + // TODO: lost of precision + let mint = a_amount + .checked_add(reserve_a) + .and_then(|c1_r1| c1_r1.checked_sqrt()) + .and_then(|sqrt_c1_r1| { + b_amount + .checked_add(reserve_b) + .and_then(|c2_r2| c2_r2.checked_sqrt()) + .and_then(|sqrt_c2_r2| sqrt_c1_r1.checked_mul(sqrt_c2_r2)) + }) + .expect(OVERFLOW_MSG); + + (mint, a_amount, b_amount) + }, + (true, true, true) => { + // Normal case: existing LP supply and non-zero reserves + // calculate the amount each contribution can be contributed to keep the ratio of the reserves the + // same + + let required_contribution_a = a_amount + .checked_div(reserve_a) + .and_then(|r| r.checked_mul(reserve_b)) + .map(|b_required| (a_amount, b_required)) + .expect(OVERFLOW_MSG); + + let required_contribution_b = b_amount + .checked_div(reserve_b) + .and_then(|r| r.checked_mul(reserve_a)) + .map(|a_required| (a_required, b_amount)) + .expect(OVERFLOW_MSG); + + [required_contribution_a, required_contribution_b] + .into_iter() + // filter only the contributions that are less than or equal to what the user provided + .filter(|(c_a, c_b)| *c_a <= a_amount && *c_b <= b_amount) + .map(|(c_a, c_b)| { + let mint = c_a + .checked_div(reserve_a) + .and_then(|r| r.checked_mul(lp_supply)) + .expect(OVERFLOW_MSG); + (mint, c_a, c_b) + }) + .max_by(|(mint_a, _, _), (mint_b, _, _)| mint_a.cmp(mint_b)) + .expect("Insufficient contribution amounts for existing pool reserves") + }, + (true, _, _) => { + // One or both reserves are zero with existing LP supply. LP Supply should always represent + // reserves, so this is an inconsistent state. + panic!("Inconsistent pool state: non-zero LP supply with zero reserve"); + }, + }; + + // mint and return the new lp tokens + let mint_lp_tokens = self.lp_resource.mint_fungible(lp_mint_amount); + + let contributed_a = bucket_a.take(a_contribution); + let contributed_b = bucket_b.take(b_contribution); + + // deposit the liquidity to the pool + self.vault_a.deposit(contributed_a); + self.vault_b.deposit(contributed_b); + + emit_event("contribute", metadata![ + "resource_a" => resource_a.to_string(), + "resource_b" => resource_b.to_string(), + "minted_lp_tokens" => lp_mint_amount.to_string(), + "contributed_a" => a_contribution.to_string(), + "contributed_b" => b_contribution.to_string(), + ]); + + mint_lp_tokens + } + + pub fn redeem(&mut self, lp_bucket: Bucket) -> (Bucket, Bucket) { + // check that the bucket is correct + let lp_resource = self.lp_resource.resource_address(); + let bucket_resource = lp_bucket.resource_address(); + if bucket_resource != lp_resource { + panic!( + "The provided bucket {} must be pool LP resource {}", + bucket_resource, lp_resource + ); + } + + let redeem_amount = lp_bucket.amount(); + let lp_total_supply = self.lp_total_supply(); + + let (a_amount, b_amount) = self.calculate_redemption_amounts(lp_total_supply, redeem_amount); + + // withdraw the redeemed amounts from the pool + let a_bucket = self.vault_a.withdraw(a_amount); + let b_bucket = self.vault_b.withdraw(b_amount); + + // burn the redeemed lp tokens + lp_bucket.burn(); + + emit_event("redeem", [ + ("lp_redeemed", redeem_amount.to_string()), + ("resource_a", a_bucket.resource_address().to_string()), + ("amount_a", a_amount.to_string()), + ("resource_b", b_bucket.resource_address().to_string()), + ("amount_b", b_amount.to_string()), + ]); + + (a_bucket, b_bucket) + } + + pub fn add_liquidity(&mut self, bucket: Bucket) { + // check that the buckets are correct + let resource = bucket.resource_address(); + emit_event("add_liquidity", [ + ("resource_address", resource.to_string()), + ("amount", bucket.amount().to_string()), + ]); + + // add the liquidity to the pool + let pool = self.get_pool_from_resource(resource); + self.get_pool_vault(pool).deposit(bucket); + } + + pub fn remove_liquidity(&mut self, resource_address: ResourceAddress, amount: Amount) -> Bucket { + let pool = self.get_pool_from_resource(resource_address); + emit_event("remove_liquidity", [ + ("resource_address", resource_address.to_string()), + ("amount", amount.to_string()), + ]); + + self.get_pool_vault(pool).withdraw(amount) + } + + pub fn get_redemption_value(&self, lp_redeem_amount: Amount) -> (Amount, Amount) { + let lp_total_supply = self.lp_total_supply(); + + if !lp_redeem_amount.is_positive() || lp_redeem_amount > lp_total_supply { + panic!( + "Invalid redemption amount {} (total supply {})", + lp_redeem_amount, lp_total_supply + ); + } + + self.calculate_redemption_amounts(lp_total_supply, lp_redeem_amount) + } + + fn calculate_redemption_amounts(&self, lp_total_supply: Amount, lp_redeem_amount: Amount) -> (Amount, Amount) { + // get the pool reserve + let a_reserve = self.vault_a.balance(); + let b_reserve = self.vault_b.balance(); + + // calculate the amounts owed to the user based on provided LP tokens + let lp_ratio = lp_redeem_amount.checked_div(lp_total_supply).expect("Div zero"); + let a_amount_owed = lp_ratio + .checked_mul(a_reserve) + .expect("Amount overflow when calculating redemption value for resource A"); + + let b_amount_owed = lp_ratio + .checked_mul(b_reserve) + .expect("Amount overflow when calculating redemption value for resource B"); + + (a_amount_owed, b_amount_owed) + } + + pub fn get_pool_balances(&self) -> (Amount, Amount) { + (self.vault_a.balance(), self.vault_b.balance()) + } + + fn get_pool_from_resource(&self, resource_address: ResourceAddress) -> Pool { + if self.vault_a.resource_address() == resource_address { + Pool::A + } else if self.vault_b.resource_address() == resource_address { + Pool::B + } else { + panic!("Resource {} is not in the pool", resource_address); + } + } + + fn get_pool_vault(&self, pool: Pool) -> &Vault { + match pool { + Pool::A => &self.vault_a, + Pool::B => &self.vault_b, + } + } + + fn lp_total_supply(&self) -> Amount { + self.lp_resource.total_supply() + } + + fn check_resource_is_fungible(resource: ResourceAddress) { + let resource_type = ResourceManager::get(resource).resource_type(); + assert!( + matches!( + resource_type, + ResourceType::Fungible | ResourceType::Confidential | ResourceType::Stealth + ), + "Resource {} is not fungible (Fungible, Stealth, Confidential)", + resource + ); + } + } +} diff --git a/crates/template_builtin/tests/liquidity_pool.rs b/crates/template_builtin/tests/liquidity_pool.rs new file mode 100644 index 0000000000..3a56a126bf --- /dev/null +++ b/crates/template_builtin/tests/liquidity_pool.rs @@ -0,0 +1,122 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{collections::HashMap, path::Path}; + +use tari_template_lib::{ + auth::{AccessRule, OwnerRule}, + constants::XTR, + metadata, +}; +use tari_template_test_tooling::{xtr_faucet_component, TemplateTest}; +use tari_transaction::{args, Transaction}; + +#[test] +fn initial_contribution_and_redeem() { + // TODO: once builtin, we can remove this code + let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("templates") + .join("liquidity_pool"); + let mut test = TemplateTest::new([crate_dir]); + + // create a user account + let (account_address, owner_token, owner_secret) = test.create_empty_account(); + + // Create another resource + let (faucet_component, faucet_resx) = test.create_test_faucet_component(100000); + + let template_addr = test.get_template_address("TwoResourceLiquidityPool"); + + // Fund + test.execute_expect_success( + Transaction::builder() + // Create the liquidity pool + .allocate_component_address("pool") + .call_function(template_addr, "instantiate", args![ + OwnerRule::default(), + AccessRule::AllowAll, + XTR, + faucet_resx, + metadata!["name" => "TestPool".to_string()], + Workspace("pool") + ]) + // Fund the pool + .call_method(faucet_component, "take_free_coins_custom", args![10000]) + .put_last_instruction_output_on_workspace("faucet_coins") + .call_method(xtr_faucet_component(), "take", args![5000]) + .put_last_instruction_output_on_workspace("xtr_coins") + .call_method( + "pool", + "contribute", + args![Workspace("xtr_coins"), Workspace("faucet_coins")], + ) + .put_last_instruction_output_on_workspace("pool_tokens") + // Deposit pool tokens to user account + .call_method( + account_address, + "deposit", + args![Workspace("pool_tokens")], + ) + .build_and_seal(test.secret_key()), + vec![], + ); + + let store = test.read_only_state_store(); + let vaults = store.get_vaults_for_account(account_address).unwrap(); + let vaults = vaults + .into_iter() + .map(|(addr, vault)| { + let resource = store.get_resource(&addr).unwrap(); + (resource.token_symbol().unwrap().to_string(), vault) + }) + .collect::>(); + + // Note that the rounded down token amount is expected here - TODO: fix precision loss + let expected_balance = (10000u64 * 5000).isqrt(); + let lp_vault = vaults.get("LP").unwrap(); + assert_eq!(lp_vault.balance(), expected_balance); + + // Redeem some liquidity + let (pool_component, _) = store + .get_components_by_template_address(template_addr) + .unwrap() + .pop() + .unwrap(); + + drop(store); + + test.execute_expect_success( + Transaction::builder() + .call_method(account_address, "withdraw", args![lp_vault.resource_address(), expected_balance]) + .put_last_instruction_output_on_workspace("pool_tokens") + .call_method( + pool_component, + "redeem", + args![Workspace("pool_tokens")], + ) + .put_last_instruction_output_on_workspace("redeemed_coins") + // Deposit redeemed coins to user account + .call_method(account_address, "deposit", args![Workspace("redeemed_coins.0")]) + .call_method(account_address, "deposit", args![Workspace("redeemed_coins.1")]) + .build_and_seal(&owner_secret), + vec![owner_token], + ); + + let store = test.read_only_state_store(); + let vaults = store.get_vaults_for_account(account_address).unwrap(); + let vaults = vaults + .into_iter() + .map(|(addr, vault)| { + let resource = store.get_resource(&addr).unwrap(); + (resource.token_symbol().unwrap().to_string(), vault) + }) + .collect::>(); + + // Assert that we redeemed all XTR and faucet tokens back for the LP tokens + let vault = vaults.get("tXTR").unwrap(); + assert_eq!(vault.balance(), 5000); + let vault = vaults.get("faucets").unwrap(); + assert_eq!(vault.balance(), 10000); + let vault = vaults.get("LP").unwrap(); + assert_eq!(vault.balance(), 0); +} diff --git a/crates/template_lib/Cargo.toml b/crates/template_lib/Cargo.toml index af9693322f..92a84121eb 100644 --- a/crates/template_lib/Cargo.toml +++ b/crates/template_lib/Cargo.toml @@ -27,3 +27,4 @@ std = ["serde/std", "tari_bor/std", "tari_template_abi/std", "tari_template_lib_ alloc = ["serde/alloc", "tari_bor/alloc", "tari_template_abi/alloc"] ts = ["ts-rs", "tari_bor/ts", "tari_template_abi/ts", "tari_template_lib_types/ts"] borsh = ["dep:borsh", "tari_template_lib_types/borsh", "tari_bor/borsh"] +extra-arith = ["tari_template_lib_types/extra-arith"] diff --git a/crates/template_lib/src/args/types.rs b/crates/template_lib/src/args/types.rs index 23db2d1ff4..58b0930b03 100644 --- a/crates/template_lib/src/args/types.rs +++ b/crates/template_lib/src/args/types.rs @@ -229,8 +229,8 @@ pub enum ResourceAction { UpdateNonFungibleData, /// Get the total supply of a resource GetTotalSupply, - /// Get the [ResourceType] of a resource - GetResourceType, + /// Get the [ResourceInfo] of a resource + GetResourceInfo, /// Gets a non-fungible resource by its ID GetNonFungible, /// Update the access rules of a resource @@ -483,6 +483,7 @@ pub enum BucketAction { GetNonFungibleIds, GetNonFungibles, CountConfidentialCommitments, + DropEmpty, } /// A bucket burn operation argument @@ -491,6 +492,15 @@ pub struct BucketBurnArg { pub bucket_id: BucketId, } +/// BucketAction::GetAmount argument +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum BucketGetAmountArg { + AmountOnly, + LockedOnly, + AmountAndLocked, + Everything, +} + // -------------------------------- Workspace -------------------------------- // /// The possible actions that can be performed on workspace variables diff --git a/crates/template_lib/src/models/account.rs b/crates/template_lib/src/models/account.rs index 99e9d99739..1efca11144 100644 --- a/crates/template_lib/src/models/account.rs +++ b/crates/template_lib/src/models/account.rs @@ -34,4 +34,9 @@ impl Account { pub fn get_vault_by_resource(&self, resource_address: &ResourceAddress) -> Option<&Vault> { self.vaults.get(resource_address) } + + /// Returns an iterator over all resource addresses in the account. + pub fn all_resources_iter(&self) -> impl Iterator { + self.vaults.keys() + } } diff --git a/crates/template_lib/src/models/bucket.rs b/crates/template_lib/src/models/bucket.rs index b8043a67f8..685467911c 100644 --- a/crates/template_lib/src/models/bucket.rs +++ b/crates/template_lib/src/models/bucket.rs @@ -34,7 +34,7 @@ use super::{ StealthTransferStatement, }; use crate::{ - args::{BucketAction, BucketInvokeArg, BucketRef, InvokeResult}, + args::{BucketAction, BucketGetAmountArg, BucketInvokeArg, BucketRef, InvokeResult}, resource::ResourceManager, types::{Amount, ResourceType}, }; @@ -169,14 +169,58 @@ impl Bucket { resp.decode().expect("Bucket join returned invalid result") } + /// Drops the bucket if it is empty. Panics if the bucket is not empty. + /// This must be called if all funds have been taken out of the bucket to prevent a dangling bucket error. + pub fn drop_empty(self) { + let resp: InvokeResult = call_engine(EngineOp::BucketInvoke, &BucketInvokeArg { + bucket_ref: BucketRef::Ref(self.id), + action: BucketAction::DropEmpty, + args: invoke_args![], + }); + + resp.decode().expect("Bucket DropEmpty returned invalid result") + } + + /// Returns true if the bucket is empty (i.e. contains zero tokens), otherwise false. + pub fn is_empty(&self) -> bool { + let resp: InvokeResult = call_engine(EngineOp::BucketInvoke, &BucketInvokeArg { + bucket_ref: BucketRef::Ref(self.id), + action: BucketAction::GetAmount, + args: invoke_args![BucketGetAmountArg::Everything], + }); + + let amount: Amount = resp.decode().expect("Bucket GetAmount returned invalid amount"); + amount.is_zero() + } + /// Returns the amount of tokens held in this bucket. /// + /// Fungible: Returns the number of fungible tokens in the bucket excluding locked amounts. + /// Non-Fungible: Returns the number of NFT tokens for non-fungible resources. + /// Confidential: Returns the number of revealed tokens in the bucket excluding locked amounts and confidential + /// commitments. Stealth: Returns the number of revealed tokens in the bucket excluding locked amounts. Stealth + /// UTXOs are not able to be contained in buckets, and so are not counted. + /// + /// /// Note that if the resource is confidential, only the revealed amount is returned. pub fn amount(&self) -> Amount { let resp: InvokeResult = call_engine(EngineOp::BucketInvoke, &BucketInvokeArg { bucket_ref: BucketRef::Ref(self.id), action: BucketAction::GetAmount, - args: invoke_args![], + args: invoke_args![BucketGetAmountArg::AmountOnly], + }); + + resp.decode().expect("Bucket GetAmount returned invalid amount") + } + + /// Returns the amount of tokens held in this bucket excluding unlocked amounts. + /// + /// Note that if the resource is confidential, only the revealed amount is returned. + pub fn locked_amount(&self) -> Amount { + let resp: InvokeResult = call_engine(EngineOp::BucketInvoke, &BucketInvokeArg { + bucket_ref: BucketRef::Ref(self.id), + action: BucketAction::GetAmount, + args: invoke_args![BucketGetAmountArg::LockedOnly], }); resp.decode().expect("Bucket GetAmount returned invalid amount") diff --git a/crates/template_lib/src/models/metadata.rs b/crates/template_lib/src/models/metadata.rs index dd1f83f630..f374aa98ff 100644 --- a/crates/template_lib/src/models/metadata.rs +++ b/crates/template_lib/src/models/metadata.rs @@ -46,8 +46,17 @@ impl Metadata { self } - pub fn get(&self, key: &str) -> Option<&String> { - self.0.get(key) + pub fn get(&self, key: &str) -> Option<&str> { + self.0.get(key).map(|s| s.as_str()) + } + + pub fn get_or_insert, V: Into>(&mut self, key: K, default: V) -> &str { + let key = key.into(); + self.0.entry(key).or_insert_with(|| default.into()) + } + + pub fn remove(&mut self, key: &str) -> Option { + self.0.remove(key) } pub fn contains_key(&self, key: &str) -> bool { @@ -170,12 +179,9 @@ mod tests { "index" => i.to_string() ); - assert_eq!(metadata.get("name"), Some(&"My NFT".to_string())); - assert_eq!(metadata.get("description"), Some(&"This is my first NFT".to_string())); - assert_eq!( - metadata.get("image"), - Some(&"https://example.com/my-nft.png".to_string()) - ); - assert_eq!(metadata.get("index"), Some(&"123".to_string())); + assert_eq!(metadata.get("name"), Some("My NFT")); + assert_eq!(metadata.get("description"), Some("This is my first NFT")); + assert_eq!(metadata.get("image"), Some("https://example.com/my-nft.png")); + assert_eq!(metadata.get("index"), Some("123")); } } diff --git a/crates/template_lib/src/resource/manager.rs b/crates/template_lib/src/resource/manager.rs index d577e41bf8..75dfeee928 100644 --- a/crates/template_lib/src/resource/manager.rs +++ b/crates/template_lib/src/resource/manager.rs @@ -43,7 +43,7 @@ use std::collections::BTreeSet; use serde::{Deserialize, Serialize}; use tari_bor::to_value; use tari_template_abi::{call_engine, rust::collections::BTreeMap, EngineOp}; -use tari_template_lib_types::crypto::StealthValueProof; +use tari_template_lib_types::{crypto::StealthValueProof, ResourceInfo}; use crate::{ args::{ @@ -110,20 +110,38 @@ impl ResourceManager { self.resource_address } - /// A public function that returns the resource type of the resource being managed. + /// Returns the resource type of the resource being managed. + /// NOTE: this calls `resource_info()` internally, prefer using that if you need the divisibility as well. /// /// # Panics /// - /// If the resource type is not recognized on a resource or if the resource address is not set via - /// `ResourceManager`. + /// If the resource address does not exist in the runtime context. pub fn resource_type(&self) -> ResourceType { + self.resource_info().resource_type + } + + /// Returns the divisibility of the resource being managed. + /// NOTE: this calls `resource_info()` internally, prefer using that if you need the resource type as well. + /// + /// # Panics + /// + /// If the resource address does not exist in the runtime context. + pub fn divisibility(&self) -> u8 { + self.resource_info().divisibility + } + + /// Returns the resource info of the resource being managed. + /// + /// # Panics + /// + /// If the resource address does not exist in the runtime context. + pub fn resource_info(&self) -> ResourceInfo { let resp: InvokeResult = call_engine(EngineOp::ResourceInvoke, &ResourceInvokeArg { resource_ref: self.resource_address.into(), - action: ResourceAction::GetResourceType, + action: ResourceAction::GetResourceInfo, args: invoke_args![], }); - resp.decode() - .expect("Resource GetResourceType returned invalid resource type") + resp.decode::().expect("Failed to decode ResourceInfo") } /// Creates a new resource on the Tari network. diff --git a/crates/template_lib_types/Cargo.toml b/crates/template_lib_types/Cargo.toml index 3a38871b05..8e79343fb9 100644 --- a/crates/template_lib_types/Cargo.toml +++ b/crates/template_lib_types/Cargo.toml @@ -9,7 +9,8 @@ license.workspace = true [features] default = ["std"] -std = ["serde/std", "tari_template_abi/std", "borsh?/std"] +std = ["serde/std", "tari_template_abi/std", "borsh?/std", "num-integer?/std"] +extra-arith = ["bnum/numtraits", "dep:num-integer"] borsh = ["dep:borsh", "bnum/borsh"] ts = ["ts-rs"] # This feature removes the ability to deserialize Amounts as an integer/string (using deserialize_any), as formats like bincode do not support this. @@ -19,6 +20,7 @@ bincode-compat = [] tari_template_abi = { workspace = true, default-features = false } bnum = { workspace = true } +num-integer = { workspace = true, optional = true, default-features = false } newtype-ops = { workspace = true } borsh = { workspace = true, optional = true, default-features = false, features = ["derive"] } serde = { workspace = true, default-features = false, features = ["derive"] } diff --git a/crates/template_lib_types/src/amount/amount.rs b/crates/template_lib_types/src/amount/amount.rs index 6b42a9585e..bb54cc2f28 100644 --- a/crates/template_lib_types/src/amount/amount.rs +++ b/crates/template_lib_types/src/amount/amount.rs @@ -4,7 +4,7 @@ use bnum::BUint; use newtype_ops::newtype_ops; use serde::ser::Error; -use tari_template_abi::rust::{cmp, fmt, fmt::Debug, iter::Sum, ops::Neg, str::FromStr, write}; +use tari_template_abi::rust::{cmp, fmt, fmt::Debug, iter::Sum, str::FromStr, write}; use crate::{impl_from, partial_eq_impl, partial_ord_impl}; @@ -149,7 +149,7 @@ impl Amount { } /// Returns the product of two amounts, returning `None` if the result overflows. - pub const fn checked_mul(&self, other: &Self) -> Option { + pub const fn checked_mul(&self, other: Self) -> Option { match self.into_inner_value().checked_mul(other.into_inner_value()) { Some(value) => Some(Self::new(value)), None => None, @@ -276,6 +276,20 @@ impl Amount { *self.inner_value().to_bits().digits() } + #[cfg(feature = "extra-arith")] + pub fn checked_sqrt(&self) -> Option { + use num_integer::Roots; + if self.is_negative() { + return None; + } + if self.is_zero() { + return Some(Self::zero()); + } + let inner = self.into_inner_value(); + let sqrt_inner = inner.sqrt(); + Some(Self::new(sqrt_inner)) + } + /// If the amount is negative (< 0), returns `None`, otherwise returns `Some(self)`. pub fn non_negative_checked(self) -> Option { if self.is_negative() { @@ -294,6 +308,16 @@ impl Amount { } } + /// Returns the amount raised to the power of `exp`. + pub fn pow(&self, exp: u32) -> Self { + Self::new(self.into_inner_value().pow(exp)) + } + + /// Returns the amount raised to the power of `exp`, returning `None` if the result overflows. + pub fn checked_pow(&self, exp: u32) -> Option { + self.into_inner_value().checked_pow(exp).map(Self::new) + } + /// Parses a string as an `Amount` in the specified radix. /// /// This function works in constant context, allowing it to be used to define constants. @@ -381,8 +405,8 @@ impl Default for Amount { } } -newtype_ops! { [Amount] {add sub mul div} {:=} Self Self } -newtype_ops! { [Amount] {add sub mul div} {:=} &Self &Self } +newtype_ops! { [Amount] {add sub mul div rem neg} {:=} Self Self } +newtype_ops! { [Amount] {add sub mul div rem neg} {:=} &Self &Self } impl_from!(Amount, u8); impl_from!(Amount, i8); @@ -397,20 +421,6 @@ impl_from!(Amount, i128); impl_from!(Amount, usize); impl_from!(Amount, isize); -// impl From for Amount { -// fn from(value: Amount) -> Self { -// Self::new(value.as_signed_value().into()) -// } -// } -// -// impl TryFrom for Amount { -// type Error = bnum::errors::TryFromIntError; -// -// fn try_from(value: Amount) -> Result { -// Ok(Amount::new(value.into_inner_value().try_into()?)) -// } -// } - impl TryFrom for usize { type Error = bnum::errors::TryFromIntError; @@ -419,36 +429,6 @@ impl TryFrom for usize { } } -impl Neg for Amount { - type Output = Self; - - fn neg(self) -> Self::Output { - Self::new(-self.inner_value()) - } -} - -// impl PartialEq for Amount { -// fn eq(&self, other: &Amount) -> bool { -// self.into_inner_value() -// .try_into() -// .ok() -// .map(|n: i64| n == other.as_signed_value()) -// .unwrap_or(false) -// } -// } -// -// impl PartialOrd for Amount { -// fn partial_cmp(&self, other: &Amount) -> Option { -// if self.is_negative() { -// return Some(cmp::Ordering::Less); -// } -// match self.into_inner_value().try_into().map(Amount::new) { -// Ok(value) => Some(value.cmp(other)), -// Err(_) => Some(cmp::Ordering::Greater), -// } -// } -// } - impl PartialOrd for Amount { fn partial_cmp(&self, other: &usize) -> Option { if self.is_negative() { @@ -529,10 +509,16 @@ mod tests { assert_eq!(c, Amount::from(10)); let d = a.checked_sub(b).unwrap(); assert_eq!(d, Amount::from(-2)); - let e = a.checked_mul(&b).unwrap(); + let e = a.checked_mul(b).unwrap(); assert_eq!(e, Amount::from(24)); let f = b.checked_div(a).unwrap(); assert_eq!(f, Amount::from(1)); + let g = Amount::from(7); + let h = g.checked_div_ceil(Amount::from(2)).unwrap(); + assert_eq!(h, 4); + let i = Amount::from(8); + let j = i.checked_pow(3).unwrap(); + assert_eq!(j, Amount::from(512)); // Test overflow let max = Amount::MAX; @@ -540,12 +526,25 @@ mod tests { assert!(overflow_add.is_none(), "Overflow should return None"); let overflow_sub = Amount::MIN.checked_sub(Amount::from(1)); assert!(overflow_sub.is_none(), "Underflow should return None"); - let overflow_mul = max.checked_mul(&Amount::from(2)); + let overflow_mul = max.checked_mul(Amount::from(2)); assert!(overflow_mul.is_none(), "Overflow should return None"); let overflow_div = Amount::from(1).checked_div(Amount::zero()); assert!(overflow_div.is_none(), "Division by zero should return None"); let overflow_div_ceil = Amount::from(1).checked_div_ceil(Amount::zero()); assert!(overflow_div_ceil.is_none(), "Division by zero should return None"); + let overflow_pow = max.checked_pow(10); + assert!(overflow_pow.is_none(), "Overflow should return None"); + } + + #[test] + #[cfg(feature = "extra-arith")] + fn extra_arithmetic() { + let k = Amount::from(27); + let l = k.checked_sqrt().unwrap(); + assert_eq!(l, Amount::from(5)); + + let negative_sqrt = Amount::from(-4).checked_sqrt(); + assert!(negative_sqrt.is_none(), "Square root of negative should return None"); } #[test] diff --git a/crates/template_lib_types/src/resource_type.rs b/crates/template_lib_types/src/resource_type.rs index 3d0d82711b..a798fd6ed4 100644 --- a/crates/template_lib_types/src/resource_type.rs +++ b/crates/template_lib_types/src/resource_type.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_template_abi::rust::{fmt, str::FromStr}; +use tari_template_abi::rust::fmt; /// Represents every possible type of resource in the Tari network. /// @@ -21,7 +21,6 @@ use tari_template_abi::rust::{fmt, str::FromStr}; pub enum ResourceType { /// Fungible tokens do not have individual identity, making them interchangeable. /// Examples include monetary units, liquidity pool tokens, or tokenized shares. - // TODO: rename to PublicFungible Fungible, /// A resource (i.e., collection) of non-fungible tokens. /// Each NFT is uniquely identifiable within the parent resource and indivisible. @@ -62,28 +61,42 @@ impl fmt::Display for ResourceType { } } -impl FromStr for ResourceType { - type Err = ParseResourceTypeError; +#[cfg(feature = "std")] +mod parsing { + use std::str::FromStr; + + use super::*; + + impl FromStr for ResourceType { + type Err = ParseResourceTypeError; - fn from_str(s: &str) -> Result { - match s { - "Fungible" => Ok(ResourceType::Fungible), - "NonFungible" | "nft" => Ok(ResourceType::NonFungible), - "Confidential" => Ok(ResourceType::Confidential), - "Stealth" => Ok(ResourceType::Stealth), - _ => Err(ParseResourceTypeError(s.to_string())), + fn from_str(s: &str) -> Result { + match s { + "Fungible" => Ok(ResourceType::Fungible), + "NonFungible" | "nft" => Ok(ResourceType::NonFungible), + "Confidential" => Ok(ResourceType::Confidential), + "Stealth" => Ok(ResourceType::Stealth), + _ => Err(ParseResourceTypeError(s.to_string())), + } } } -} -#[derive(Debug)] -pub struct ParseResourceTypeError(String); + #[derive(Debug)] + pub struct ParseResourceTypeError(String); -impl fmt::Display for ParseResourceTypeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Invalid Resource Type string: '{}'", self.0) + impl fmt::Display for ParseResourceTypeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid Resource Type string: '{}'", self.0) + } } -} + impl std::error::Error for ParseResourceTypeError {} +} #[cfg(feature = "std")] -impl std::error::Error for ParseResourceTypeError {} +pub use parsing::ParseResourceTypeError; + +#[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct ResourceInfo { + pub resource_type: ResourceType, + pub divisibility: u8, +} diff --git a/crates/template_test_tooling/src/builtin_component_state.rs b/crates/template_test_tooling/src/builtin_component_state.rs index 04cfcb27df..b3b29103b3 100644 --- a/crates/template_test_tooling/src/builtin_component_state.rs +++ b/crates/template_test_tooling/src/builtin_component_state.rs @@ -6,13 +6,12 @@ use tari_crypto::{ristretto::RistrettoPublicKey, tari_utilities::ByteArray}; use tari_engine::state_store::{StateStoreError, StateWriter}; use tari_engine_types::{ component::{ComponentBody, ComponentHeader}, - id_provider::{IdProvider, ObjectIds}, resource::Resource, resource_container::ResourceContainer, substate::{Substate, SubstateId}, vault::Vault, }; -use tari_template_builtin::NFT_FAUCET_TEMPLATE_ADDRESS; +use tari_template_builtin::{NFT_FAUCET_TEMPLATE_ADDRESS, XTR_FAUCET_TEMPLATE_ADDRESS}; use tari_template_lib::{ auth::{ComponentAccessRules, OwnerRule, ResourceAccessRules}, constants::{ @@ -20,16 +19,17 @@ use tari_template_lib::{ NFT_FAUCET_RESOURCE_ADDRESS, PUBLIC_IDENTITY_RESOURCE_ADDRESS, STEALTH_TARI_RESOURCE_ADDRESS, + XTR_FAUCET_VAULT_ADDRESS, }, metadata, models::Metadata, - prelude::{ResourceType, RistrettoPublicKeyBytes, TemplateAddress}, + prelude::{ResourceType, RistrettoPublicKeyBytes}, resource::TOKEN_SYMBOL, rule, - types::{Amount, EntityId, Hash}, + types::{Amount, EntityId}, }; -use crate::{template_test::test_nft_faucet_component, test_faucet_component}; +use crate::{template_test::test_nft_faucet_component, xtr_faucet_component}; pub fn add_tari_resources(state_db: &mut T) -> Result<(), StateStoreError> { let id = SubstateId::Resource(PUBLIC_IDENTITY_RESOURCE_ADDRESS); @@ -82,32 +82,28 @@ pub fn add_tari_resources(state_db: &mut T) -> Result<(), StateS pub fn initialize_builtin_faucet_state( store: &mut TStore, signer_public_key: &RistrettoPublicKey, - test_faucet_template_address: TemplateAddress, ) { - let initial_supply = Amount::from(1_000_000); + let initial_supply = Amount::MAX; let entity_id = EntityId::default(); - let object_ids = ObjectIds::new(10); - let id_provider = IdProvider::new(entity_id, Hash::default(), &object_ids); - let vault_id = id_provider.new_vault_id().unwrap(); let vault = Vault::new(ResourceContainer::stealth( STEALTH_TARI_RESOURCE_ADDRESS, initial_supply, )); store - .set_state(SubstateId::Vault(vault_id), Substate::new(0, vault)) + .set_state(SubstateId::Vault(XTR_FAUCET_VAULT_ADDRESS), Substate::new(0, vault)) .unwrap(); // This must mirror the test faucet component let state = cbor!({ - "vault" => tari_template_lib::models::Vault::for_test(vault_id), + "vault" => tari_template_lib::models::Vault::for_test(XTR_FAUCET_VAULT_ADDRESS), }) .unwrap(); store .set_state( - SubstateId::Component(test_faucet_component()), + SubstateId::Component(xtr_faucet_component()), Substate::new(0, ComponentHeader { - template_address: test_faucet_template_address, - module_name: "TestFaucet".to_string(), + template_address: XTR_FAUCET_TEMPLATE_ADDRESS, + module_name: "XtrFaucet".to_string(), owner_key: Some(RistrettoPublicKeyBytes::from_bytes(signer_public_key.as_bytes()).unwrap()), owner_rule: OwnerRule::None, access_rules: ComponentAccessRules::allow_all(), diff --git a/crates/template_test_tooling/src/lib.rs b/crates/template_test_tooling/src/lib.rs index a3298059e8..8d8a1a75d5 100644 --- a/crates/template_test_tooling/src/lib.rs +++ b/crates/template_test_tooling/src/lib.rs @@ -11,7 +11,7 @@ mod track_calls; mod wrapped_transaction; pub use package_builder::Package; -pub use template_test::{test_faucet_component, TemplateTest}; +pub use template_test::{xtr_faucet_component, TemplateTest}; // Re-export types used in public interfaces. This allows users to use these types when writing tests without including // the various crates themselves. diff --git a/crates/template_test_tooling/src/package_builder.rs b/crates/template_test_tooling/src/package_builder.rs index a1e71b8989..fc988b0789 100644 --- a/crates/template_test_tooling/src/package_builder.rs +++ b/crates/template_test_tooling/src/package_builder.rs @@ -63,10 +63,9 @@ impl PackageBuilder { } } - pub fn add_template

(&mut self, path: P) -> &mut Self + pub fn add_template

(&mut self, path: P) -> TemplateAddress where P: AsRef { - self.add_template_opts(path, &[], None::<(String, String)>); - self + self.add_template_opts(path, &[], None::<(String, String)>) } pub fn add_template_with_envs(&mut self, path: P, envs: TEnvs) -> &mut Self @@ -99,12 +98,14 @@ impl PackageBuilder { self } - pub fn add_builtin_template(&mut self, address: &TemplateAddress) -> &mut Self { - let wasm = get_template_builtin(address); - let template = WasmModule::from_code(wasm.to_vec()).load_template().unwrap(); - self.add_loaded_template(*address, template); + pub fn add_builtin_template(&mut self, address: TemplateAddress) -> &mut Self { + let wasm = get_template_builtin(&address); + self.add_template_from_code(address, wasm) + } - self + pub fn add_template_from_code(&mut self, address: TemplateAddress, wasm: impl Into>) -> &mut Self { + let template = WasmModule::from_code(wasm).load_template().unwrap(); + self.add_loaded_template(address, template) } pub fn build(&mut self) -> Package { 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 1a32baedf8..17d2ae51f2 100644 --- a/crates/template_test_tooling/src/read_only_state_store.rs +++ b/crates/template_test_tooling/src/read_only_state_store.rs @@ -12,7 +12,10 @@ use tari_engine_types::{ vault::Vault, Utxo, }; -use tari_template_lib::models::{Account, ComponentAddress, ResourceAddress, UtxoAddress, VaultId}; +use tari_template_lib::{ + models::{Account, ComponentAddress, ResourceAddress, UtxoAddress, VaultId}, + types::TemplateAddress, +}; pub struct ReadOnlyStateStore<'a> { store: &'a MemoryStateStore, @@ -27,6 +30,23 @@ impl<'a> ReadOnlyStateStore<'a> { Ok(substate.into_substate_value().into_component().unwrap()) } + pub fn get_components_by_template_address( + &self, + template_address: TemplateAddress, + ) -> Result, StateStoreError> { + let mut components = Vec::new(); + self.with_substates(|id, substate| { + if let SubstateId::Component(component_address) = id { + if let Some(component) = substate.substate_value().as_component() { + if component.template_address == template_address { + components.push((*component_address, component.clone())); + } + } + } + })?; + Ok(components) + } + pub fn get_account(&self, account_address: ComponentAddress) -> Result { let account = self.get_component(account_address)?; Account::from_value(account.state()).map_err(|e| StateStoreError::CustomStr(e.to_string())) diff --git a/crates/template_test_tooling/src/support/assert_error.rs b/crates/template_test_tooling/src/support/assert_error.rs index 77278284b7..10db688317 100644 --- a/crates/template_test_tooling/src/support/assert_error.rs +++ b/crates/template_test_tooling/src/support/assert_error.rs @@ -6,6 +6,7 @@ use std::{borrow::Borrow, fmt::Display}; use tari_engine::runtime::{ActionIdent, RuntimeError}; use tari_engine_types::{commit_result::RejectReason, resource_container::ResourceError}; +#[track_caller] pub fn assert_reject_reason, E: Display>(reason: B, error: E) { let s = reason.borrow().to_string(); // TODO: Would be great if we could enumerate specific reasons from within the engine rather than simply @@ -15,14 +16,14 @@ pub fn assert_reject_reason, E: Display>(reason: B, erro } } -#[allow(dead_code)] +#[track_caller] pub fn assert_access_denied_for_action, A: Into>(reason: B, action_ident: A) { assert_reject_reason(reason, RuntimeError::AccessDenied { action_ident: action_ident.into(), }) } -#[allow(dead_code)] +#[track_caller] pub fn assert_insufficient_funds_for_action>(reason: B) { assert_reject_reason( reason, diff --git a/crates/template_test_tooling/src/template_test.rs b/crates/template_test_tooling/src/template_test.rs index f3453bf955..ab8c1b1c5c 100644 --- a/crates/template_test_tooling/src/template_test.rs +++ b/crates/template_test_tooling/src/template_test.rs @@ -27,6 +27,7 @@ use tari_engine::{ }; use tari_engine_types::{ commit_result::{ExecuteResult, RejectReason}, + indexed_value::IndexedWellKnownTypes, substate::{SubstateDiff, SubstateId}, virtual_substate::{VirtualSubstate, VirtualSubstateId, VirtualSubstates}, ToByteType, @@ -37,10 +38,10 @@ use tari_ootle_common_types::{ Network, SubstateRequirement, }; -use tari_template_builtin::{ACCOUNT_TEMPLATE_ADDRESS, NFT_FAUCET_TEMPLATE_ADDRESS, XTR_FAUCET_TEMPLATE_ADDRESS}; +use tari_template_builtin::all_builtin_templates; use tari_template_lib::{ constants::{NFT_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS}, - models::{ComponentAddress, NonFungibleAddress}, + models::{ComponentAddress, NonFungibleAddress, ResourceAddress}, prelude::RistrettoPublicKeyBytes, types::{Amount, TemplateAddress}, }; @@ -67,7 +68,7 @@ use crate::{ Package, }; -pub const fn test_faucet_component() -> ComponentAddress { +pub const fn xtr_faucet_component() -> ComponentAddress { XTR_FAUCET_COMPONENT_ADDRESS } @@ -91,6 +92,9 @@ pub struct TemplateTest { } impl TemplateTest { + /// The initial balance of a funded account created by `create_funded_account`. + pub const FUNDED_ACCOUNT_INITIAL_BALANCE: u64 = 1_000_000_000; + pub fn new, P: Clone + AsRef>(template_paths: I) -> Self { Self::new_internal(template_paths, None::<(String, String)>) } @@ -119,11 +123,11 @@ impl TemplateTest { let mut builder = Package::builder(); // Add builtin templates - builder.add_builtin_template(&ACCOUNT_TEMPLATE_ADDRESS); - builder.add_builtin_template(&NFT_FAUCET_TEMPLATE_ADDRESS); - builder.add_builtin_template(&XTR_FAUCET_TEMPLATE_ADDRESS); + for (addr, code) in all_builtin_templates() { + builder.add_template_from_code(addr, code); + } - // Add the faucet template for fungible tokens + // Add the faucet template for non-XTR fungible tokens builder.add_template(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/faucet")); // Add all of the templates specified in the argument @@ -183,9 +187,8 @@ impl TemplateTest { } pub fn bootstrap_state(&mut self) { - let template_addr = self.get_template_address("TestFaucet"); add_tari_resources(&mut self.state_store).unwrap(); - initialize_builtin_faucet_state(&mut self.state_store, &self.public_key, template_addr); + initialize_builtin_faucet_state(&mut self.state_store, &self.public_key); initialize_builtin_nft_faucet_state(&mut self.state_store) } @@ -314,6 +317,7 @@ impl TemplateTest { .unwrap_or_else(|| panic!("No template with name {}", name)) } + #[track_caller] pub fn create_account( &mut self, owner_public_key: RistrettoPublicKeyBytes, @@ -338,6 +342,7 @@ impl TemplateTest { .unwrap() } + #[track_caller] pub fn call_function( &mut self, template_name: &str, @@ -367,6 +372,7 @@ impl TemplateTest { .unwrap() } + #[track_caller] pub fn call_method( &mut self, component_address: ComponentAddress, @@ -417,6 +423,7 @@ impl TemplateTest { self.public_key.to_byte_type() } + #[track_caller] pub fn create_empty_account(&mut self) -> (ComponentAddress, NonFungibleAddress, RistrettoSecretKey) { let (owner_proof, public_key, secret_key) = self.create_owner_proof(); let old_fail_fees = self.enable_fees; @@ -434,13 +441,16 @@ impl TemplateTest { self.create_funded_account() } + #[track_caller] pub fn create_funded_account(&mut self) -> (ComponentAddress, NonFungibleAddress, RistrettoSecretKey) { let (owner_proof, public_key, secret_key) = self.create_owner_proof(); let old_fail_fees = self.enable_fees; self.enable_fees = false; let result = self.execute_expect_success( Transaction::builder() - .call_method(test_faucet_component(), "take_free_coins", args![]) + .call_method(xtr_faucet_component(), "take", args![ + Self::FUNDED_ACCOUNT_INITIAL_BALANCE + ]) .put_last_instruction_output_on_workspace("bucket") .create_account_with_bucket(public_key.to_byte_type(), "bucket") .build_and_seal(&secret_key), @@ -459,6 +469,7 @@ impl TemplateTest { (component, owner_proof, secret_key) } + #[track_caller] pub fn create_custom_funded_account>( &mut self, amount: A, @@ -473,7 +484,7 @@ impl TemplateTest { self.enable_fees = false; let result = self.execute_expect_success( Transaction::builder() - .call_method(test_faucet_component(), "take_free_coins_custom", args![amount.into()]) + .call_method(xtr_faucet_component(), "take", args![amount.into()]) .put_last_instruction_output_on_workspace("bucket") .create_account_with_bucket(public_key.to_byte_type(), "bucket") .build_and_seal(&secret_key), @@ -492,18 +503,61 @@ impl TemplateTest { (component, owner_proof, secret_key, public_key) } + #[track_caller] + pub fn create_test_faucet_component>( + &mut self, + initial_supply: A, + ) -> (ComponentAddress, ResourceAddress) { + let template_addr = self.get_template_address("TestFaucet"); + let result = self.execute_expect_success( + Transaction::builder() + .call_function(template_addr, "mint", args![initial_supply.into()]) + .build_and_seal(&self.secret_key), + vec![], + ); + + let (addr, component) = result + .expect_success() + .up_iter() + .filter_map(|(id, substate)| { + id.as_component_address().and_then(|addr| { + let component = substate.substate_value().as_component()?; + if component.template_address == template_addr { + Some((addr, component.clone())) + } else { + None + } + }) + }) + .next() + .expect("No component address found in faucet creation result"); + + let indexed = IndexedWellKnownTypes::from_value(component.state()).unwrap(); + let vault_id = indexed + .vault_ids() + .first() + .expect("No vault id found in faucet component state"); + let vault = self + .read_only_state_store() + .get_vault(vault_id) + .expect("No vault id found in faucet component state"); + (addr, *vault.resource_address()) + } + fn next_key_seed(&mut self) -> u8 { let seed = self.key_seed; self.key_seed += 1; seed } + #[track_caller] pub fn create_owner_proof(&mut self) -> (NonFungibleAddress, RistrettoPublicKey, RistrettoSecretKey) { let (secret_key, public_key) = create_key_pair_from_seed(self.next_key_seed()); let owner_token = NonFungibleAddress::from_public_key(public_key.to_byte_type()); (owner_token, public_key, secret_key) } + #[track_caller] pub fn try_execute_instructions( &mut self, fee_instructions: Vec, @@ -518,6 +572,7 @@ impl TemplateTest { self.try_execute(transaction, proofs) } + #[track_caller] pub fn try_execute( &mut self, transaction: Transaction, @@ -583,6 +638,7 @@ impl TemplateTest { Ok(result) } + #[track_caller] pub fn execute_and_commit_on_success( &mut self, transaction: Transaction, @@ -640,6 +696,7 @@ impl TemplateTest { result.expect_failure().clone() } + #[track_caller] pub fn execute_and_commit( &mut self, instructions: Vec, @@ -648,6 +705,7 @@ impl TemplateTest { self.execute_and_commit_with_fees(vec![], instructions, proofs) } + #[track_caller] pub fn execute_and_commit_with_fees( &mut self, fee_instructions: Vec, @@ -672,6 +730,7 @@ impl TemplateTest { Ok(result) } + #[track_caller] pub fn execute_and_commit_manifest<'a, I: IntoIterator>( &mut self, manifest: &str, diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index c72e6b3d4b..e23674b4d7 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -9,8 +9,8 @@ license.workspace = true [dependencies] # Apps -minotari_console_wallet = { workspace = true, features = ["grpc"] } -minotari_node = { workspace = true } +minotari_console_wallet = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } +minotari_node = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } tari_validator_node = { workspace = true, default-features = false } tari_indexer = { workspace = true, default-features = false } tari_ootle_walletd = { workspace = true, default-features = false } @@ -23,10 +23,10 @@ tari_transaction_key_manager = { workspace = true } minotari_app_grpc = { workspace = true } minotari_app_utilities = { workspace = true } minotari_node_grpc_client = { workspace = true } +minotari_wallet = { git = "https://github.com/tari-project/tari.git", tag = "v5.1.0-rc.1" } tari_common = { workspace = true } tari_common_types = { workspace = true } tari_common_sqlite = { workspace = true } -minotari_wallet = { workspace = true } tari_p2p = { workspace = true } tari_shutdown = { workspace = true } tari_sidechain = { workspace = true } diff --git a/lints.toml b/lints.toml index 500b5118d5..0d7d47942e 100644 --- a/lints.toml +++ b/lints.toml @@ -70,6 +70,9 @@ allow = [ 'clippy::mutable_key_type', # Using `format!("hello {}", x)` vs `format!("hello {x}")` is not a big deal/will cause code churn to fix, so we'll allow it. 'clippy::uninlined_format_args', + # Dropping a type that does not implement Drop is sometimes used legitamately e.g. a struct containing a reference, subsequent code wants to make a mutable borrow. + # The lint is only a warn by default and does not include a complete explanation of why this is bad or any caveats. + 'clippy::drop_non_drop', ] warn = [