diff --git a/Cargo.lock b/Cargo.lock index 1ddef52daa..c532f866e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,15 +241,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "ascii-canvas" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" -dependencies = [ - "term", -] - [[package]] name = "ascii_utils" version = "0.9.3" @@ -372,6 +363,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -403,8 +404,8 @@ checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.3.0", - "futures-lite 2.6.0", + "fastrand", + "futures-lite", "pin-project-lite", "slab", ] @@ -420,7 +421,7 @@ dependencies = [ "async-io", "async-lock", "blocking", - "futures-lite 2.6.0", + "futures-lite", "once_cell", ] @@ -524,9 +525,9 @@ dependencies = [ "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.6.0", + "futures-lite", "parking", - "polling 3.8.0", + "polling", "rustix 1.0.7", "slab", "tracing", @@ -567,7 +568,7 @@ dependencies = [ "blocking", "cfg-if", "event-listener 5.4.0", - "futures-lite 2.6.0", + "futures-lite", "rustix 1.0.7", "tracing", ] @@ -605,6 +606,7 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" dependencies = [ + "async-attributes", "async-channel 1.9.0", "async-global-executor", "async-io", @@ -614,7 +616,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "futures-lite 2.6.0", + "futures-lite", "gloo-timers", "kv-log-macro", "log", @@ -733,7 +735,7 @@ dependencies = [ "bitflags 1.3.2", "bytes 1.10.1", "futures-util", - "headers", + "headers 0.3.9", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -967,17 +969,6 @@ dependencies = [ "serde", ] -[[package]] -name = "basic-cookies" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" -dependencies = [ - "lalrpop", - "lalrpop-util", - "regex", -] - [[package]] name = "bigdecimal" version = "0.4.8" @@ -1193,7 +1184,7 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.6.0", + "futures-lite", "piper", ] @@ -1385,11 +1376,10 @@ dependencies = [ [[package]] name = "cacache" -version = "12.0.0" +version = "13.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142316461ed3a3dfcba10417317472da5bfd0461e4d276bf7c07b330766d9490" +checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b" dependencies = [ - "async-std", "digest", "either", "futures 0.3.31", @@ -1406,6 +1396,8 @@ dependencies = [ "ssri", "tempfile", "thiserror 1.0.69", + "tokio", + "tokio-stream", "walkdir", ] @@ -1475,12 +1467,6 @@ dependencies = [ "cipher 0.4.4", ] -[[package]] -name = "castaway" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" - [[package]] name = "cbc" version = "0.1.2" @@ -2413,37 +2399,6 @@ dependencies = [ "regex-syntax 0.7.5", ] -[[package]] -name = "curl" -version = "0.4.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2d5c8f48d9c0c23250e52b55e82a6ab4fdba6650c931f5a0a57a43abda812b" -dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2", - "windows-sys 0.59.0", -] - -[[package]] -name = "curl-sys" -version = "0.4.82+curl-8.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4d63638b5ec65f1a4ae945287b3fd035be4554bbaf211901159c9a2a74fb5be" -dependencies = [ - "cc", - "libc", - "libnghttp2-sys", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "windows-sys 0.59.0", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -3254,15 +3209,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ena" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" -dependencies = [ - "log", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -3466,15 +3412,6 @@ dependencies = [ "ascii_utils", ] -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -3715,28 +3652,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-lite" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ - "fastrand 2.3.0", + "fastrand", "futures-core", "futures-io", "parking", @@ -4123,13 +4045,28 @@ checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", "bytes 1.10.1", - "headers-core", + "headers-core 0.2.0", "http 0.2.12", "httpdate", "mime", "sha1 0.10.6", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes 1.10.1", + "headers-core 0.3.0", + "http 1.3.1", + "httpdate", + "mime", + "sha1 0.10.6", +] + [[package]] name = "headers-core" version = "0.2.0" @@ -4139,6 +4076,15 @@ dependencies = [ "http 0.2.12", ] +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.3.1", +] + [[package]] name = "heck" version = "0.3.3" @@ -4371,28 +4317,36 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "httpmock" -version = "0.6.8" +version = "0.8.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b02e044d3b4c2f94936fb05f9649efa658ca788f44eb6b87554e2033fc8ce93" +checksum = "b9d649264818ad8f19c01f72b4ddf2f0cfcd1183691b956de733673e81d8a51f" dependencies = [ "assert-json-diff", "async-object-pool", + "async-std", "async-trait", - "base64 0.21.7", - "basic-cookies", + "base64 0.22.1", + "bytes 1.10.1", "crossbeam-utils", "form_urlencoded", + "futures-timer", "futures-util", - "hyper 0.14.32", - "isahc", + "headers 0.4.1", + "http 1.3.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", "lazy_static", - "levenshtein", "log", + "path-tree", "regex", "serde", "serde_json", "serde_regex", "similar", + "stringmetrics", + "tabwriter", + "thiserror 1.0.69", "tokio", "url", ] @@ -5107,33 +5061,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "isahc" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" -dependencies = [ - "async-channel 1.9.0", - "castaway", - "crossbeam-utils", - "curl", - "curl-sys", - "encoding_rs", - "event-listener 2.5.3", - "futures-lite 1.13.0", - "http 0.2.12", - "log", - "mime", - "once_cell", - "polling 2.8.0", - "slab", - "sluice", - "tracing", - "tracing-futures", - "url", - "waker-fn", -] - [[package]] name = "iter-read" version = "1.1.0" @@ -5149,15 +5076,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -5408,37 +5326,6 @@ dependencies = [ "log", ] -[[package]] -name = "lalrpop" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" -dependencies = [ - "ascii-canvas", - "bit-set", - "ena", - "itertools 0.11.0", - "lalrpop-util", - "petgraph 0.6.5", - "pico-args", - "regex", - "regex-syntax 0.8.5", - "string_cache", - "term", - "tiny-keccak", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" -dependencies = [ - "regex-automata 0.4.9", -] - [[package]] name = "lazy-regex" version = "3.4.1" @@ -5534,12 +5421,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "levenshtein" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" - [[package]] name = "libc" version = "0.2.174" @@ -5593,16 +5474,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libnghttp2-sys" -version = "0.1.11+1.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6c24e48a7167cffa7119da39d577fa482e66c688a4aac016bee862e1a713c4" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "libp2p" version = "0.56.0" @@ -5838,7 +5709,7 @@ dependencies = [ "futures-bounded", "libp2p", "prometheus-client", - "prost 0.12.6", + "prost 0.13.5", "smallvec 2.0.0-alpha.11", "tracing", ] @@ -7211,12 +7082,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "newtype-ops" version = "0.1.4" @@ -7820,6 +7685,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" +[[package]] +name = "path-tree" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a97453bc21a968f722df730bfe11bd08745cb50d1300b0df2bda131dece136" +dependencies = [ + "smallvec 1.15.1", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -8021,21 +7895,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "0.4.30" @@ -8095,7 +7954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.3.0", + "fastrand", "futures-io", ] @@ -8126,22 +7985,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - [[package]] name = "polling" version = "3.8.0" @@ -8210,12 +8053,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "prettyplease" version = "0.1.25" @@ -8375,16 +8212,6 @@ dependencies = [ "syn 2.0.103", ] -[[package]] -name = "prost" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" -dependencies = [ - "bytes 1.10.1", - "prost-derive 0.9.0", -] - [[package]] name = "prost" version = "0.11.9" @@ -8395,16 +8222,6 @@ dependencies = [ "prost-derive 0.11.9", ] -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes 1.10.1", - "prost-derive 0.12.6", -] - [[package]] name = "prost" version = "0.13.5" @@ -8437,27 +8254,6 @@ dependencies = [ "which", ] -[[package]] -name = "prost-build" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" -dependencies = [ - "bytes 1.10.1", - "heck 0.5.0", - "itertools 0.12.1", - "log", - "multimap 0.10.1", - "once_cell", - "petgraph 0.6.5", - "prettyplease 0.2.34", - "prost 0.12.6", - "prost-types 0.12.6", - "regex", - "syn 2.0.103", - "tempfile", -] - [[package]] name = "prost-build" version = "0.13.5" @@ -8478,19 +8274,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "prost-derive" version = "0.11.9" @@ -8504,19 +8287,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.103", -] - [[package]] name = "prost-derive" version = "0.13.5" @@ -8530,16 +8300,6 @@ dependencies = [ "syn 2.0.103", ] -[[package]] -name = "prost-types" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" -dependencies = [ - "bytes 1.10.1", - "prost 0.9.0", -] - [[package]] name = "prost-types" version = "0.11.9" @@ -8549,15 +8309,6 @@ dependencies = [ "prost 0.11.9", ] -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost 0.12.6", -] - [[package]] name = "prost-types" version = "0.13.5" @@ -8571,7 +8322,7 @@ dependencies = [ name = "proto_builder" version = "0.11.2" dependencies = [ - "prost-build 0.12.6", + "prost-build 0.13.5", "sha2", ] @@ -10215,12 +9966,6 @@ dependencies = [ "time", ] -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "skeptic" version = "0.13.7" @@ -10258,17 +10003,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "sluice" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" -dependencies = [ - "async-channel 1.9.0", - "futures-core", - "futures-io", -] - [[package]] name = "smallvec" version = "1.15.1" @@ -10485,16 +10219,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] -name = "string_cache" -version = "0.8.9" +name = "stringmetrics" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] +checksum = "7b3c8667cd96245cbb600b8dec5680a7319edd719c5aa2b5d23c6bff94f39765" [[package]] name = "strip-ansi-escapes" @@ -10762,6 +10490,15 @@ dependencies = [ "libc", ] +[[package]] +name = "tabwriter" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce91f2f0ec87dff7e6bcbbeb267439aa1188703003c6055193c821487400432" +dependencies = [ + "unicode-width 0.2.1", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -11310,6 +11047,7 @@ dependencies = [ "async-graphql-axum", "axum 0.6.20", "axum-jrpc", + "cacache", "clap 3.2.25", "config", "diesel", @@ -11326,6 +11064,7 @@ dependencies = [ "serde_json", "serde_with", "tari_base_node_client", + "tari_bor", "tari_common", "tari_common_types", "tari_consensus", @@ -11381,7 +11120,6 @@ dependencies = [ name = "tari_indexer_lib" version = "0.11.2" dependencies = [ - "async-trait", "log", "serde", "tari_engine_types", @@ -11509,8 +11247,6 @@ name = "tari_ootle_app_utilities" version = "0.11.2" dependencies = [ "anyhow", - "async-trait", - "cacache", "config", "json5", "libp2p-identity", @@ -11528,7 +11264,6 @@ dependencies = [ "tari_engine_types", "tari_epoch_manager", "tari_epoch_oracles", - "tari_indexer_lib", "tari_networking", "tari_ootle_common_types", "tari_ootle_storage", @@ -11553,8 +11288,8 @@ dependencies = [ "indexmap 2.9.0", "libp2p-identity", "newtype-ops", - "prost 0.12.6", - "prost-types 0.9.0", + "prost 0.13.5", + "prost-types 0.13.5", "rand 0.8.5", "serde", "tari_bor", @@ -11574,7 +11309,7 @@ name = "tari_ootle_p2p" version = "0.11.2" dependencies = [ "anyhow", - "prost 0.12.6", + "prost 0.13.5", "proto_builder", "serde", "tari_bor", @@ -11729,7 +11464,7 @@ dependencies = [ "serde", "serde_json", "tari_bor", - "tari_common_types", + "tari_crypto", "tari_engine_types", "tari_ootle_common_types", "tari_ootle_wallet_sdk", @@ -11837,8 +11572,8 @@ dependencies = [ "log", "once_cell", "pin-project 1.1.10", - "prost 0.12.6", - "prost-build 0.12.6", + "prost 0.13.5", + "prost-build 0.13.5", "proto_builder", "tari_metrics", "tari_shutdown", @@ -12070,6 +11805,7 @@ dependencies = [ "tari_engine_types", "tari_ootle_app_utilities", "tari_ootle_common_types", + "tari_ootle_wallet_sdk", "tari_shutdown", "tari_sidechain", "tari_template_lib_types", @@ -12300,7 +12036,6 @@ dependencies = [ "tari_engine_types", "tari_epoch_manager", "tari_epoch_oracles", - "tari_indexer_lib", "tari_networking", "tari_ootle_app_utilities", "tari_ootle_common_types", @@ -12310,7 +12045,6 @@ dependencies = [ "tari_rpc_framework", "tari_rpc_state_sync", "tari_shutdown", - "tari_sidechain", "tari_state_store_rocksdb", "tari_state_tree", "tari_swarm", @@ -12380,7 +12114,7 @@ name = "tari_validator_node_rpc" version = "0.11.2" dependencies = [ "anyhow", - "prost 0.12.6", + "prost 0.13.5", "proto_builder", "serde", "tari_bor", @@ -12474,24 +12208,13 @@ version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "fastrand 2.3.0", + "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.7", "windows-sys 0.59.0", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next 2.0.0", - "rustversion", - "winapi", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -13060,16 +12783,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project 1.1.10", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -13563,12 +13276,6 @@ dependencies = [ "atomic-waker", ] -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index dda1d9d1da..dbecd87a38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,7 +166,6 @@ borsh = { version = "1.5", default-features = false } bytes = "1.10.0" bnum = "0.13.0" bounded-vec = "0.9.0" -cacache = "12.0.0" cargo_toml = "0.20.5" ciborium = { version = "0.2.2", default-features = false } ciborium-io = { version = "0.2.2", default-features = false } @@ -191,7 +190,6 @@ json5 = "0.4.1" jsonwebtoken = "9.3.1" hashbrown = { version = "0.13.2" } hex = "0.4" -httpmock = "0.6.8" humantime = "2.1.0" humantime-serde = "1.1.1" include_dir = "0.7.2" @@ -215,9 +213,9 @@ once_cell = "1.18.0" pin-project = "1.1" proc-macro2 = "1.0" prometheus-client = { version = "0.23", default-features = false } -prost = "0.12" -prost-build = "0.12" -prost-types = "0.9" +prost = "0.13" +prost-build = "0.13" +prost-types = "0.13" quick-protobuf = "0.8" quick-protobuf-codec = "0.3.1" quote = "1.0.7" diff --git a/applications/tari_app_utilities/Cargo.toml b/applications/tari_app_utilities/Cargo.toml index 869779b9e0..9b3ecd6ddb 100644 --- a/applications/tari_app_utilities/Cargo.toml +++ b/applications/tari_app_utilities/Cargo.toml @@ -21,12 +21,9 @@ tari_template_lib = { workspace = true } tari_template_manager = { workspace = true } tari_transaction = { workspace = true } tari_bor = { workspace = true, default-features = true } -tari_indexer_lib = { workspace = true } tari_networking = { workspace = true } anyhow = { workspace = true } -async-trait = { workspace = true } -cacache = { workspace = true } json5 = { workspace = true } libp2p-identity = { workspace = true } log = { workspace = true, features = ["std"] } diff --git a/applications/tari_app_utilities/src/lib.rs b/applications/tari_app_utilities/src/lib.rs index 4101f560cf..638ec1fe27 100644 --- a/applications/tari_app_utilities/src/lib.rs +++ b/applications/tari_app_utilities/src/lib.rs @@ -26,7 +26,6 @@ pub mod epoch_oracle_config; pub mod keypair; pub mod p2p_config; pub mod seed_peer; -pub mod substate_file_cache; pub mod template_download_queue; pub mod transaction_executor; pub mod utxo_store; diff --git a/applications/tari_app_utilities/src/transaction_executor.rs b/applications/tari_app_utilities/src/transaction_executor.rs index 9f107f187b..3400425188 100644 --- a/applications/tari_app_utilities/src/transaction_executor.rs +++ b/applications/tari_app_utilities/src/transaction_executor.rs @@ -45,7 +45,7 @@ impl ExecutionOutput { &self, inputs: I, ) -> Vec { - if let Some(diff) = self.result.finalize.accept() { + if let Some(diff) = self.result.finalize.any_accept() { inputs .into_iter() .map(|(substate_req, substate)| { diff --git a/applications/tari_indexer/Cargo.toml b/applications/tari_indexer/Cargo.toml index f81b782be7..c1fd44d376 100644 --- a/applications/tari_indexer/Cargo.toml +++ b/applications/tari_indexer/Cargo.toml @@ -9,30 +9,31 @@ license.workspace = true [dependencies] minotari_app_utilities = { workspace = true } +tari_base_node_client = { workspace = true } +tari_bor = { workspace = true } tari_common = { workspace = true } tari_common_types = { workspace = true } +tari_consensus = { workspace = true } tari_consensus_types = { workspace = true } tari_crypto = { workspace = true } -tari_shutdown = { workspace = true } -tari_ootle_app_utilities = { workspace = true } -tari_epoch_oracles = { workspace = true, features = ["base_layer"] } -tari_ootle_common_types = { workspace = true } -tari_base_node_client = { workspace = true } tari_engine = { workspace = true } -tari_ootle_storage = { workspace = true } -tari_ootle_storage_sqlite = { workspace = true } -tari_epoch_manager = { workspace = true } tari_engine_types = { workspace = true } +tari_epoch_manager = { workspace = true } +tari_epoch_oracles = { workspace = true, features = ["base_layer"] } tari_indexer_client = { workspace = true, default-features = false } tari_indexer_lib = { workspace = true } +tari_networking = { workspace = true } +tari_ootle_app_utilities = { workspace = true } +tari_ootle_common_types = { workspace = true } +tari_ootle_p2p = { workspace = true } +tari_ootle_storage = { workspace = true } +tari_ootle_storage_sqlite = { workspace = true } +tari_rpc_framework = { workspace = true } +tari_shutdown = { workspace = true } tari_template_lib = { workspace = true } tari_template_manager = { workspace = true } tari_transaction = { workspace = true } -tari_ootle_p2p = { workspace = true } -tari_consensus = { workspace = true } tari_validator_node_rpc = { workspace = true } -tari_rpc_framework = { workspace = true } -tari_networking = { workspace = true } anyhow = { workspace = true } axum = { workspace = true } @@ -72,6 +73,7 @@ tokio = { workspace = true, features = [ ] } tower-http = { workspace = true, features = ["default", "cors"] } url = { workspace = true, features = ["serde"] } +cacache = { version = "13.1.0", default-features = false, features = ["mmap", "tokio-runtime"] } [package.metadata.cargo-machete] ignored = [ diff --git a/applications/tari_indexer/src/json_rpc/handlers.rs b/applications/tari_indexer/src/json_rpc/handlers.rs index 8562df68db..bbff335c98 100644 --- a/applications/tari_indexer/src/json_rpc/handlers.rs +++ b/applications/tari_indexer/src/json_rpc/handlers.rs @@ -570,6 +570,17 @@ impl JsonRpcHandlers { ), ) }, + TransactionManagerError::InvalidTransaction { + transaction_id, + details, + } => JsonRpcResponse::error( + answer_id, + JsonRpcError::new( + JsonRpcErrorReason::ApplicationError(400), + format!("Transaction {} is invalid: {}", transaction_id, details), + json::Value::Null, + ), + ), e => Self::internal_error(answer_id, e), })?; diff --git a/applications/tari_indexer/src/lib.rs b/applications/tari_indexer/src/lib.rs index d5546abeb5..0b34e49505 100644 --- a/applications/tari_indexer/src/lib.rs +++ b/applications/tari_indexer/src/lib.rs @@ -40,6 +40,7 @@ mod json_rpc; mod network_client; mod network_state_sync; mod storage_sqlite; +mod substate_file_cache; mod substate_manager; mod transaction_manager; @@ -61,11 +62,7 @@ use tari_epoch_manager::{ use tari_epoch_oracles::EpochOracle; use tari_indexer_lib::substate_scanner::SubstateScanner; use tari_networking::NetworkingService; -use tari_ootle_app_utilities::{ - keypair::setup_keypair_prompt, - substate_file_cache::SubstateFileCache, - template_download_queue::TemplateDownloadQueue, -}; +use tari_ootle_app_utilities::{keypair::setup_keypair_prompt, template_download_queue::TemplateDownloadQueue}; use tari_ootle_common_types::{layer_one_transaction::LayerOneTransactionDef, Epoch, PeerAddress}; use tari_ootle_storage::global::{DbFactory, GlobalDb}; use tari_ootle_storage_sqlite::{global::SqliteGlobalDbAdapter, SqliteDbFactory}; @@ -79,6 +76,7 @@ use crate::{ event_manager::EventManager, graphql::server::run_graphql, json_rpc::{spawn_json_rpc, JsonRpcHandlers}, + substate_file_cache::SubstateFileCache, transaction_manager::TransactionManager, }; diff --git a/applications/tari_app_utilities/src/substate_file_cache.rs b/applications/tari_indexer/src/substate_file_cache.rs similarity index 54% rename from applications/tari_app_utilities/src/substate_file_cache.rs rename to applications/tari_indexer/src/substate_file_cache.rs index cb1801ce72..deb3976145 100644 --- a/applications/tari_app_utilities/src/substate_file_cache.rs +++ b/applications/tari_indexer/src/substate_file_cache.rs @@ -1,28 +1,8 @@ -// Copyright 2023. The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause use std::{fs, path::PathBuf}; -use async_trait::async_trait; use tari_bor::{decode, encode}; use tari_indexer_lib::substate_cache::{SubstateCache, SubstateCacheEntry, SubstateCacheError}; @@ -45,7 +25,6 @@ impl SubstateFileCache { } } -#[async_trait] impl SubstateCache for SubstateFileCache { async fn read(&self, address: String) -> Result, SubstateCacheError> { let res = cacache::read(&self.cache_dir_path, address).await; diff --git a/applications/tari_indexer/src/substate_manager.rs b/applications/tari_indexer/src/substate_manager.rs index b7ea60b497..b19d50e819 100644 --- a/applications/tari_indexer/src/substate_manager.rs +++ b/applications/tari_indexer/src/substate_manager.rs @@ -28,7 +28,6 @@ use tari_engine_types::substate::{SubstateId, SubstateValue}; use tari_epoch_manager::service::EpochManagerHandle; use tari_indexer_client::types::ListSubstateItem; use tari_indexer_lib::{substate_scanner::SubstateScanner, NonFungibleSubstate}; -use tari_ootle_app_utilities::substate_file_cache::SubstateFileCache; use tari_ootle_common_types::{ shard::Shard, substate_type::SubstateType, @@ -43,7 +42,10 @@ use tari_template_lib::{ }; use tari_validator_node_rpc::client::{SubstateResult, TariValidatorNodeRpcClientFactory}; -use crate::storage_sqlite::store_factory::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore}; +use crate::{ + storage_sqlite::store_factory::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore}, + substate_file_cache::SubstateFileCache, +}; #[derive(Debug, Serialize, Deserialize)] pub struct SubstateResponse { diff --git a/applications/tari_indexer/src/transaction_manager/error.rs b/applications/tari_indexer/src/transaction_manager/error.rs index 738a3190a9..c603ff58de 100644 --- a/applications/tari_indexer/src/transaction_manager/error.rs +++ b/applications/tari_indexer/src/transaction_manager/error.rs @@ -4,11 +4,17 @@ use tari_indexer_lib::error::IndexerError; use tari_ootle_common_types::optional::IsNotFoundError; use tari_ootle_storage::StorageError; +use tari_transaction::TransactionId; use crate::network_client::NetworkClientError; #[derive(Debug, thiserror::Error)] pub enum TransactionManagerError { + #[error("Transaction {transaction_id} is invalid: {details}")] + InvalidTransaction { + transaction_id: TransactionId, + details: String, + }, #[error(transparent)] NetworkClientError(#[from] NetworkClientError), #[error("{entity} not found: {key}")] diff --git a/applications/tari_indexer/src/transaction_manager/mod.rs b/applications/tari_indexer/src/transaction_manager/mod.rs index 2f8dab978f..9404f1eb98 100644 --- a/applications/tari_indexer/src/transaction_manager/mod.rs +++ b/applications/tari_indexer/src/transaction_manager/mod.rs @@ -62,6 +62,14 @@ where } pub async fn submit_transaction(&self, transaction: Transaction) -> Result { + if !transaction.verify_all_signatures() { + // DEV note: If signatures are invalid here (but they should be valid), this probably indicates an issue + // with the JSON decoding (crates/engine_types/src/argument_parser.rs) + return Err(TransactionManagerError::InvalidTransaction { + transaction_id: transaction.calculate_id(), + details: "Transaction has one or more invalid signature(s)".to_string(), + }); + } self.store .with_write_tx(|tx| tx.insert_or_ignore_transaction(&transaction))?; let id = self.network_client.submit_transaction(transaction).await?; diff --git a/applications/tari_indexer/web_ui/src/routes/Transaction/components/FeeInformation.tsx b/applications/tari_indexer/web_ui/src/routes/Transaction/components/FeeInformation.tsx index e932b4a101..7b83e24a66 100644 --- a/applications/tari_indexer/web_ui/src/routes/Transaction/components/FeeInformation.tsx +++ b/applications/tari_indexer/web_ui/src/routes/Transaction/components/FeeInformation.tsx @@ -46,13 +46,14 @@ interface FeeInformationProps extends FeeReceipt { } function FeeInformation({ - total_fee_payment, - total_fees_paid, - cost_breakdown, - expandAllTrigger = 0, - collapseAllTrigger = 0, - onExpandedChange, -}: FeeInformationProps) { + total_fee_payment, + total_fees_paid, + total_fee_overcharge, + cost_breakdown, + expandAllTrigger = 0, + collapseAllTrigger = 0, + onExpandedChange, + }: FeeInformationProps) { const [expanded, setExpanded] = useState(false); useEffect(() => { @@ -91,7 +92,7 @@ function FeeInformation({ Total Fees Paid - {formatXTM(total_fees_paid)} + {formatXTM(total_fees_paid)}{total_fee_overcharge > 0 ? ` Overcharge: ${total_fee_overcharge}` : ""} Cost Breakdown @@ -105,7 +106,7 @@ function FeeInformation({ variant="filled" color="default" /> - ) + ), )} diff --git a/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx b/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx index 85ddf451ec..4968890907 100644 --- a/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx +++ b/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx @@ -45,7 +45,7 @@ import { validateHash } from "../../../utils/helpers"; // Type guard to check if result is finalized const isFinalized = ( - result: IndexerTransactionFinalizedResult + result: IndexerTransactionFinalizedResult, ): result is { Finalized: any } => { return typeof result === "object" && result !== null && "Finalized" in result; }; @@ -59,7 +59,7 @@ function Result({ transaction_id }: IndexerGetTransactionResultRequest) { const normalizedTransactionId = transaction_id.toLowerCase(); const isValidHash = validateHash(normalizedTransactionId); const { data, isLoading, error, isError } = useGetTransactionResult( - normalizedTransactionId + normalizedTransactionId, ); if (!isValidHash) { @@ -105,12 +105,12 @@ function Result({ transaction_id }: IndexerGetTransactionResultRequest) { {data.result.Finalized.execution_result?.execution_time ? `${ - data.result.Finalized.execution_result - .execution_time.secs - }s ${Math.round( - data.result.Finalized.execution_result - .execution_time.nanos / 1000000 - )}ms` + data.result.Finalized.execution_result + .execution_time.secs + }s ${Math.round( + data.result.Finalized.execution_result + .execution_time.nanos / 1000000, + )}ms` : "N/A"} @@ -121,10 +121,7 @@ function Result({ transaction_id }: IndexerGetTransactionResultRequest) { {data.result.Finalized.execution_result?.finalize ?.fee_receipt && ( - + )} , - ) -> anyhow::Result { - let mut client = self.connect_client(webauthn_finish_auth_request).await?; - let AccountGetResponse { public_key, .. } = client.accounts_get(name.into()).await?; - Ok(public_key) + pub async fn create_nonce_key(&self) -> anyhow::Result<(RistrettoPublicKeyBytes, u64)> { + let mut client = self.connect_client(None).await?; + let response = client.create_key(KeyBranch::Nonce).await.map_err(|e| anyhow!(e))?; + Ok((response.public_key, response.id)) } pub fn instance(&self) -> &Instance { diff --git a/applications/tari_validator_node/Cargo.toml b/applications/tari_validator_node/Cargo.toml index 33ada6e52e..7b07133b5d 100644 --- a/applications/tari_validator_node/Cargo.toml +++ b/applications/tari_validator_node/Cargo.toml @@ -37,7 +37,6 @@ tari_validator_node_client = { workspace = true } tari_base_node_client = { workspace = true } tari_template_manager = { workspace = true } tari_epoch_manager = { workspace = true, features = ["service"] } -tari_indexer_lib = { workspace = true } tari_rpc_state_sync = { workspace = true } tari_bor = { workspace = true, default-features = true } tari_consensus = { workspace = true } @@ -47,7 +46,6 @@ tari_rpc_framework = { workspace = true } tari_template_builtin = { workspace = true } tari_swarm = { workspace = true } tari_state_tree = { workspace = true } -tari_sidechain = { workspace = true } sqlite_message_logger = { workspace = true } diff --git a/applications/tari_validator_node/src/bootstrap.rs b/applications/tari_validator_node/src/bootstrap.rs index 2f34771f84..9443c2302b 100644 --- a/applications/tari_validator_node/src/bootstrap.rs +++ b/applications/tari_validator_node/src/bootstrap.rs @@ -49,7 +49,6 @@ use tari_epoch_oracles::{ store::EpochOracleStore, EpochOracle, }; -use tari_indexer_lib::substate_scanner::SubstateScanner; use tari_networking::{MessagingMode, NetworkingHandle, RelayCircuitLimits, RelayReservationLimits, SwarmConfig}; use tari_ootle_app_utilities::{ common::verify_correct_network, @@ -57,7 +56,6 @@ use tari_ootle_app_utilities::{ epoch_oracle_config::{BaseLayerOracleConfig, EpochOracleType}, keypair::RistrettoKeypair, seed_peer::SeedPeer, - substate_file_cache::SubstateFileCache, template_download_queue::TemplateDownloadQueue, transaction_executor::TariTransactionProcessor, utxo_store::StateUtxoStore, @@ -81,7 +79,6 @@ use tokio::{ use crate::consensus::metrics::PrometheusConsensusMetrics; use crate::{ consensus::{self, ConsensusHandle, TarBlockTransactionExecutor, ValidationContext}, - dry_run_transaction_processor::DryRunTransactionProcessor, file_l1_submitter::FileLayerOneSubmitter, p2p::{ create_tari_validator_node_rpc_service, @@ -93,7 +90,6 @@ use crate::{ NopLogger, }, state_bootstrap::bootstrap_state, - substate_resolver::TariSubstateResolver, transaction_validators::{ EpochRangeValidator, FeeTransactionValidator, @@ -367,19 +363,6 @@ pub async fn spawn_services( ); handles.push(join_handle); - // substate cache - let substate_cache_dir = config.common.base_path.join("substate_cache"); - let substate_cache = SubstateFileCache::new(substate_cache_dir) - .map_err(|e| ExitError::new(ExitCode::ConfigError, format!("Substate cache error: {}", e)))?; - - // Dry-run services (TODO: should we implement dry-run on validator nodes, or just keep it in the indexer?) - let scanner = SubstateScanner::new( - epoch_manager.clone(), - validator_node_client_factory.clone(), - substate_cache, - ); - let substate_resolver = TariSubstateResolver::new(state_store.clone(), scanner); - spawn_p2p_rpc( &config, &mut networking, @@ -393,9 +376,6 @@ pub async fn spawn_services( // changed by comms during initialization when using tor. save_identities(&config, &keypair)?; - let dry_run_transaction_processor = - DryRunTransactionProcessor::new(epoch_manager.clone(), payload_processor, substate_resolver); - Ok(Services { config, keypair, @@ -406,7 +386,6 @@ pub async fn spawn_services( template_manager: template_manager_service, consensus_handle, state_store, - dry_run_transaction_processor, handles, layer_one_transaction_submitter, }) @@ -431,7 +410,6 @@ pub struct Services { pub epoch_manager: EpochManagerHandle, pub template_manager: TemplateManagerHandle, pub consensus_handle: ConsensusHandle, - pub dry_run_transaction_processor: DryRunTransactionProcessor, pub state_store: TStore, pub global_db: GlobalDb>, pub layer_one_transaction_submitter: FileLayerOneSubmitter, diff --git a/applications/tari_validator_node/src/consensus/block_transaction_executor.rs b/applications/tari_validator_node/src/consensus/block_transaction_executor.rs index 495f5233ba..44f1169fd5 100644 --- a/applications/tari_validator_node/src/consensus/block_transaction_executor.rs +++ b/applications/tari_validator_node/src/consensus/block_transaction_executor.rs @@ -105,7 +105,7 @@ where .result .finalize .result - .accept() + .any_accept() .map(|diff| { diff.up_iter() .map(|(addr, substate)| { diff --git a/applications/tari_validator_node/src/dry_run_transaction_processor.rs b/applications/tari_validator_node/src/dry_run_transaction_processor.rs deleted file mode 100644 index da98667c68..0000000000 --- a/applications/tari_validator_node/src/dry_run_transaction_processor.rs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2022. The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use log::info; -use tari_engine::state_store::{new_memory_store, StateStoreError}; -use tari_engine_types::{ - commit_result::ExecuteResult, - virtual_substate::{VirtualSubstate, VirtualSubstateId, VirtualSubstates}, -}; -use tari_epoch_manager::{service::EpochManagerHandle, EpochManagerError, EpochManagerReader}; -use tari_ootle_app_utilities::{ - substate_file_cache::SubstateFileCache, - transaction_executor::{TariTransactionProcessor, TransactionExecutor, TransactionProcessorError}, -}; -use tari_ootle_common_types::PeerAddress; -use tari_ootle_storage::{StateStore, StorageError}; -use tari_rpc_framework::RpcStatus; -use tari_template_manager::implementation::TemplateManager; -use tari_transaction::Transaction; -use tari_validator_node_client::ValidatorNodeClientError; -use tari_validator_node_rpc::client::TariValidatorNodeRpcClientFactory; -use thiserror::Error; -use tokio::task; - -use crate::{ - p2p::services::mempool::{ResolvedSubstates, SubstateResolver}, - substate_resolver::{SubstateResolverError, TariSubstateResolver}, -}; - -const LOG_TARGET: &str = "tari::ootle::validator_node::dry_run_transaction_processor"; - -#[derive(Error, Debug)] -pub enum DryRunTransactionProcessorError { - #[error("PayloadProcessor error: {0}")] - PayloadProcessor(#[from] TransactionProcessorError), - #[error("Storage error: {0}")] - Storage(#[from] StorageError), - #[error("EpochManager error: {0}")] - EpochManager(#[from] EpochManagerError), - #[error("Validator node client error: {0}")] - ValidatorNodeClient(#[from] ValidatorNodeClientError), - #[error("Rpc error: {0}")] - RpcRequestFailed(#[from] RpcStatus), - #[error("State store error: {0}")] - StateStoreError(#[from] StateStoreError), - #[error("Substate resolver error: {0}")] - SubstateResoverError(#[from] SubstateResolverError), - #[error("Execution thread failed: {0}")] - ExecutionThreadFailed(#[from] task::JoinError), -} - -#[derive(Clone, Debug)] -pub struct DryRunTransactionProcessor { - substate_resolver: TariSubstateResolver< - TStateStore, - EpochManagerHandle, - TariValidatorNodeRpcClientFactory, - SubstateFileCache, - >, - epoch_manager: EpochManagerHandle, - payload_processor: TariTransactionProcessor>, -} - -impl + Send + Sync> DryRunTransactionProcessor { - pub fn new( - epoch_manager: EpochManagerHandle, - payload_processor: TariTransactionProcessor>, - substate_resolver: TariSubstateResolver< - TStateStore, - EpochManagerHandle, - TariValidatorNodeRpcClientFactory, - SubstateFileCache, - >, - ) -> Self { - Self { - substate_resolver, - epoch_manager, - payload_processor, - } - } - - pub async fn process_transaction( - &self, - transaction: Transaction, - ) -> Result { - // Resolve all local and foreign substates - let mut temp_state_store = new_memory_store(); - - // TODO: the current epoch should come from consensus - let current_epoch = self.epoch_manager.current_epoch().await?; - let mut virtual_substates = VirtualSubstates::new(); - virtual_substates.insert( - VirtualSubstateId::CurrentEpoch, - VirtualSubstate::CurrentEpoch(current_epoch.as_u64()), - ); - - let ResolvedSubstates { - local: inputs, - unresolved_foreign: foreign, - } = self.substate_resolver.try_resolve_local(&transaction)?; - temp_state_store.set_many(inputs)?; - // Dry-run we can request the foreign inputs from validator nodes. The execution result may vary if inputs are - // mutated between the dry-run and live execution. - let foreign_inputs = self.substate_resolver.try_resolve_foreign(&foreign).await?; - temp_state_store.set_many(foreign_inputs)?; - - // execute the payload in the WASM engine and return the result - let processor = self.payload_processor.clone(); - let exec_output = task::spawn_blocking(move || { - processor.execute(&transaction, temp_state_store.into_read_only(), virtual_substates) - }) - .await??; - let result = exec_output.result; - - let fees = &result.finalize.fee_receipt; - info!(target: LOG_TARGET, "Transaction fees: {}", fees.total_fees_charged()); - - Ok(result) - } -} diff --git a/applications/tari_validator_node/src/json_rpc/handlers.rs b/applications/tari_validator_node/src/json_rpc/handlers.rs index 714089c5d7..8301f52722 100644 --- a/applications/tari_validator_node/src/json_rpc/handlers.rs +++ b/applications/tari_validator_node/src/json_rpc/handlers.rs @@ -62,7 +62,6 @@ use tari_ootle_storage::{ StorageError, }; use tari_ootle_storage_sqlite::global::SqliteGlobalDbAdapter; -use tari_sidechain::QuorumDecision; use tari_template_lib::prelude::{RistrettoPublicKeyBytes, Scalar32Bytes, SchnorrSignatureBytes}; use tari_template_manager::interface::TemplateManagerHandle; use tari_validator_node_client::types::{ @@ -70,7 +69,6 @@ use tari_validator_node_client::types::{ AddPeerRequest, AddPeerResponse, ConnectionDirection, - DryRunTransactionFinalizeResult, FunctionDef, GetAllVnsRequest, GetAllVnsResponse, @@ -116,7 +114,6 @@ use tari_validator_node_client::types::{ use crate::{ bootstrap::Services, consensus::{spec::ValidatorNodeStateStore, ConsensusHandle}, - dry_run_transaction_processor::DryRunTransactionProcessor, file_l1_submitter::FileLayerOneSubmitter, json_rpc::jrpc_errors::{general_error, internal_error, invalid_operation, not_found}, p2p::services::mempool::MempoolHandle, @@ -136,7 +133,6 @@ pub struct JsonRpcHandlers { consensus: ConsensusHandle, networking: NetworkingHandle, state_store: ValidatorNodeStateStore, - dry_run_transaction_processor: DryRunTransactionProcessor, } impl JsonRpcHandlers { @@ -152,7 +148,6 @@ impl JsonRpcHandlers { layer_one_transaction_submitter: services.layer_one_transaction_submitter.clone(), networking: services.networking.clone(), state_store: services.state_store.clone(), - dry_run_transaction_processor: services.dry_run_transaction_processor.clone(), } } @@ -183,10 +178,7 @@ impl JsonRpcHandlers { pub async fn submit_transaction(&self, value: JsonRpcExtractor) -> JrpcResult { let answer_id = value.get_answer_id(); - let SubmitTransactionRequest { - transaction, - is_dry_run, - } = value.parse_params()?; + let SubmitTransactionRequest { transaction } = value.parse_params()?; debug!( target: LOG_TARGET, "Transaction {} has {} involved shards", @@ -196,31 +188,11 @@ impl JsonRpcHandlers { let tx_id = transaction.calculate_id(); - if is_dry_run { - let result = self - .dry_run_transaction_processor - .process_transaction(transaction) - .await; - match result { - Ok(exec_result) => { - let response = SubmitTransactionResponse { - transaction_id: tx_id, - dry_run_result: Some(DryRunTransactionFinalizeResult { - decision: QuorumDecision::Accept, - fee_breakdown: Some(exec_result.finalize.fee_receipt.to_cost_breakdown()), - finalize: exec_result.finalize, - }), - }; - - return Ok(JsonRpcResponse::success(answer_id, response)); - }, - Err(e) => { - return Err(JsonRpcResponse::error( - answer_id, - JsonRpcError::new(JsonRpcErrorReason::ApplicationError(1), e.to_string(), json!(null)), - )); - }, - } + if transaction.is_dry_run() { + return Err(invalid_operation( + answer_id, + "Dry-run transactions cannot be submitted via this endpoint.", + )); } // Submit to mempool. diff --git a/applications/tari_validator_node/src/lib.rs b/applications/tari_validator_node/src/lib.rs index e8386b3440..555d5effe1 100644 --- a/applications/tari_validator_node/src/lib.rs +++ b/applications/tari_validator_node/src/lib.rs @@ -23,8 +23,8 @@ mod bootstrap; mod config; pub mod consensus; -mod dry_run_transaction_processor; mod event_subscription; +mod file_l1_submitter; #[cfg(feature = "web_ui")] mod http_ui; mod json_rpc; @@ -32,9 +32,6 @@ mod json_rpc; mod metrics; mod node; mod p2p; -mod substate_resolver; - -mod file_l1_submitter; mod state_bootstrap; pub mod transaction_validators; mod validator; diff --git a/applications/tari_validator_node/src/p2p/rpc/service_impl.rs b/applications/tari_validator_node/src/p2p/rpc/service_impl.rs index ef999d8611..8aa77d288a 100644 --- a/applications/tari_validator_node/src/p2p/rpc/service_impl.rs +++ b/applications/tari_validator_node/src/p2p/rpc/service_impl.rs @@ -293,7 +293,6 @@ impl ValidatorNodeRpcSe None => { let epoch = req .epoch - .clone() .map(Epoch::from) .map(|end| end.min(current_epoch)) .unwrap_or(current_epoch); diff --git a/applications/tari_validator_node/src/p2p/services/mempool/error.rs b/applications/tari_validator_node/src/p2p/services/mempool/error.rs index 1dbd3859b9..bd3887d0ed 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/error.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/error.rs @@ -6,11 +6,7 @@ use tari_networking::NetworkingError; use tari_ootle_storage::StorageError; use tokio::sync::{mpsc, oneshot}; -use crate::{ - dry_run_transaction_processor::DryRunTransactionProcessorError, - p2p::services::mempool::MempoolRequest, - transaction_validators::TransactionValidationError, -}; +use crate::{p2p::services::mempool::MempoolRequest, transaction_validators::TransactionValidationError}; #[derive(thiserror::Error, Debug)] pub enum MempoolError { @@ -22,8 +18,6 @@ pub enum MempoolError { RequestCancelled, #[error("Consensus channel closed")] ConsensusChannelClosed, - #[error("DryRunTransactionProcessor Error: {0}")] - DryRunTransactionProcessorError(#[from] DryRunTransactionProcessorError), #[error("Storage Error: {0}")] StorageError(#[from] StorageError), #[error("Transaction validation error: {0}")] diff --git a/applications/tari_validator_node/src/p2p/services/mempool/mod.rs b/applications/tari_validator_node/src/p2p/services/mempool/mod.rs index 7f2b66e721..6d40c18622 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/mod.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/mod.rs @@ -32,7 +32,5 @@ pub use gossip::TOPIC_PREFIX; #[cfg(feature = "metrics")] mod metrics; mod service; -mod traits; pub use error::*; -pub use traits::*; diff --git a/applications/tari_validator_node/src/p2p/services/mempool/traits.rs b/applications/tari_validator_node/src/p2p/services/mempool/traits.rs deleted file mode 100644 index 982e432a4a..0000000000 --- a/applications/tari_validator_node/src/p2p/services/mempool/traits.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use std::{collections::HashSet, future::Future}; - -use indexmap::IndexMap; -use tari_engine_types::substate::{Substate, SubstateId}; -use tari_ootle_common_types::SubstateRequirement; -use tari_transaction::Transaction; - -pub struct ResolvedSubstates { - pub local: IndexMap, - pub unresolved_foreign: HashSet, -} - -pub trait SubstateResolver { - type Error: Send + Sync + 'static; - - fn try_resolve_local(&self, transaction: &Transaction) -> Result; - - fn try_resolve_foreign( - &self, - requested_substates: &HashSet, - ) -> impl Future, Self::Error>> + Send; -} diff --git a/applications/tari_validator_node/src/state_bootstrap.rs b/applications/tari_validator_node/src/state_bootstrap.rs index b8febd3896..4d8350b3cd 100644 --- a/applications/tari_validator_node/src/state_bootstrap.rs +++ b/applications/tari_validator_node/src/state_bootstrap.rs @@ -30,15 +30,15 @@ use tari_state_tree::Version; use tari_template_lib::{ auth::{ComponentAccessRules, OwnerRule, ResourceAccessRules}, constants::{ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, NFT_FAUCET_COMPONENT_ADDRESS, NFT_FAUCET_RESOURCE_ADDRESS, PUBLIC_IDENTITY_RESOURCE_ADDRESS, + STEALTH_TARI_RESOURCE_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_VAULT_ADDRESS, }, models::Metadata, - prelude::ResourceType, + prelude::{ResourceManager, ResourceType}, resource::TOKEN_SYMBOL, rule, types::EntityId, @@ -80,15 +80,21 @@ where let is_testnet = !matches!(network, Network::MainNet); let symbol = if is_testnet { "tXTR" } else { "XTR" }; let xtr_resource = Resource::new( - ResourceType::Confidential, + ResourceType::Stealth, None, OwnerRule::None, - ResourceAccessRules::new(), + ResourceAccessRules::new() + // These are defaults, but just for explicitness + .mintable(rule!(deny_all)) + .burnable(rule!(deny_all)) + .recallable(rule!(deny_all)) + .freezable(rule!(deny_all)) + .update_access_rules(rule!(deny_all)), Metadata::from([(TOKEN_SYMBOL, symbol)]), None, None, 6, - false, + true, ); if is_testnet { @@ -98,7 +104,7 @@ where create_nft_faucet(tx, num_preshards)?; } - create_substate(tx, num_preshards, CONFIDENTIAL_TARI_RESOURCE_ADDRESS, xtr_resource)?; + create_substate(tx, num_preshards, STEALTH_TARI_RESOURCE_ADDRESS, xtr_resource)?; Ok(()) } @@ -117,18 +123,20 @@ where access_rules: ComponentAccessRules::allow_all(), entity_id: EntityId::default(), body: ComponentBody { - state: cbor!({"vault" => XTR_FAUCET_VAULT_ADDRESS}).unwrap(), + 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::Confidential { - address: CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - commitments: Default::default(), + let value = Vault::new(ResourceContainer::Stealth { + address: STEALTH_TARI_RESOURCE_ADDRESS, // just under 18.5 trillion tXTR revealed_amount: u64::MAX.into(), - locked_commitments: Default::default(), - locked_revealed_amount: Default::default(), + locked_amount: Default::default(), }); create_substate(tx, num_preshards, XTR_FAUCET_VAULT_ADDRESS, value)?; diff --git a/applications/tari_validator_node/src/substate_resolver.rs b/applications/tari_validator_node/src/substate_resolver.rs deleted file mode 100644 index d10e5f9627..0000000000 --- a/applications/tari_validator_node/src/substate_resolver.rs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2023 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use std::{collections::HashSet, time::Instant}; - -use indexmap::IndexMap; -use log::*; -use tari_engine::state_store::StateStoreError; -use tari_engine_types::substate::{Substate, SubstateId}; -use tari_epoch_manager::{EpochManagerError, EpochManagerReader}; -use tari_indexer_lib::{error::IndexerError, substate_cache::SubstateCache, substate_scanner::SubstateScanner}; -use tari_ootle_common_types::SubstateRequirement; -use tari_ootle_storage::{consensus_models::SubstateRecord, StateStore, StorageError}; -use tari_transaction::Transaction; -use tari_validator_node_rpc::client::{SubstateResult, ValidatorNodeClientFactory}; - -use crate::p2p::services::mempool::{ResolvedSubstates, SubstateResolver}; - -const LOG_TARGET: &str = "tari::ootle::substate_resolver"; - -#[derive(Debug, Clone)] -pub struct TariSubstateResolver { - store: TStateStore, - scanner: SubstateScanner, -} - -impl - TariSubstateResolver -where - TStateStore: StateStore, - TEpochManager: EpochManagerReader, - TValidatorNodeClientFactory: ValidatorNodeClientFactory, - TSubstateCache: SubstateCache, -{ - pub fn new( - store: TStateStore, - scanner: SubstateScanner, - ) -> Self { - Self { store, scanner } - } - - fn resolve_local_substates(&self, transaction: &Transaction) -> Result { - let inputs = transaction.all_inputs_substate_ids_iter(); - let (found_local_substates, missing_substate_ids) = self - .store - .with_read_tx(|tx| SubstateRecord::get_any_max_version(tx, inputs))?; - - // Reconcile requested inputs with found local substates - let mut missing_substates = HashSet::with_capacity(missing_substate_ids.len()); - for requested_input in transaction.all_inputs_iter() { - if missing_substate_ids.contains(requested_input.substate_id()) { - // TODO/NOTE: This assumes that consensus is up to date (i.e. doesnt need to sync, or catch up). We need - // to check the if the substate is in our shard range. The best action then may be to - // let consensus handle it which is what happens currently anyway. - missing_substates.insert(requested_input); - // Not a local substate, so we will need to fetch it remotely - continue; - } - - match requested_input.version() { - // Specific version requested - Some(requested_version) => { - let maybe_match = found_local_substates - .iter() - .find(|s| s.substate_id() == requested_input.substate_id()); - - match maybe_match { - Some(substate) => { - if substate.version() < requested_version { - return Err(SubstateResolverError::InputSubstateDoesNotExist { - substate_requirement: requested_input.to_owned(), - }); - } - - if substate.is_destroyed() || substate.version() > requested_version { - return Err(SubstateResolverError::InputSubstateDowned { - id: requested_input.substate_id().clone(), - version: requested_version, - }); - } - - // OK - }, - // Requested substate or version not found. We know that the requested substate is not foreign - // because we checked missing_substate_ids - None => { - return Err(SubstateResolverError::InputSubstateDoesNotExist { - substate_requirement: requested_input.to_owned(), - }); - }, - } - }, - // No version specified, so we will use the latest version - None => { - let substate = found_local_substates - .iter() - .find(| s| s.substate_id() == requested_input.substate_id()) - // This is not possible - .ok_or_else(|| { - error!( - target: LOG_TARGET, - "🐞 BUG: Requested substate {} was not missing but was also not found", - requested_input.substate_id() - ); - SubstateResolverError::InputSubstateDoesNotExist { substate_requirement: requested_input.to_owned() } - })?; - - // Latest version is DOWN - if substate.is_destroyed() { - return Err(SubstateResolverError::InputSubstateDowned { - id: requested_input.substate_id().clone(), - version: substate.version(), - }); - } - - // User did not specify the version, so we will use the latest version - // Ok - }, - } - } - - info!( - target: LOG_TARGET, - "Found {} local substates and {} missing substates", - found_local_substates.len(), - missing_substate_ids.len(), - ); - - let mut substates = IndexMap::new(); - substates.extend(found_local_substates.into_iter().map(|s| { - ( - s.substate_id.clone(), - s.into_substate().expect("All substates already checked UP"), - ) - })); - - Ok(ResolvedSubstates { - local: substates, - unresolved_foreign: missing_substates.into_iter().map(|s| s.to_owned()).collect(), - }) - } - - async fn resolve_remote_substates( - &self, - requested_substates: &HashSet, - ) -> Result, SubstateResolverError> { - let mut substates = IndexMap::with_capacity(requested_substates.len()); - for substate_req in requested_substates { - let timer = Instant::now(); - let substate_result = self - .scanner - .get_substate(substate_req.substate_id(), substate_req.version()) - .await?; - - match substate_result { - SubstateResult::Up { id, substate, .. } => { - info!( - target: LOG_TARGET, - "Retrieved substate {} in {}ms", - id, - timer.elapsed().as_millis() - ); - substates.insert(id, *substate); - }, - SubstateResult::Down { id, version, .. } => { - return Err(SubstateResolverError::InputSubstateDowned { id, version }); - }, - SubstateResult::DoesNotExist => { - return Err(SubstateResolverError::InputSubstateDoesNotExist { - substate_requirement: substate_req.clone(), - }); - }, - } - } - - Ok(substates) - } -} - -impl SubstateResolver - for TariSubstateResolver -where - TStateStore: StateStore + Sync + Send, - TEpochManager: EpochManagerReader, - TValidatorNodeClientFactory: ValidatorNodeClientFactory, - TSubstateCache: SubstateCache, -{ - type Error = SubstateResolverError; - - fn try_resolve_local(&self, transaction: &Transaction) -> Result { - self.resolve_local_substates(transaction) - } - - async fn try_resolve_foreign( - &self, - requested_substates: &HashSet, - ) -> Result, Self::Error> { - self.resolve_remote_substates(requested_substates).await - } -} - -#[derive(Debug, thiserror::Error)] -pub enum SubstateResolverError { - #[error("Storage error: {0}")] - StorageError(#[from] StorageError), - #[error("Indexer error: {0}")] - IndexerError(#[from] IndexerError), - #[error("Input substate does not exist: {substate_requirement}")] - InputSubstateDoesNotExist { substate_requirement: SubstateRequirement }, - #[error("Input substate is downed: {id} (version: {version})")] - InputSubstateDowned { id: SubstateId, version: u32 }, - #[error("Epoch manager error: {0}")] - EpochManagerError(#[from] EpochManagerError), - #[error("State store error: {0}")] - StateStorageError(#[from] StateStoreError), -} diff --git a/applications/tari_validator_node/web_ui/src/routes/VN/Components/Templates.tsx b/applications/tari_validator_node/web_ui/src/routes/VN/Components/Templates.tsx index d12c8f2620..4c63f2b6e1 100644 --- a/applications/tari_validator_node/web_ui/src/routes/VN/Components/Templates.tsx +++ b/applications/tari_validator_node/web_ui/src/routes/VN/Components/Templates.tsx @@ -138,14 +138,14 @@ function Templates() { {templates .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map(({ address, binary_sha, name }, i) => ( + .map(({ address, name }, i) => ( - {shortenString(address)} + template_{shortenString(address)} - + {name} Active diff --git a/applications/tari_validator_node_cli/src/command/account.rs b/applications/tari_validator_node_cli/src/command/account.rs index a38faecf53..49f29a4167 100644 --- a/applications/tari_validator_node_cli/src/command/account.rs +++ b/applications/tari_validator_node_cli/src/command/account.rs @@ -38,10 +38,7 @@ pub enum AccountsSubcommand { } #[derive(Debug, Args, Clone)] -pub struct CreateArgs { - #[clap(long, alias = "dry-run")] - pub is_dry_run: bool, -} +pub struct CreateArgs {} impl AccountsSubcommand { pub async fn handle>( @@ -59,7 +56,7 @@ impl AccountsSubcommand { } pub async fn handle_create( - args: CreateArgs, + _args: CreateArgs, base_dir: impl AsRef, client: &mut ValidatorNodeClient, ) -> Result { @@ -82,7 +79,6 @@ pub async fn handle_create( version: None, dump_outputs_into: None, account_template_address: None, - dry_run: args.is_dry_run, }; submit_transaction(vec![instruction], common, base_dir, client).await diff --git a/applications/tari_validator_node_cli/src/command/transaction.rs b/applications/tari_validator_node_cli/src/command/transaction.rs index 3bab275497..e241588cae 100644 --- a/applications/tari_validator_node_cli/src/command/transaction.rs +++ b/applications/tari_validator_node_cli/src/command/transaction.rs @@ -102,8 +102,6 @@ pub struct CommonSubmitArgs { pub dump_outputs_into: Option, #[clap(long, short = 'a')] pub account_template_address: Option, - #[clap(long)] - pub dry_run: bool, } #[derive(Debug, Args, Clone)] @@ -252,7 +250,7 @@ pub async fn submit_transaction( // Convert to shard id let inputs = inputs.into_iter().collect::>(); - summarize_request(&instructions, &inputs, 1, common.dry_run); + summarize_request(&instructions, &inputs); println!(); let transaction = Transaction::builder() @@ -260,10 +258,7 @@ pub async fn submit_transaction( .with_inputs(inputs) .build_and_seal(&key.secret_key); - let request = SubmitTransactionRequest { - transaction, - is_dry_run: common.dry_run, - }; + let request = SubmitTransactionRequest { transaction }; let mut resp = client.submit_transaction(request).await?; @@ -284,7 +279,7 @@ pub async fn submit_transaction( .await?; let result = transaction_execution.result(); if transaction_execution.decision().is_commit() { - if let Some(diff) = result.finalize.result.accept() { + if let Some(diff) = result.finalize.result.any_accept() { component_manager.commit_diff(diff)?; } } @@ -332,12 +327,7 @@ async fn wait_for_transaction_result( } } -fn summarize_request(instructions: &[Instruction], inputs: &[SubstateRequirement], fee: u64, is_dry_run: bool) { - if is_dry_run { - println!("NOTE: Dry run is enabled. This transaction will not be processed by the network."); - println!(); - } - println!("Fee: {}", fee); +fn summarize_request(instructions: &[Instruction], inputs: &[SubstateRequirement]) { println!("Inputs:"); if inputs.is_empty() { println!(" None"); diff --git a/applications/tari_wallet_cli/src/command/account.rs b/applications/tari_wallet_cli/src/command/account.rs index ea9421f190..89c0ded887 100644 --- a/applications/tari_wallet_cli/src/command/account.rs +++ b/applications/tari_wallet_cli/src/command/account.rs @@ -20,12 +20,8 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{fs, io, path::PathBuf}; - -use anyhow::anyhow; use clap::{Args, Subcommand}; -use serde_json as json; -use tari_ootle_common_types::{displayable::Displayable, optional::Optional}; +use tari_ootle_common_types::displayable::Displayable; use tari_wallet_daemon_client::{ types::{ AccountInfo, @@ -33,8 +29,6 @@ use tari_wallet_daemon_client::{ AccountsCreateOrGetRequest, AccountsCreateRequest, AccountsGetBalancesRequest, - ClaimBurnProof, - ClaimBurnRequest, RevealFundsRequest, }, ComponentAddressOrName, @@ -52,7 +46,6 @@ pub enum AccountsSubcommand { List, Get(GetArgs), - ClaimBurn(ClaimBurnArgs), #[clap(alias = "reveal")] RevealFunds(RevealFundsArgs), #[clap(alias = "faucet")] @@ -85,21 +78,6 @@ pub struct GetArgs { pub name: ComponentAddressOrName, } -#[derive(Debug, Args, Clone)] -pub struct ClaimBurnArgs { - #[clap(long, short = 'a', alias = "account")] - account: Option, - #[clap(long, short = 'i', alias = "input")] - proof_file: Option, - /// Optional proof JSON from the L1 console wallet. If not provided, you will be prompted to enter it. - #[clap(long, short = 'j', alias = "json")] - proof_json: Option, - #[clap(long, short = 'f')] - fee: Option, - #[clap(long)] - key_id: Option, -} - #[derive(Debug, Args, Clone)] pub struct RevealFundsArgs { /// Amount of funds to reveal @@ -139,7 +117,6 @@ impl AccountsSubcommand { handle_list(&mut client).await?; }, AccountsSubcommand::Get(args) => handle_get(args, &mut client).await?, - AccountsSubcommand::ClaimBurn(args) => handle_claim_burn(args, &mut client).await?, AccountsSubcommand::RevealFunds(args) => handle_reveal_funds(args, &mut client).await?, AccountsSubcommand::CreateFreeTestCoins(args) => handle_create_free_test_coins(args, &mut client).await?, AccountsSubcommand::SetDefault(args) => handle_set_default(args, &mut client).await?, @@ -200,92 +177,6 @@ async fn handle_get_balances(args: GetBalancesArgs, client: &mut WalletDaemonCli Ok(()) } -pub async fn handle_claim_burn(args: ClaimBurnArgs, client: &mut WalletDaemonClient) -> Result<(), anyhow::Error> { - let ClaimBurnArgs { - account, - proof_json, - fee, - proof_file, - key_id, - } = args; - - let claim_proof: ClaimBurnProof = if let Some(proof_json) = proof_json { - json::from_value(proof_json).map_err(|e| anyhow!("Failed to serialize proof JSON: {}", e))? - } else if let Some(proof_file) = proof_file { - let mut file = fs::File::open(proof_file).map_err(|e| anyhow!("Failed to read proof file: {}", e))?; - json::from_reader(&mut file).map_err(|e| anyhow!("Failed to parse proof JSON: {}", e))? - } else { - println!( - "Please paste console wallet JSON output from claim_burn call in the terminal: Press once \ - done" - ); - - json::from_reader(&mut io::stdin()).map_err(|e| anyhow!("Failed to parse proof JSON: {}", e))? - }; - - println!("✅ Claim burn submitted"); - let existing_account = match account { - Some(account) => { - let account_name = account.name().map(|s| s.to_string()); - match client.accounts_get(account).await.optional()? { - Some(resp) => resp.account, - None => { - let resp = client - .create_account(AccountsCreateRequest { - account_name, - is_default: None, - key_id, - }) - .await?; - resp.account - }, - } - }, - None => { - let existing_for_key = match key_id { - Some(id) => client - .accounts_get_by_key_index(id) - .await - .optional()? - .map(|resp| resp.account), - None => None, - }; - match existing_for_key { - Some(account) => account, - None => { - println!("No account found for the provided key_id or key_id is None. Creating a new account..."); - let resp = client - .create_account(AccountsCreateRequest { - account_name: None, - is_default: None, - key_id, - }) - .await?; - - resp.account - }, - } - }, - }; - - let req = ClaimBurnRequest { - account: existing_account.address.into(), - claim_proof, - max_fee: fee.map(Into::into), - }; - - let resp = client - .claim_burn(req) - .await - .map_err(|e| anyhow!("Failed to claim burn with error = {}", e.to_string()))?; - - println!("Total transaction fee: {}", resp.fee); - println!(); - - summarize_finalize_result(&resp.result); - Ok(()) -} - async fn handle_create_free_test_coins( args: CreateFreeTestCoinsArgs, client: &mut WalletDaemonClient, diff --git a/applications/tari_wallet_cli/src/command/transaction.rs b/applications/tari_wallet_cli/src/command/transaction.rs index d5f72648ce..7287caf103 100644 --- a/applications/tari_wallet_cli/src/command/transaction.rs +++ b/applications/tari_wallet_cli/src/command/transaction.rs @@ -45,7 +45,7 @@ use tari_ootle_wallet_sdk::apis::confidential_transfer::ConfidentialTransferInpu use tari_template_lib::{ args::InstructionArg, call_arg, - constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + constants::STEALTH_TARI_RESOURCE_ADDRESS, models::{BucketId, NonFungibleAddress, NonFungibleId}, prelude::{ResourceAddress, RistrettoPublicKeyBytes}, types::{Amount, TemplateAddress}, @@ -402,7 +402,7 @@ pub async fn handle_confidential_transfer( account: source_account, input_selection: ConfidentialTransferInputSelection::PreferConfidential, amount: amount.into(), - resource_address: resource_address.unwrap_or(CONFIDENTIAL_TARI_RESOURCE_ADDRESS), + resource_address: resource_address.unwrap_or(STEALTH_TARI_RESOURCE_ADDRESS), destination_public_key, max_fee: common.max_fee, output_to_revealed: false, diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index 484fabb1fb..986870c263 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -1,7 +1,7 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::collections::HashSet; +use std::{array, collections::HashSet}; use anyhow::anyhow; use axum::headers::authorization::Bearer; @@ -11,13 +11,17 @@ use rand::rngs::OsRng; use tari_crypto::{keys::PublicKey as _, ristretto::RistrettoPublicKey}; use tari_engine_types::{ component::derive_component_address_from_public_key, - confidential::ConfidentialClaim, + confidential::TariStealthClaim, substate::SubstateId, FromByteType, ToByteType, }; use tari_ootle_common_types::{optional::Optional, SubstateRequirement}; -use tari_ootle_wallet_crypto::UnblindedOutputStatement; +use tari_ootle_wallet_crypto::{ + UnblindedOutputStatement, + UnblindedStealthInputStatement, + UnblindedStealthOutputStatement, +}; use tari_ootle_wallet_sdk::{ apis::{ confidential_transfer::ConfidentialTransferParams, @@ -29,7 +33,7 @@ use tari_ootle_wallet_sdk::{ }; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::{ - constants::{CONFIDENTIAL_TARI_RESOURCE_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_VAULT_ADDRESS}, + constants::{STEALTH_TARI_RESOURCE_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_VAULT_ADDRESS}, models::UnclaimedConfidentialOutputAddress, prelude::ResourceType, types::Amount, @@ -65,6 +69,7 @@ use tari_wallet_daemon_client::{ ClaimBurnResponse, ConfidentialTransferRequest, ConfidentialTransferResponse, + ExtClaimBurnProof, RevealFundsRequest, RevealFundsResponse, StealthTransferRequest, @@ -379,10 +384,6 @@ pub async fn handle_reveal_funds( task::spawn(async move { let account = get_account_or_default(req.account.as_ref(), &sdk.accounts_api())?; - let vault = sdk - .accounts_api() - .get_vault_by_resource(account.address(), &CONFIDENTIAL_TARI_RESOURCE_ADDRESS)?; - let max_fee = req.max_fee.unwrap_or(DEFAULT_FEE); let amount_to_reveal = req.amount_to_reveal + if req.pay_fee_from_reveal { @@ -391,43 +392,59 @@ pub async fn handle_reveal_funds( 0.into() }; - let proof_id = sdk.confidential_outputs_api().add_output_lock(&vault.id)?; + let lock_id = sdk.stealth_outputs_api().create_lock()?; - let (inputs, input_amount) = - sdk.confidential_outputs_api() - .lock_outputs_by_amount(proof_id, &vault.id, amount_to_reveal)?; + let (inputs, input_amount) = sdk.stealth_outputs_api().lock_outputs_for_at_least_amount( + account.address(), + &STEALTH_TARI_RESOURCE_ADDRESS, + lock_id, + amount_to_reveal, + )?; let account_key = sdk.key_manager_api().derive_account_key(account.key_index())?; + let account_public_key = RistrettoPublicKey::from_secret_key(&account_key.key); - let output_mask = sdk.key_manager_api().next_key(KeyBranch::ConfidentialMasks)?; - let (_, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); + let output_mask = sdk.key_manager_api().next_key(KeyBranch::StealthMasks)?; + let (nonce, public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); let remaining_confidential_amount = input_amount - amount_to_reveal; - let encrypted_data = sdk.confidential_crypto_api().encrypt_value_and_mask( + let encrypted_data = sdk.stealth_crypto_api().encrypt_value_and_mask( remaining_confidential_amount.to_u64_checked().unwrap(), &output_mask.key, &public_nonce, &account_key.key, )?; - let output_statement = UnblindedOutputStatement { - amount: remaining_confidential_amount, - mask: output_mask.key, - sender_public_nonce: public_nonce, - minimum_value_promise: 0, - encrypted_data, - resource_view_key: None, + let network = sdk.config_api().get_network()?; + let tag = sdk + .stealth_crypto_api() + .derive_stealth_output_tag(network, &account.owner_public_key); + let stealth_address = + sdk.stealth_crypto_api() + .derive_stealth_owner_public_key(network, &account_public_key, &nonce); + + let output_statement = UnblindedStealthOutputStatement { + statement: UnblindedOutputStatement { + amount: remaining_confidential_amount, + mask: output_mask.key, + sender_public_nonce: public_nonce, + minimum_value_promise: 0, + encrypted_data, + resource_view_key: None, + }, + output_owner_public_key: stealth_address, + tag, }; - let inputs = sdk.confidential_outputs_api().resolve_output_masks(inputs)?; + let inputs = sdk + .stealth_outputs_api() + .resolve_output_masks_for_spending(account.account(), inputs)?; - let reveal_proof = sdk.confidential_crypto_api().generate_withdraw_proof( - &inputs, + let transfer = sdk.stealth_crypto_api().generate_transfer_statement( + inputs.iter().map(|i| &i.statement), Amount::zero(), - Some(&output_statement), + array::from_ref(&output_statement), amount_to_reveal, - None, - Amount::zero(), )?; info!( @@ -444,10 +461,7 @@ pub async fn handle_reveal_funds( if req.pay_fee_from_reveal { builder = builder.with_fee_instructions_builder(|builder| { builder - .call_method(account_address, "withdraw_confidential", args![ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - reveal_proof.clone() - ]) + .stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, transfer) .put_last_instruction_output_on_workspace("revealed") .call_method(account_address, "deposit", args![Workspace("revealed")]) .call_method(account_address, "pay_fee", args![max_fee]) @@ -455,10 +469,7 @@ pub async fn handle_reveal_funds( } else { builder = builder .fee_transaction_pay_from_component(account_address, max_fee) - .call_method(account_address, "withdraw_confidential", args![ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - reveal_proof - ]) + .stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, transfer) .put_last_instruction_output_on_workspace("revealed") .call_method(account_address, "deposit", args![Workspace("revealed")]); } @@ -475,8 +486,8 @@ pub async fn handle_reveal_funds( let transaction = builder.with_inputs(inputs).build_and_seal(&account_key.key); - sdk.confidential_outputs_api() - .locks_set_transaction_hash(proof_id, transaction.calculate_id())?; + sdk.stealth_outputs_api() + .locks_set_transaction_id(lock_id, transaction.calculate_id())?; let mut events = notifier.subscribe(); let tx_id = transaction_service.submit_transaction(transaction).await?; @@ -512,29 +523,44 @@ pub async fn handle_claim_burn( let max_fee = max_fee.unwrap_or(DEFAULT_FEE); - let ClaimBurnProof { - reciprocal_claim_public_key, - commitment, - ownership_proof, - range_proof, + let ExtClaimBurnProof { + owner_nonce_key_index, + claim_proof: + ClaimBurnProof { + reciprocal_claim_public_key, + commitment, + ownership_proof, + range_proof, + }, } = claim_proof; - // TODO: validate the proof_of_knowledge from the claim before submitting the transaction - - let mut inputs = vec![]; - let accounts_api = sdk.accounts_api(); let account = get_account(&account, &accounts_api)?; + let account_secret_key = sdk.key_manager_api().derive_account_key(account.key_index())?; - let account_substate = sdk.substate_api().get_substate(&(*account.address()).into())?; - inputs.push(account_substate.substate_id.into()); + let network = sdk.config_api().get_network()?; + let claim_nonce_keypair = sdk + .key_manager_api() + .derive_keypair(KeyBranch::Nonce, owner_nonce_key_index)?; + + if !sdk.stealth_crypto_api().validate_burn_claim_ownership_proof( + network, + &ownership_proof, + &commitment, + &claim_nonce_keypair.public_key.to_byte_type(), + ) { + return Err(invalid_params( + "claim_proof.ownership_proof", + Some("ownership proof validation failed"), + )); + } - let (account_secret_key, account_public_key) = sdk.key_manager_api().derive_account_keypair(account.key_index())?; + let mut inputs = vec![]; info!( target: LOG_TARGET, "ℹ️ Signing claim burn with key {}. NOTE: This must be the same as the claiming key used in the burn transaction for this to succeed.", - account_public_key + account.owner_public_key ); // Add all versioned account child addresses as inputs @@ -542,11 +568,6 @@ pub async fn handle_claim_burn( let address = UnclaimedConfidentialOutputAddress::from_commitment(&commitment); inputs.push(SubstateRequirement::unversioned(address)); - let child_addresses = sdk - .substate_api() - .load_dependent_substates(&[&(*account.address()).into()])?; - inputs.extend(child_addresses); - info!( target: LOG_TARGET, "Loaded {} inputs for claim burn transaction on account: {:?}", @@ -566,23 +587,27 @@ pub async fn handle_claim_burn( address, ) })?; + let reciprocal_claim_public_key_expanded = RistrettoPublicKey::try_from_byte_type(&reciprocal_claim_public_key) .map_err(|e| invalid_params("claim_proof.reciprocal_claim_public_key", Some(e)))?; - let unmasked_output = sdk.confidential_crypto_api().unblind_output( + let unmasked_output = sdk.stealth_crypto_api().unblind_output( &output.commitment, &output.encrypted_data, - &account_secret_key.key, + claim_nonce_keypair.secret_key(), &reciprocal_claim_public_key_expanded, )?; - let mask = sdk.key_manager_api().next_key(KeyBranch::ConfidentialMasks)?; - let (nonce, output_public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); + let mask = sdk.key_manager_api().next_key(KeyBranch::StealthMasks)?; let final_amount = unmasked_output .value - .checked_sub(max_fee.into()) + .checked_sub_positive(max_fee.into()) .ok_or_else(|| invalid_params("max_fee", Some("more fees paid than claimed amount")))?; + if final_amount.is_zero() { + return Err(invalid_params("max_fee", Some("fee equals or exceeds claimed amount"))); + } + let final_amount_u64 = final_amount.to_u64_checked().ok_or_else(|| { // NOTE: this can never be anywhere close to this large because this would be more than the total supply of XTM // for thousands of years @@ -592,69 +617,72 @@ pub async fn handle_claim_burn( ) })?; - // NOTE: the confidential encryption format currently does not support amounts larger than u64. Apart from it being - // insane/basically impossible to have that much in a single UTXO, the L1 emission will reach this much in many - // thousands of years. - let encrypted_data = sdk.confidential_crypto_api().encrypt_value_and_mask( + let (nonce, output_public_nonce) = RistrettoPublicKey::random_keypair(&mut OsRng); + let account_owner_public_key = RistrettoPublicKey::from_secret_key(&account_secret_key.key); + // NOTE: the confidential encryption format and the bullet proofs currently do not support amounts larger than + // u64::MAX. Apart from it being insane/basically impossible to have that much XTR in a single UTXO, the L1 emission + // will reach this much in many thousands of years. + let encrypted_data = sdk.stealth_crypto_api().encrypt_value_and_mask( final_amount_u64, &mask.key, - &account_public_key, + &account_owner_public_key, &nonce, )?; - let output_statement = UnblindedOutputStatement { - amount: final_amount, - mask: mask.key, - sender_public_nonce: output_public_nonce, - minimum_value_promise: 0, - encrypted_data, - resource_view_key: None, + let tag = sdk + .stealth_crypto_api() + .derive_stealth_output_tag(network, &account.owner_public_key); + + // Create stealth address - used during spend time + let stealth_output_owner_public_key = + sdk.stealth_crypto_api() + .derive_stealth_owner_public_key(network, &account_owner_public_key, &nonce); + + let output_statement = UnblindedStealthOutputStatement { + statement: UnblindedOutputStatement { + amount: final_amount, + mask: mask.key, + sender_public_nonce: output_public_nonce.clone(), + minimum_value_promise: 0, + encrypted_data, + resource_view_key: None, + }, + output_owner_public_key: stealth_output_owner_public_key, + tag, }; - let reveal_proof = sdk.confidential_crypto_api().generate_withdraw_proof( - &[unmasked_output], + // Generate the correct secret to spend the claimed output + let input = UnblindedStealthInputStatement { + mask_and_value: unmasked_output, + owner_secret: claim_nonce_keypair.secret_key().clone(), + public_nonce: reciprocal_claim_public_key_expanded, + }; + + let pay_fee_and_mint_output = sdk.stealth_crypto_api().generate_transfer_statement( + array::from_ref(&input), 0, - Some(&output_statement).filter(|o| !o.amount.is_zero()), + array::from_ref(&output_statement), max_fee, - None, - 0, )?; let transaction = context .transaction_builder() .with_fee_instructions_builder(|fee_builder| { fee_builder - .claim_burn(ConfidentialClaim { - public_key: reciprocal_claim_public_key, + .claim_burn(TariStealthClaim { + burn_public_key: reciprocal_claim_public_key, output_address: address, range_proof, proof_of_knowledge: ownership_proof, - withdraw_proof: Some(reveal_proof), - }) - .put_last_instruction_output_on_workspace("bucket") - .then(|builder| { - if account.is_confirmed_on_chain() { - builder.call_method(*account.address(), "deposit", args![Workspace("bucket")]) - } else { - // If the account is not on-chain yet, we create it - builder.create_account_with_bucket(account_public_key.to_byte_type(), "bucket") - } + transfer_statement: None, }) - .call_method(*account.address(), "pay_fee", args![Amount(max_fee)]) + .pay_fee_stealth(pay_fee_and_mint_output) }) - .with_inputs(inputs.into_iter().map(|input| input.into_unversioned())) - .build_and_seal(&account_secret_key.key); + .with_inputs(inputs) + .build_and_seal(claim_nonce_keypair.secret_key()); let mut events = context.notifier().subscribe(); - let tx_id = context - .transaction_service() - .submit_transaction_with_opts( - transaction, - account.is_confirmed_on_chain().then(|| NewAccountData { - address: *account.address(), - }), - ) - .await?; + let tx_id = context.transaction_service().submit_transaction(transaction).await?; // Wait for the monitor to pick up the new or updated account let finalized = wait_for_result(&mut events, tx_id).await?; @@ -676,7 +704,7 @@ pub async fn handle_claim_burn( }) } -/// Mints coins into an existing account from the testnet faucet. +/// Takes tXTR from the testnet faucet and deposits them into an existing account. #[allow(clippy::too_many_lines)] pub async fn handle_create_free_test_coins( context: &HandlerContext, diff --git a/applications/tari_walletd/src/handlers/confidential.rs b/applications/tari_walletd/src/handlers/confidential.rs index ecef0f579b..ff23bd8d80 100644 --- a/applications/tari_walletd/src/handlers/confidential.rs +++ b/applications/tari_walletd/src/handlers/confidential.rs @@ -59,8 +59,7 @@ pub async fn handle_create_transfer_proof( let vault = sdk .accounts_api() .get_vault_by_resource(account.address(), &req.resource_address)?; - let proof_id = sdk.confidential_outputs_api().add_output_lock(&vault.id)?; - // Lock inputs we're going to spend + let lock_id = sdk.confidential_outputs_api().create_lock()?; let amount_to_transfer = req.amount.checked_add_positive(req.reveal_amount).ok_or_else(|| { invalid_request(format!( @@ -68,15 +67,16 @@ pub async fn handle_create_transfer_proof( req.amount, req.reveal_amount )) })?; + // Lock inputs we're going to spend let (inputs, total_input_value) = sdk.confidential_outputs_api() - .lock_outputs_by_amount(proof_id, &vault.id, amount_to_transfer)?; + .lock_outputs_by_amount(lock_id, &vault.id, amount_to_transfer)?; info!( target: LOG_TARGET, "Locked {} inputs for proof {} worth {} µT", inputs.len(), - proof_id, + lock_id, total_input_value ); @@ -161,7 +161,7 @@ pub async fn handle_create_transfer_proof( encrypted_data: encrypted_data.clone(), public_asset_tag: None, status: OutputStatus::LockedUnconfirmed, - lock_id: Some(proof_id), + lock_id: Some(lock_id), })?; Some(UnblindedOutputStatement { @@ -188,7 +188,10 @@ pub async fn handle_create_transfer_proof( Amount::zero(), )?; - Ok(ProofsGenerateResponse { proof_id, proof }) + Ok(ProofsGenerateResponse { + proof_id: lock_id, + proof, + }) } pub async fn handle_finalize_transfer( @@ -212,6 +215,7 @@ pub async fn handle_cancel_transfer( ) -> Result { let sdk = context.wallet_sdk(); context.check_auth(token, &[JrpcPermission::Admin])?; + sdk.confidential_outputs_api().release_revealed_funds(req.proof_id)?; sdk.confidential_outputs_api().release_locked_outputs(req.proof_id)?; Ok(ProofsCancelResponse {}) } diff --git a/applications/tari_walletd/src/handlers/transaction.rs b/applications/tari_walletd/src/handlers/transaction.rs index cf9b593941..58f5dec517 100644 --- a/applications/tari_walletd/src/handlers/transaction.rs +++ b/applications/tari_walletd/src/handlers/transaction.rs @@ -155,7 +155,7 @@ pub async fn handle_submit( for proof_id in req.proof_ids { // update the proofs table with the corresponding transaction hash sdk.confidential_outputs_api() - .locks_set_transaction_hash(proof_id, tx_id)?; + .locks_set_transaction_id(proof_id, tx_id)?; } info!( @@ -241,7 +241,7 @@ pub async fn handle_submit_dry_run( for proof_id in req.proof_ids { // update the proofs table with the corresponding transaction hash sdk.confidential_outputs_api() - .locks_set_transaction_hash(proof_id, transaction.calculate_id())?; + .locks_set_transaction_id(proof_id, transaction.calculate_id())?; } info!( @@ -377,6 +377,7 @@ pub async fn handle_get( transaction: transaction.transaction, result: transaction.finalize, status: transaction.status, + invalid_reason: transaction.invalid_reason, last_update_time: transaction.last_update_time, }) } @@ -387,10 +388,11 @@ pub async fn handle_get_all( req: TransactionGetAllRequest, ) -> Result { context.check_auth(token, &[JrpcPermission::TransactionGet])?; - let transactions = context - .wallet_sdk() - .transaction_api() - .fetch_all(req.status, req.component)?; + let transactions = + context + .wallet_sdk() + .transaction_api() + .fetch_all(req.status, req.component, req.signer_public_key)?; Ok(TransactionGetAllResponse { transactions }) } diff --git a/applications/tari_walletd/src/lib.rs b/applications/tari_walletd/src/lib.rs index 293544e697..abcdd86a2b 100644 --- a/applications/tari_walletd/src/lib.rs +++ b/applications/tari_walletd/src/lib.rs @@ -45,6 +45,7 @@ use tari_ootle_wallet_sdk::{ }; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; use tari_shutdown::ShutdownSignal; +use tari_template_lib::constants::XTR; use crate::{ cli::Cli, @@ -76,6 +77,16 @@ pub async fn run_tari_ootle_walletd( wallet_sdk.key_manager_api().get_or_create_initial(KeyBranch::Account)?; + tokio::spawn({ + let wallet_sdk = wallet_sdk.clone(); + async move { + // Ensures that the XTR resource is available in the substate cache + if let Err(err) = wallet_sdk.substate_api().fetch_resource(XTR).await { + error!(target: LOG_TARGET, "Failed to fetch XTR resource: {}", err); + } + } + }); + let notify = Notify::new(100); let services = spawn_services(shutdown_signal.clone(), notify.clone(), wallet_sdk.clone()); diff --git a/applications/tari_walletd/src/services/account_monitor.rs b/applications/tari_walletd/src/services/account_monitor.rs index c2cc6d70c2..387e849230 100644 --- a/applications/tari_walletd/src/services/account_monitor.rs +++ b/applications/tari_walletd/src/services/account_monitor.rs @@ -34,7 +34,7 @@ use tari_shutdown::ShutdownSignal; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::{ models::{NonFungibleAddress, VaultId}, - prelude::{ComponentAddress, NonFungibleId, ResourceAddress}, + prelude::{ComponentAddress, NonFungibleId, ResourceAddress, XTR}, resource::TOKEN_SYMBOL, }; use tari_transaction::TransactionId; @@ -290,18 +290,11 @@ where } fn refresh_stealth_utxos(&self, account_address: ComponentAddress) -> Result<(), AccountMonitorError> { - let stealth_resources = self + let mut stealth_resources = self .wallet_sdk .accounts_api() .get_associated_stealth_resources(&account_address)?; - if stealth_resources.is_empty() { - debug!( - target: LOG_TARGET, - "👁️‍🗨️ No associated stealth resources for account {}", - account_address - ); - return Ok(()); - } + stealth_resources.insert(XTR); info!( target: LOG_TARGET, @@ -818,7 +811,7 @@ where } }, WalletEvent::TransactionFinalized(event) => { - if let Some(diff) = event.finalize.result.accept() { + if let Some(diff) = event.finalize.result.any_accept() { self.process_result(event.transaction_id, diff).await?; } }, diff --git a/applications/tari_walletd/src/services/template_monitor.rs b/applications/tari_walletd/src/services/template_monitor.rs index 635d3aeb70..337d69cca0 100644 --- a/applications/tari_walletd/src/services/template_monitor.rs +++ b/applications/tari_walletd/src/services/template_monitor.rs @@ -78,7 +78,7 @@ where async fn handle_wallet_event(&self, event: WalletEvent) -> anyhow::Result<()> { if let WalletEvent::TransactionFinalized(event) = event { - let Some(diff) = event.finalize.result.accept() else { + let Some(diff) = event.finalize.result.any_accept() else { return Ok(()); }; diff --git a/applications/tari_walletd/src/services/transaction_service/service.rs b/applications/tari_walletd/src/services/transaction_service/service.rs index 8e9287f87c..8a11c0b489 100644 --- a/applications/tari_walletd/src/services/transaction_service/service.rs +++ b/applications/tari_walletd/src/services/transaction_service/service.rs @@ -119,7 +119,7 @@ where let transaction_id = transaction.calculate_id(); let transaction_api = self.wallet_sdk.transaction_api(); // Unlock all locks related to the transaction immediately since this is a dry run - transaction_api.release_all_outputs_for_transaction(transaction_id)?; + transaction_api.release_all_locks_for_transaction(transaction_id)?; match transaction_api.submit_dry_run_transaction(transaction).await { Ok(finalized_transaction) => { let finalize = finalized_transaction.finalize.ok_or_else(|| { @@ -154,13 +154,21 @@ where let transaction_id = transaction_api .insert_new_transaction(transaction, new_account_info.clone(), false) .await?; - transaction_api.submit_transaction(transaction_id).await?; - - self.notify.notify(TransactionSubmittedEvent { - transaction_id, - new_account: new_account_info, - }); - Ok(transaction_id) + if transaction_api.submit_transaction(transaction_id).await? { + self.notify.notify(TransactionSubmittedEvent { + transaction_id, + new_account: new_account_info, + }); + Ok(transaction_id) + } else { + self.notify.notify(TransactionInvalidEvent { + transaction_id, + status: TransactionStatus::InvalidTransaction, + finalize: None, + final_fee: None, + }); + Ok(transaction_id) + } } async fn on_poll(&self) -> Result<(), TransactionServiceError> { @@ -176,7 +184,7 @@ where let notify = self.notify.clone(); tokio::spawn(async move { if let Err(err) = Self::resubmit_new_transactions(&wallet_sdk, ¬ify).await { - error!(target: LOG_TARGET, "Error re-submitting new transactions: {}", err); + error!(target: LOG_TARGET, "Error resubmitting new transactions: {}", err); } if let Err(err) = Self::check_pending_transactions(&wallet_sdk, ¬ify).await { error!(target: LOG_TARGET, "Error checking pending transactions: {}", err); @@ -192,7 +200,7 @@ where notify: &Notify, ) -> Result<(), TransactionServiceError> { let transaction_api = wallet_sdk.transaction_api(); - let new_transactions = transaction_api.fetch_all(Some(TransactionStatus::New), None)?; + let new_transactions = transaction_api.fetch_all(Some(TransactionStatus::New), None, None)?; let log_level = if new_transactions.is_empty() { Level::Debug } else { @@ -211,11 +219,19 @@ where transaction.id, ); let transaction_id = transaction.id; - transaction_api.submit_transaction(transaction_id).await?; - notify.notify(TransactionSubmittedEvent { - transaction_id, - new_account: transaction.new_account_info, - }); + if transaction_api.submit_transaction(transaction_id).await? { + notify.notify(TransactionSubmittedEvent { + transaction_id, + new_account: transaction.new_account_info, + }); + } else { + notify.notify(TransactionInvalidEvent { + transaction_id, + status: TransactionStatus::InvalidTransaction, + finalize: None, + final_fee: None, + }); + } } Ok(()) } @@ -225,7 +241,7 @@ where notify: &Notify, ) -> Result<(), TransactionServiceError> { let transaction_api = wallet_sdk.transaction_api(); - let pending_transactions = transaction_api.fetch_all(Some(TransactionStatus::Pending), None)?; + let pending_transactions = transaction_api.fetch_all(Some(TransactionStatus::Pending), None, None)?; let log_level = if pending_transactions.is_empty() { Level::Debug } else { diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useAccounts.ts b/applications/tari_walletd/web_ui/src/api/hooks/useAccounts.ts index 72abc7cac1..e155043274 100644 --- a/applications/tari_walletd/web_ui/src/api/hooks/useAccounts.ts +++ b/applications/tari_walletd/web_ui/src/api/hooks/useAccounts.ts @@ -208,9 +208,10 @@ export const useMintTestnetFaucetNfts = () => { onError: (error: ApiError) => { console.error(error); }, - onSettled: () => { + onSettled: (_resp, _err, req) => { queryClient.invalidateQueries(["transactions"]); queryClient.invalidateQueries(["accounts_balances"]); + queryClient.invalidateQueries(["nfts_list"]); }, }); }; @@ -274,20 +275,12 @@ export const useAccountsGet = (account: ComponentAddress) => { export const useAccountNFTsList = (account: ComponentAddress, offset: number, limit: number) => { return useQuery({ - queryKey: ["nfts_list_" + account + "_" + offset + "_" + limit], + queryKey: ["nfts_list", account, offset, limit], queryFn: () => nftList({ account: { ComponentAddress: account }, offset, limit }), onError: (_error: ApiError) => {}, - structuralSharing: (oldData, newData) => { - if (!oldData || !newData) return newData; - if (JSON.stringify(oldData) === JSON.stringify(newData)) { - return oldData; - } - return newData; - }, }); }; - export const useValidatorFees = (accountOrKeyIndex: AccountOrKeyIndex, shardGroup = null) => { return useQuery({ queryKey: ["validator_fees"], diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useKeys.tsx b/applications/tari_walletd/web_ui/src/api/hooks/useKeys.tsx index eecba50b3e..92ee70c9ee 100644 --- a/applications/tari_walletd/web_ui/src/api/hooks/useKeys.tsx +++ b/applications/tari_walletd/web_ui/src/api/hooks/useKeys.tsx @@ -24,12 +24,13 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { ApiError } from "../helpers/types"; import queryClient from "../queryClient"; import { keysCreate, keysList, keysSetActive } from "../../utils/json_rpc"; +import { KeyBranch } from "@tari-project/typescript-bindings"; -export const useKeysList = () => { +export const useKeysList = (branch: KeyBranch) => { return useQuery({ - queryKey: ["keys_list"], + queryKey: ["keys_list", branch], queryFn: () => { - return keysList({ branch: "transaction" }); + return keysList({ branch }); }, onError: (error: ApiError) => { error; @@ -37,8 +38,8 @@ export const useKeysList = () => { }); }; -export const useKeysCreate = () => { - return useMutation(() => keysCreate({ branch: "transaction", specific_index: null }), { +export const useKeysCreate = (branch: KeyBranch) => { + return useMutation(() => keysCreate({ branch, specific_index: null }), { onError: (error: ApiError) => { error; }, diff --git a/applications/tari_walletd/web_ui/src/api/hooks/useTransactions.tsx b/applications/tari_walletd/web_ui/src/api/hooks/useTransactions.tsx index 8879c12a5e..76d59fb161 100644 --- a/applications/tari_walletd/web_ui/src/api/hooks/useTransactions.tsx +++ b/applications/tari_walletd/web_ui/src/api/hooks/useTransactions.tsx @@ -32,7 +32,7 @@ import { import { ApiError } from "../helpers/types"; import queryClient from "../queryClient"; -import type { AccountOrKeyIndex, TransactionStatus } from "@tari-project/typescript-bindings"; +import type { AccountOrKeyIndex, TransactionGetAllRequest, TransactionStatus } from "@tari-project/typescript-bindings"; export const useTransactionDetails = (hash: string) => { return useQuery({ @@ -46,22 +46,15 @@ export const useTransactionDetails = (hash: string) => { }); }; -export const useGetAllTransactions = (status: TransactionStatus | null, component: string | null) => { +export const useGetAllTransactions = (req: TransactionGetAllRequest) => { return useQuery({ - queryKey: ["transactions"], - queryFn: () => transactionsGetAll({ status: status, component: component }), + queryKey: ["transactions", req.status], + queryFn: () => transactionsGetAll(req), onError: (error: ApiError) => { error; }, refetchInterval: 5000, keepPreviousData: true, - structuralSharing: (oldData, newData) => { - if (!oldData || !newData) return newData; - if (JSON.stringify(oldData) === JSON.stringify(newData)) { - return oldData; - } - return newData; - }, }); }; diff --git a/applications/tari_walletd/web_ui/src/routes/AccountDetails/AccountDetails.tsx b/applications/tari_walletd/web_ui/src/routes/AccountDetails/AccountDetails.tsx index bc4f0e667b..50355450fd 100644 --- a/applications/tari_walletd/web_ui/src/routes/AccountDetails/AccountDetails.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AccountDetails/AccountDetails.tsx @@ -34,7 +34,7 @@ import { useAccountsGetBalances, useAccountNFTsList, useAccountsGet } from "../. import { DataTableCell } from "../../Components/StyledComponents"; import FetchStatusCheck from "../../Components/FetchStatusCheck"; import { AccountGetResponse, BalanceEntry, substateIdToString } from "@tari-project/typescript-bindings"; -import NFTList from "../AssetVault/NFTs/NFTList"; +import NftList from "../AssetVault/NFTs/NFTList"; import CopyAddress from "../../Components/CopyAddress"; import useAccountStore from "../../store/accountStore"; import { Form, useParams } from "react-router-dom"; @@ -207,7 +207,7 @@ function AccountDetailsLayout() { Account NFTs - { + const onClaimBurn = async (e: FormEvent) => { + e.preventDefault(); try { setClaimBurnFormState({ ...claimBurnFormState, disabled: true }); - await accountsClaimBurn({ - account: { Name: claimBurnFormState.account }, + const resp = await accountsClaimBurn({ + account: { ComponentAddress: claimBurnFormState.account }, claim_proof: JSON.parse(claimBurnFormState.claimProof), max_fee: +claimBurnFormState.fee, }); + const waitResp = await transactionsWaitResult({ transaction_id: resp.transaction_id, timeout_secs: 30 }); + if (waitResp.status != "Accepted") { + throw new Error(`Transaction not accepted: ${waitResp.status}`); + } setOpen(false); setPopup({ title: "Claimed", error: false }); setClaimBurnFormState(INITIAL_FORM_STATE); diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx index 29f58784c9..6ca04b5544 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/ClaimFees.tsx @@ -73,7 +73,7 @@ export default function ClaimFees() { const [isLoading, setIsLoading] = useState(false); const [validity, setValidity] = useState<{ [key: string]: boolean }>(INITIAL_VALIDITY); const { data: dataAccountsList } = useAccountsList(0, 10); - const { data: dataKeysList } = useKeysList(); + const { data: dataKeysList } = useKeysList("account"); const { setPopup } = useAccountStore(); const theme = useTheme(); diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx index f8a5afcc2b..276ee8343d 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Components/MyAssets.tsx @@ -40,7 +40,7 @@ import { Refresh } from "@mui/icons-material"; function MyAssets() { const theme = useTheme(); - const account = useAccountStore((state) => state.account); + const { account, publicKey } = useAccountStore(); if (!account) { return <>Loading...; @@ -127,7 +127,7 @@ function MyAssets() { Transactions - + diff --git a/applications/tari_walletd/web_ui/src/routes/Onboarding/Onboarding.tsx b/applications/tari_walletd/web_ui/src/routes/Onboarding/Onboarding.tsx index 6f432fe65f..3abaeb3704 100644 --- a/applications/tari_walletd/web_ui/src/routes/Onboarding/Onboarding.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Onboarding/Onboarding.tsx @@ -109,7 +109,7 @@ function Onboarding() { textAlign: "center", }} > - Create your test account below to get started + Give your account a friendly name to get started
- + {template.name} {template.tari_version} diff --git a/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx b/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx index e2cbb0a97c..9d991699f1 100644 --- a/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx @@ -46,6 +46,7 @@ import { TransactionSignature, } from "@tari-project/typescript-bindings"; import { getRejectReasonFromTransactionResult, rejectReasonToString } from "@tari-project/typescript-bindings"; +import { BsQuestionCircle } from "react-icons/bs"; export default function TransactionDetails() { const [expandedPanels, setExpandedPanels] = useState([]); @@ -123,45 +124,7 @@ export default function TransactionDetails() { const seal_signature = data.transaction.V1?.seal_signature; const transaction_body = data.transaction.V1?.body; const transaction = transaction_body?.transaction; - - if (data.status === "Rejected" || data.status === "InvalidTransaction") { - return ( - <> - - - - - Transaction Hash - {transactionId} - - - Timestamp - {last_update_time.toLocaleString()} - - - Status - - - - - - JSON - - - - - - Reason - {getTransactionFailure(data?.result?.result)} - - -
-
- - ); - } + const feeReceipt = data.result?.fee_receipt; return ( @@ -180,7 +143,22 @@ export default function TransactionDetails() {
Total Fees - {data?.result?.fee_receipt.total_fees_paid.toString() || 0} + + {feeReceipt?.total_fees_paid.toString() || 0} + {feeReceipt?.total_fee_overcharge ? ( + <> + {" "} + ({feeReceipt.total_fee_overcharge} overcharge{" "} + + ) + + ) : ( + "" + )} + Status @@ -190,7 +168,9 @@ export default function TransactionDetails() { Result - {renderResult(data?.result)} + + {data.invalid_reason ? data.invalid_reason : renderResult(data?.result)} + JSON @@ -335,30 +315,24 @@ export default function TransactionDetails() { Signers - {transaction_body?.signatures?.length ? ( - - - - {seal_signature ? ( - - {seal_signature.public_key} - - ) : ( - <> - )} - {transaction_body.signatures.map((item: TransactionSignature, i: number) => { - return ( - - {item.public_key} - - ); - })} - -
-
- ) : ( - Empty - )} + + + + {seal_signature ? ( + + Sealed by: {seal_signature.public_key} + + ) : ( + <> + )} + {(transaction_body?.signatures ?? []).map((item: TransactionSignature, i: number) => ( + + {item.public_key} + + ))} + +
+
diff --git a/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx b/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx index f69e0a7582..be29b2ce32 100644 --- a/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Transactions/Transactions.tsx @@ -40,22 +40,29 @@ import StatusChip from "../../Components/StatusChip"; import { DataTableCell } from "../../Components/StyledComponents"; import { useGetAllTransactions } from "../../api/hooks/useTransactions"; import { emptyRows, handleChangePage, handleChangeRowsPerPage } from "../../utils/helpers"; -import { Account, substateIdToString, WalletTransaction } from "@tari-project/typescript-bindings"; +import { Account, WalletTransaction } from "@tari-project/typescript-bindings"; -export default function Transactions({ account }: { account: Account }) { +export default function Transactions({ account }: { account: Account; ownerPublicKey: string }) { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); - const { data, isLoading, error, isError, refetch } = useGetAllTransactions( - null, - account ? substateIdToString(account.address) : null, - ); + const { data, isLoading, error, isError, refetch, isRefetching } = useGetAllTransactions({ + status: null, + // Some stealth transactions cannot be identified by component/public key in the wallet - so we fetch all transactions. + // If this feature is badly needed, we can "tag" transactions as involving a specific account when they are created. + component: null, //: account ? substateIdToString(account.address) : null, + signer_public_key: null, //: ownerPublicKey ? ownerPublicKey : null, + }); useEffect(() => { refetch(); }, [account]); const theme = useTheme(); return ( - + @@ -71,7 +78,8 @@ export default function Transactions({ account }: { account: Account }) { {data?.transactions ?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) .map((transaction: WalletTransaction) => { - const { transaction: _, finalize: result, status, id: hash } = transaction; + const { finalize: result, status, id: hash } = transaction; + const { fee_receipt } = result || {}; return ( @@ -88,7 +96,7 @@ export default function Transactions({ account }: { account: Account }) { - {result?.fee_receipt.total_fees_paid.toString() || "--"} + {fee_receipt?.total_fees_paid.toString() || "--"} state.account); + const { account, publicKey } = useAccountStore(); return ( <> @@ -35,7 +35,7 @@ function TransactionsLayout() { - + diff --git a/applications/tari_walletd/web_ui/src/routes/Wallet/Components/Keys.tsx b/applications/tari_walletd/web_ui/src/routes/Wallet/Components/Keys.tsx index c4241bd7fa..bdd4970dbc 100644 --- a/applications/tari_walletd/web_ui/src/routes/Wallet/Components/Keys.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Wallet/Components/Keys.tsx @@ -48,9 +48,9 @@ function Key(key: [number, string, boolean], setActive: any) { function Keys() { const [showKeyDialog, setShowAddKeyDialog] = useState(false); - const { data, isLoading, isError, error } = useKeysList(); + const { data, isLoading, isError, error } = useKeysList("account"); const { mutate: mutateSetActive } = useKeysSetActive(); - const { mutate: mutateCreateKey } = useKeysCreate(); + const { mutate: mutateCreateKey } = useKeysCreate("account"); const showAddKeyDialog = (setElseToggle: boolean = !showKeyDialog) => { setShowAddKeyDialog(setElseToggle); diff --git a/applications/tari_walletd/web_ui/src/routes/Wallet/Wallet.tsx b/applications/tari_walletd/web_ui/src/routes/Wallet/Wallet.tsx index 79b0c049b5..883024bc5d 100644 --- a/applications/tari_walletd/web_ui/src/routes/Wallet/Wallet.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Wallet/Wallet.tsx @@ -31,7 +31,7 @@ import AccessTokens from "./Components/AccessTokens"; import useAccountStore from "../../store/accountStore"; function Wallet() { - const account = useAccountStore((state) => state.account); + const { account, publicKey } = useAccountStore(); return ( <> @@ -48,7 +48,7 @@ function Wallet() { - + diff --git a/bindings/src/index.ts b/bindings/src/index.ts index 4b855b8241..8c9f3b6b3d 100644 --- a/bindings/src/index.ts +++ b/bindings/src/index.ts @@ -25,7 +25,6 @@ export * from "./types/ComponentCall"; export * from "./types/ComponentHeader"; export * from "./types/ComponentKey"; export * from "./types/CompressedElgamalVerifiableBalance"; -export * from "./types/ConfidentialClaim"; export * from "./types/ConfidentialOutputStatement"; export * from "./types/ConfidentialTransferInputSelection"; export * from "./types/ConfidentialWithdrawProof"; @@ -115,6 +114,7 @@ export * from "./types/SubstateRequirement"; export * from "./types/Substate"; export * from "./types/SubstateType"; export * from "./types/SubstateValue"; +export * from "./types/TariStealthClaim"; export * from "./types/TemplateDef"; export * from "./types/TemplateDefV1"; export * from "./types/TimeoutCertificate"; diff --git a/bindings/src/types/FeeReceipt.ts b/bindings/src/types/FeeReceipt.ts index 4d5d642f3a..e5f98c923b 100644 --- a/bindings/src/types/FeeReceipt.ts +++ b/bindings/src/types/FeeReceipt.ts @@ -10,6 +10,11 @@ export type FeeReceipt = { * Total fees paid after refunds */ total_fees_paid: bigint; + /** + * The amount of non-refundable fees which the user overpaid. Fees cannot be refunded when paying purely with a + * stealth reveal (since we do not know the account/vault to refund). + */ + total_fee_overcharge: bigint; /** * Breakdown of fee costs */ diff --git a/bindings/src/types/Instruction.ts b/bindings/src/types/Instruction.ts index 90276e94c6..50482627cc 100644 --- a/bindings/src/types/Instruction.ts +++ b/bindings/src/types/Instruction.ts @@ -3,12 +3,12 @@ import type { AllocatableAddressType } from "./AllocatableAddressType"; import type { Amount } from "./Amount"; import type { ComponentAccessRules } from "./ComponentAccessRules"; import type { ComponentCall } from "./ComponentCall"; -import type { ConfidentialClaim } from "./ConfidentialClaim"; import type { LogLevel } from "./LogLevel"; import type { OwnerRule } from "./OwnerRule"; import type { ResourceAddress } from "./ResourceAddress"; import type { ResourceAddressRef } from "./ResourceAddressRef"; import type { StealthTransferStatement } from "./StealthTransferStatement"; +import type { TariStealthClaim } from "./TariStealthClaim"; import type { WorkspaceOffsetId } from "./WorkspaceOffsetId"; export type Instruction = @@ -24,7 +24,7 @@ export type Instruction = | { CallMethod: { call: ComponentCall; method: string; args: Array } } | { PutLastInstructionOutputOnWorkspace: { key: number } } | { EmitLog: { level: LogLevel; message: string } } - | { ClaimBurn: { claim: ConfidentialClaim } } + | { ClaimBurn: { claim: TariStealthClaim } } | { ClaimValidatorFees: { address: string } } | "DropAllProofsInWorkspace" | { AssertBucketContains: { key: WorkspaceOffsetId; resource_address: ResourceAddress; min_amount: Amount } } @@ -36,4 +36,5 @@ export type Instruction = statement: StealthTransferStatement; revealed_input_bucket: WorkspaceOffsetId | null; }; - }; + } + | { PayFee: { statement: StealthTransferStatement; revealed_input_bucket: WorkspaceOffsetId | null } }; diff --git a/bindings/src/types/ConfidentialClaim.ts b/bindings/src/types/TariStealthClaim.ts similarity index 65% rename from bindings/src/types/ConfidentialClaim.ts rename to bindings/src/types/TariStealthClaim.ts index d7cb90e1f2..ba013c9b27 100644 --- a/bindings/src/types/ConfidentialClaim.ts +++ b/bindings/src/types/TariStealthClaim.ts @@ -1,14 +1,17 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CommitmentSignatureBytes } from "./CommitmentSignatureBytes"; -import type { ConfidentialWithdrawProof } from "./ConfidentialWithdrawProof"; import type { RangeProofBytes } from "./RangeProofBytes"; import type { RistrettoPublicKeyBytes } from "./RistrettoPublicKeyBytes"; +import type { StealthTransferStatement } from "./StealthTransferStatement"; import type { UnclaimedConfidentialOutputAddress } from "./UnclaimedConfidentialOutputAddress"; -export type ConfidentialClaim = { - public_key: RistrettoPublicKeyBytes; +export type TariStealthClaim = { + /** + * This is typically the public nonce that the UTXO was burnt with + */ + burn_public_key: RistrettoPublicKeyBytes; output_address: UnclaimedConfidentialOutputAddress; range_proof: RangeProofBytes; proof_of_knowledge: CommitmentSignatureBytes; - withdraw_proof: ConfidentialWithdrawProof | null; + transfer_statement: StealthTransferStatement | null; }; diff --git a/bindings/src/types/validator-node-client/VNSubmitTransactionRequest.ts b/bindings/src/types/validator-node-client/VNSubmitTransactionRequest.ts index 0dde980559..85297f093a 100644 --- a/bindings/src/types/validator-node-client/VNSubmitTransactionRequest.ts +++ b/bindings/src/types/validator-node-client/VNSubmitTransactionRequest.ts @@ -4,4 +4,4 @@ import type { Transaction } from "../Transaction"; /** * A request to submit a transaction */ -export type VNSubmitTransactionRequest = { transaction: Transaction; is_dry_run: boolean }; +export type VNSubmitTransactionRequest = { transaction: Transaction }; diff --git a/bindings/src/types/wallet-daemon-client/ClaimBurnRequest.ts b/bindings/src/types/wallet-daemon-client/ClaimBurnRequest.ts index ea91e2d1ca..5b2d80d7fc 100644 --- a/bindings/src/types/wallet-daemon-client/ClaimBurnRequest.ts +++ b/bindings/src/types/wallet-daemon-client/ClaimBurnRequest.ts @@ -1,5 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ClaimBurnProof } from "./ClaimBurnProof"; import type { ComponentAddressOrName } from "./ComponentAddressOrName"; +import type { ExtClaimBurnProof } from "./ExtClaimBurnProof"; -export type ClaimBurnRequest = { account: ComponentAddressOrName; claim_proof: ClaimBurnProof; max_fee: number | null }; +export type ClaimBurnRequest = { + account: ComponentAddressOrName; + claim_proof: ExtClaimBurnProof; + max_fee: number | null; +}; diff --git a/bindings/src/types/wallet-daemon-client/ExtClaimBurnProof.ts b/bindings/src/types/wallet-daemon-client/ExtClaimBurnProof.ts new file mode 100644 index 0000000000..19450ef692 --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/ExtClaimBurnProof.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ClaimBurnProof } from "./ClaimBurnProof"; + +export type ExtClaimBurnProof = { claim_proof: ClaimBurnProof; owner_nonce_key_index: number }; diff --git a/bindings/src/types/wallet-daemon-client/KeyBranch.ts b/bindings/src/types/wallet-daemon-client/KeyBranch.ts index 4263fd8a35..d4a9f775b9 100644 --- a/bindings/src/types/wallet-daemon-client/KeyBranch.ts +++ b/bindings/src/types/wallet-daemon-client/KeyBranch.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type KeyBranch = "account" | "transaction" | "view_key" | "stealth_masks" | "confidential_masks"; +export type KeyBranch = "account" | "transaction" | "view_key" | "stealth_masks" | "confidential_masks" | "nonce"; diff --git a/bindings/src/types/wallet-daemon-client/TransactionGetAllRequest.ts b/bindings/src/types/wallet-daemon-client/TransactionGetAllRequest.ts index 743feaf638..24e70f3555 100644 --- a/bindings/src/types/wallet-daemon-client/TransactionGetAllRequest.ts +++ b/bindings/src/types/wallet-daemon-client/TransactionGetAllRequest.ts @@ -1,5 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ComponentAddress } from "../ComponentAddress"; +import type { RistrettoPublicKeyBytes } from "../RistrettoPublicKeyBytes"; import type { TransactionStatus } from "../TransactionStatus"; -export type TransactionGetAllRequest = { status: TransactionStatus | null; component: ComponentAddress | null }; +export type TransactionGetAllRequest = { + status: TransactionStatus | null; + component: ComponentAddress | null; + signer_public_key: RistrettoPublicKeyBytes | null; +}; diff --git a/bindings/src/types/wallet-daemon-client/TransactionGetResponse.ts b/bindings/src/types/wallet-daemon-client/TransactionGetResponse.ts index 30ea67649a..a07730bae0 100644 --- a/bindings/src/types/wallet-daemon-client/TransactionGetResponse.ts +++ b/bindings/src/types/wallet-daemon-client/TransactionGetResponse.ts @@ -7,5 +7,6 @@ export type TransactionGetResponse = { transaction: Transaction; result: FinalizeResult | null; status: TransactionStatus; + invalid_reason: string | null; last_update_time: string; }; diff --git a/bindings/src/wallet-daemon-client.ts b/bindings/src/wallet-daemon-client.ts index 7110c64cdb..8ec286ca2b 100644 --- a/bindings/src/wallet-daemon-client.ts +++ b/bindings/src/wallet-daemon-client.ts @@ -102,6 +102,7 @@ export * from "./types/wallet-daemon-client/TransferNftResponse"; export * from "./types/wallet-daemon-client/SubstatesListRequest"; export * from "./types/wallet-daemon-client/BalanceEntry"; export * from "./types/wallet-daemon-client/TemplatesGetRequest"; +export * from "./types/wallet-daemon-client/ExtClaimBurnProof"; export * from "./types/wallet-daemon-client/ProofsCancelRequest"; export * from "./types/wallet-daemon-client/AccountSetDefaultResponse"; export * from "./types/wallet-daemon-client/GetNftRequest"; diff --git a/clients/validator_node_client/src/types.rs b/clients/validator_node_client/src/types.rs index f986822f8f..e1c8a03fd1 100644 --- a/clients/validator_node_client/src/types.rs +++ b/clients/validator_node_client/src/types.rs @@ -136,7 +136,6 @@ pub struct TemplateMetadata { )] pub struct SubmitTransactionRequest { pub transaction: Transaction, - pub is_dry_run: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index e4bdae3c4b..6eff04bf28 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -39,7 +39,7 @@ use tari_ootle_common_types::{ }; use tari_ootle_wallet_sdk::{ apis::{confidential_transfer::ConfidentialTransferInputSelection, key_manager::KeyBranch}, - models::{Account, AuthoredTemplateModel, NonFungibleToken, OutputLockId, TransactionStatus, WalletTransaction}, + models::{Account, AuthoredTemplateModel, NonFungibleToken, TransactionStatus, WalletLockId, WalletTransaction}, }; use tari_template_abi::{FunctionDef, TemplateDef}; use tari_template_lib::{ @@ -85,7 +85,7 @@ pub struct CallInstructionRequest { pub new_outputs: Option, #[serde(default)] #[cfg_attr(feature = "ts", ts(type = "Array"))] - pub proof_ids: Vec, + pub proof_ids: Vec, #[serde(default)] #[cfg_attr(feature = "ts", ts(type = "number | null"))] pub min_epoch: Option, @@ -109,7 +109,7 @@ pub struct TransactionSubmitRequest { #[serde(default = "return_true")] pub detect_inputs_use_unversioned: bool, #[cfg_attr(feature = "ts", ts(type = "Array"))] - pub proof_ids: Vec, + pub proof_ids: Vec, } const fn return_true() -> bool { @@ -131,7 +131,7 @@ pub struct TransactionSubmitDryRunRequest { pub detect_inputs: bool, pub detect_inputs_use_unversioned: bool, #[cfg_attr(feature = "ts", ts(type = "Array"))] - pub proof_ids: Vec, + pub proof_ids: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -196,6 +196,7 @@ pub struct TransactionGetResponse { pub transaction: Transaction, pub result: Option, pub status: TransactionStatus, + pub invalid_reason: Option, #[cfg_attr(feature = "ts", ts(type = "string"))] pub last_update_time: PrimitiveDateTime, } @@ -205,6 +206,7 @@ pub struct TransactionGetResponse { pub struct TransactionGetAllRequest { pub status: Option, pub component: Option, + pub signer_public_key: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -492,7 +494,7 @@ pub struct ProofsGenerateRequest { #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct ProofsGenerateResponse { #[cfg_attr(feature = "ts", ts(type = "number"))] - pub proof_id: OutputLockId, + pub proof_id: WalletLockId, pub proof: ConfidentialWithdrawProof, } @@ -500,7 +502,7 @@ pub struct ProofsGenerateResponse { #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct ProofsFinalizeRequest { #[cfg_attr(feature = "ts", ts(type = "number"))] - pub proof_id: OutputLockId, + pub proof_id: WalletLockId, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -511,7 +513,7 @@ pub struct ProofsFinalizeResponse {} #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct ProofsCancelRequest { #[cfg_attr(feature = "ts", ts(type = "number"))] - pub proof_id: OutputLockId, + pub proof_id: WalletLockId, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -574,11 +576,19 @@ pub struct ConfidentialViewVaultBalanceResponse { #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct ClaimBurnRequest { pub account: ComponentAddressOrName, - pub claim_proof: ClaimBurnProof, + pub claim_proof: ExtClaimBurnProof, #[cfg_attr(feature = "ts", ts(type = "number | null"))] pub max_fee: Option, } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct ExtClaimBurnProof { + pub claim_proof: ClaimBurnProof, + #[cfg_attr(feature = "ts", ts(type = "number"))] + pub owner_nonce_key_index: u64, +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct ClaimBurnProof { diff --git a/crates/consensus/src/hotstuff/block_change_set.rs b/crates/consensus/src/hotstuff/block_change_set.rs index b49ada4bf6..a518c792a0 100644 --- a/crates/consensus/src/hotstuff/block_change_set.rs +++ b/crates/consensus/src/hotstuff/block_change_set.rs @@ -654,17 +654,17 @@ mod tests { use super::*; #[test] - fn check_max_stack_mem_usage() { + fn check_max_mem_usage() { let sz = size_of::(); eprintln!("ProposedBlockChangeSet: {}", sz); - const TARGET_MAX_MEM_USAGE: usize = 13_232_000; + const TARGET_MAX_MEM_USAGE: usize = 13_240_000; let mem_block_diff = size_of::() * MEM_MAX_BLOCK_DIFF_CHANGES; eprintln!("mem_block_diff: {}KiB", mem_block_diff / 1024); let mem_state_tree_diffs = size_of::() * size_of::() * MEM_MAX_STATE_TREE_DIFF_SIZE; eprintln!("mem_state_tree_diffs: {}", mem_state_tree_diffs); let mem_substate_locks = (size_of::() + size_of::()) * MEM_MAX_SUBSTATE_LOCK_SIZE; - eprintln!("mem_substate_locks: {}", mem_substate_locks); + eprintln!("mem_substate_locks: {}MiB", mem_substate_locks / 1024 / 1024); let mem_transaction_changes = (size_of::() + size_of::()) * MEM_MAX_TRANSACTION_CHANGE_SIZE; eprintln!("mem_transaction_changes: {}", mem_transaction_changes); diff --git a/crates/consensus/src/hotstuff/on_propose.rs b/crates/consensus/src/hotstuff/on_propose.rs index 67cefe2918..c55a72e5c9 100644 --- a/crates/consensus/src/hotstuff/on_propose.rs +++ b/crates/consensus/src/hotstuff/on_propose.rs @@ -674,7 +674,7 @@ where TConsensusSpec: ConsensusSpec self.config.consensus_constants.fee_exhaust_divisor, ); pool_tx.set_leader_fee(leader_fee); - let diff = execution.result().finalize.result.accept().ok_or_else(|| { + let diff = execution.result().finalize.result.any_accept().ok_or_else(|| { HotStuffError::InvariantError(format!( "prepare_transaction: Transaction {} has COMMIT decision but execution failed when \ proposing", @@ -879,7 +879,7 @@ where TConsensusSpec: ConsensusSpec tx_rec.transaction_id(), )) })?; - let diff = execution.result().finalize.accept().ok_or_else(|| { + let diff = execution.result().finalize.any_accept().ok_or_else(|| { HotStuffError::InvariantError(format!( "local_accept_transaction: Transaction {} has COMMIT decision but execution failed when proposing", tx_rec.transaction_id(), diff --git a/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs b/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs index d8f6b5a8dc..c795822380 100644 --- a/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs +++ b/crates/consensus/src/hotstuff/on_ready_to_vote_on_local_block.rs @@ -612,7 +612,7 @@ where TConsensusSpec: ConsensusSpec } if pool_tx.current_decision().is_commit() { - if let Some(diff) = execution.result().finalize.accept() { + if let Some(diff) = execution.result().finalize.any_accept() { substate_store.put_diff(diff)?; } @@ -1310,7 +1310,7 @@ where TConsensusSpec: ConsensusSpec )) })?; - let diff = execution.result().finalize.accept().ok_or_else(|| { + let diff = execution.result().finalize.any_accept().ok_or_else(|| { HotStuffError::InvariantError(format!( "evaluate_local_accept_command: Transaction {} has COMMIT decision but execution failed when proposing", tx_rec.transaction_id(), diff --git a/crates/consensus_tests/src/support/transaction.rs b/crates/consensus_tests/src/support/transaction.rs index c18e094606..5c2c101d9a 100644 --- a/crates/consensus_tests/src/support/transaction.rs +++ b/crates/consensus_tests/src/support/transaction.rs @@ -112,6 +112,7 @@ pub fn create_execution_result_for_transaction( fee_receipt: FeeReceipt { total_fee_payment: fee, total_fees_paid: fee, + total_fee_overcharge: 0, cost_breakdown: FeeBreakdown::default(), }, }), @@ -135,6 +136,7 @@ pub fn create_execution_result_for_transaction( FeeReceipt { total_fee_payment: fee, total_fees_paid: fee, + total_fee_overcharge: 0, cost_breakdown: FeeBreakdown::default(), }, ), diff --git a/crates/consensus_tests/src/support/transaction_executor.rs b/crates/consensus_tests/src/support/transaction_executor.rs index 2da6793a4c..bbe417c3fc 100644 --- a/crates/consensus_tests/src/support/transaction_executor.rs +++ b/crates/consensus_tests/src/support/transaction_executor.rs @@ -132,7 +132,7 @@ impl BlockTransactionExecutor for TestBloc let resulting_outputs = result .finalize .result - .accept() + .any_accept() .map(|diff| { diff.up_iter() .map(|(addr, substate)| { diff --git a/crates/consensus_tests/src/support/validator/instance.rs b/crates/consensus_tests/src/support/validator/instance.rs index 30b0d508cd..0edc34f544 100644 --- a/crates/consensus_tests/src/support/validator/instance.rs +++ b/crates/consensus_tests/src/support/validator/instance.rs @@ -97,7 +97,7 @@ impl Validator { let tx = self.state_store().create_read_tx().unwrap(); let tx_rec = tx.transactions_get(tx_id).unwrap(); let exec_result = tx_rec.get_finalized_execution(&tx).unwrap(); - if let Some(diff) = exec_result.result().finalize.accept() { + if let Some(diff) = exec_result.result().finalize.any_accept() { for (substate_id, substate) in diff.up_iter() { let id = VersionedSubstateIdRef::new(substate_id, substate.version()); if tx.substates_any_exist(Some(id)).unwrap() { diff --git a/crates/engine/src/runtime/error.rs b/crates/engine/src/runtime/error.rs index d87eb259ba..b5f0193afb 100644 --- a/crates/engine/src/runtime/error.rs +++ b/crates/engine/src/runtime/error.rs @@ -198,6 +198,8 @@ pub enum RuntimeError { TemplateNotFound { template_address: TemplateAddress }, #[error("Insufficient fees paid: required {required_fee}, paid {fees_paid}")] InsufficientFeesPaid { required_fee: u64, fees_paid: u64 }, + #[error("No fees paid from stealth transfer: {details}")] + NoFeesPaid { details: String }, #[error("No fee checkpoint")] NoFeeCheckpoint, #[error("Component address must be sequential. Index before {index} was not found")] diff --git a/crates/engine/src/runtime/fee_state.rs b/crates/engine/src/runtime/fee_state.rs index 3b8d37504f..75f19e6217 100644 --- a/crates/engine/src/runtime/fee_state.rs +++ b/crates/engine/src/runtime/fee_state.rs @@ -1,15 +1,21 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_engine_types::{fees::FeeBreakdown, resource_container::ResourceContainer}; -use tari_template_lib::models::VaultId; +use tari_engine_types::{ + fees::{FeeBreakdown, FeeSource}, + resource_container::ResourceContainer, +}; +use tari_template_lib::{models::VaultId, prelude::XTR}; + +use crate::runtime::RuntimeError; #[derive(Debug, Clone, Default)] pub struct FeeState { + fee_payments_without_refund: Vec, /// The fee payments made by the user, used to pay for the transaction fees with the return vault. - pub fee_payments: Vec<(ResourceContainer, VaultId)>, - pub running_total: u64, - pub fee_charges: FeeBreakdown, + fee_payments: Vec<(ResourceContainer, VaultId)>, + running_total: u64, + fee_charges: FeeBreakdown, } impl FeeState { @@ -17,17 +23,64 @@ impl FeeState { Self::default() } - #[must_use] - pub fn add_fee_payment(&mut self, resource_container: ResourceContainer, vault_id: VaultId) -> bool { + pub fn add_fee_payment_checked( + &mut self, + resource_container: ResourceContainer, + vault_id: Option, + ) -> Result<(), RuntimeError> { + if *resource_container.resource_address() != XTR { + return Err(RuntimeError::InvalidArgument { + argument: "vault_ref", + reason: format!( + "Fees can only be paid using XTR, however the vault contained resource {}", + resource_container.resource_address() + ), + }); + } + let Some(amount) = resource_container.amount().to_u64_checked() else { - return false; // Invalid amount, cannot add payment + return Err(RuntimeError::InvalidAmount { + amount: resource_container.amount(), + reason: "Payed an invalid amount. Amount must be positive and not overflow".to_string(), + }); }; match self.running_total.checked_add(amount) { Some(new_total) => self.running_total = new_total, - None => return false, // Overflow, cannot add payment + None => { + return Err(RuntimeError::InvalidAmount { + amount: resource_container.amount(), + reason: "Payed an invalid amount. Amount overflowed".to_string(), + }); + }, + } + if let Some(vault_id) = vault_id { + self.fee_payments.push((resource_container, vault_id)); + } else { + self.fee_payments_without_refund.push(resource_container); } - self.fee_payments.push((resource_container, vault_id)); - true + Ok(()) + } + + pub fn drain_refundable_fee_payments(&mut self) -> impl Iterator + '_ { + self.fee_payments.drain(..) + } + + pub fn refundable_fee_payments_iter_mut( + &mut self, + ) -> impl Iterator + '_ { + self.fee_payments.iter_mut().map(|(rc, vid)| (rc, vid)) + } + + pub fn non_refundable_fee_payments_mut_iter(&mut self) -> impl Iterator + '_ { + self.fee_payments_without_refund.iter_mut() + } + + pub fn add_charge(&mut self, source: FeeSource, amount: u64) { + self.fee_charges.add(source, amount) + } + + pub fn take_fee_charges(&mut self) -> FeeBreakdown { + std::mem::take(&mut self.fee_charges) } pub fn total_charges(&self) -> u64 { diff --git a/crates/engine/src/runtime/impl.rs b/crates/engine/src/runtime/impl.rs index 52164b6f53..5c89eee641 100644 --- a/crates/engine/src/runtime/impl.rs +++ b/crates/engine/src/runtime/impl.rs @@ -32,7 +32,7 @@ use tari_crypto::{ use tari_engine_types::{ commit_result::{FinalizeResult, RejectReason, TransactionResult}, component::ComponentHeader, - confidential::ConfidentialClaim, + confidential::TariStealthClaim, crypto::{get_commitment_factory, get_static_range_proof_service, PrivateOutput}, entity_id_provider::EntityIdProvider, events::Event, @@ -45,15 +45,14 @@ use tari_engine_types::{ published_template::{PublishedTemplate, PublishedTemplateAddress}, resource::Resource, resource_container::{ResourceContainer, ResourceError}, - stealth, substate::{SubstateId, SubstateValue}, vault::Vault, ComponentCall, FromByteType, ResourceAddressRef, - ToByteType, Utxo, UtxoAddress, + UtxoOutput, ValidatorFeePoolAddress, }; use tari_ootle_common_types::{ @@ -107,7 +106,7 @@ use tari_template_lib::{ }, auth::{AuthHook, AuthHookCaller, ComponentAccessRules, OwnerRule, ResourceAccessRules, ResourceAuthAction}, call_args, - constants::{CONFIDENTIAL_TARI_RESOURCE_ADDRESS, XTR}, + constants::{STEALTH_TARI_RESOURCE_ADDRESS, XTR}, models::{ BucketId, ComponentAddress, @@ -124,7 +123,7 @@ use tari_template_lib::{ prelude::{ResourceType, RistrettoPublicKeyBytes}, resource::{IMAGE_URL, TOKEN_SYMBOL}, template::BuiltinTemplate, - types::{Amount, EntityId, TemplateAddress}, + types::{crypto::UtxoTagByte, Amount, EntityId, TemplateAddress}, }; use super::{working_state::WorkingState, Runtime}; @@ -135,7 +134,6 @@ use crate::{ locking::{LockError, LockedSubstate}, scope::PushCallFrame, tracker::StateTracker, - validation::check_stealth_transfer_limits, RuntimeError, RuntimeInterface, RuntimeModule, @@ -424,122 +422,6 @@ impl> RuntimeInte Ok(()) } - - fn execute_stealth_transfer( - &self, - resource_address: ResourceAddressRef, - statement: StealthTransferStatement, - revealed_funds_bucket_id: Option, - ) -> Result, RuntimeError> { - check_stealth_transfer_limits(&limits::STEALTH_LIMITS, &statement)?; - - self.tracker.write_with(|state_mut| { - let resource_address = state_mut.resolve_resource_address_ref(resource_address)?; - - let resource_lock = state_mut.read_lock_substate(&SubstateId::Resource(resource_address))?; - { - let resource = state_mut.get_resource(&resource_lock)?; - if !resource.resource_type().is_stealth() { - return Err(ResourceError::OperationNotAllowed(format!( - "Stealth transfer is only allowed for stealth resources: {}", - resource_address - )) - .into()); - } - - // Authorize transfer - state_mut.authorization().check_resource_access_rules( - // TODO: specific auth action for stealth transfer? Technically this is a withdraw and deposit, but - // a separate AccessRule may be excessive/not useful. - ResourceAuthAction::Withdraw, - resource.as_ownership(), - resource.access_rules(), - )?; - } - - let revealed_funds_bucket = revealed_funds_bucket_id - .map(|id| state_mut.take_bucket(id)) - .transpose()?; - if let Some(ref bucket) = revealed_funds_bucket { - if *bucket.resource_address() != resource_address { - return Err(RuntimeError::InvalidArgument { - argument: "revealed_funds_bucket", - reason: format!( - "Revealed funds bucket resource address ({}) does not match the statement's resource \ - address ({})", - bucket.resource_address(), - resource_address - ), - }); - } - } - - match revealed_funds_bucket { - Some(ref bucket) => { - if bucket.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(), - statement.inputs_statement.revealed_amount - ), - }); - } - }, - None => { - if !statement.inputs_statement.revealed_amount.is_zero() { - return Err(RuntimeError::InvalidArgument { - argument: "revealed_funds_bucket", - reason: format!( - "An input bucket is required but not provided for stealth transfers with revealed \ - input amount ({})", - statement.inputs_statement.revealed_amount - ), - }); - } - }, - } - - state_mut.spend_stealth_utxos(resource_address, &statement.inputs_statement)?; - - let resource = state_mut.get_resource(&resource_lock)?; - let view_key = resource - .view_key() - .map(RistrettoPublicKey::try_from_byte_type) - .transpose() - .map_err(|e| { - warn!(target: LOG_TARGET, "Stealth transfer failed - malformed view key: {}", e); - RuntimeError::InvalidArgument { - argument: "view_key", - reason: "Malformed RistrettoPublicKeyBytes".to_string(), - } - })?; - - let valid_transfer = stealth::validate_transfer(&statement, view_key.as_ref())?; - - for output in valid_transfer.outputs { - let address = UtxoAddress::new(resource_address, output.output.commitment.to_byte_type().into()); - let value = Utxo::new(output.to_utxo_output()); - state_mut.new_substate(address, value)?; - } - - state_mut.unlock_substate(resource_lock)?; - - if valid_transfer.revealed_output_amount.is_zero() { - return Ok(None); - } - - let bucket_id = state_mut.new_bucket_id(); - state_mut.new_bucket( - bucket_id, - ResourceContainer::stealth(resource_address, valid_transfer.revealed_output_amount), - )?; - - Ok(Some(bucket_id)) - }) - } } impl> RuntimeInterface @@ -1319,8 +1201,7 @@ impl> RuntimeInte reason: "StealthTransfer resource action requires a resource address".to_string(), })?; let arg: StealthTransferResourceArg = args.assert_one_arg()?; - let maybe_bucket = - self.execute_stealth_transfer(resource_address.into(), arg.transfer, arg.input_bucket)?; + let maybe_bucket = self.stealth_transfer(resource_address.into(), arg.transfer, arg.input_bucket)?; Ok(InvokeResult::encode( &maybe_bucket.map(tari_template_lib::models::Bucket::from_id), )?) @@ -1387,9 +1268,10 @@ impl> RuntimeInte state.new_substate(vault_id, vault)?; debug!( target: LOG_TARGET, - "Created vault {} for resource {}", + "Created vault {} for resource {} ({})", vault_id, - resource_address + resource_address, + resource_type ); state.unlock_substate(resource_lock)?; @@ -1420,9 +1302,9 @@ impl> RuntimeInte freeze_flag: VaultFreezeFlag::Deposits, }); } - let resource_address = vault.resource_address(); + let resource_address = *vault.resource_address(); - let resource_lock = state_mut.read_lock_substate(&SubstateId::Resource(*resource_address))?; + let resource_lock = state_mut.read_lock_substate(&SubstateId::Resource(resource_address))?; let resource = state_mut.get_resource(&resource_lock)?; @@ -1460,6 +1342,7 @@ impl> RuntimeInte self.emit_std_event("vault", "deposit", vault_id.into(), payload, state_mut)?; let vault_mut = state_mut.get_vault_mut(&vault_lock)?; + vault_mut.deposit(bucket)?; state_mut.unlock_substate(resource_lock)?; @@ -1671,27 +1554,20 @@ impl> RuntimeInte resource.as_ownership(), resource.access_rules(), )?; - let view_key = resource - .to_view_key_public_key() - .map_err(|e| RuntimeError::InvariantError { - function: "VaultAction::PayFee", - details: format!( - "Resource {} has a malformed view key: {}", - resource_lock.substate_id(), - e - ), - })?; let vault_mut = state_mut.get_vault_mut(&vault_lock)?; - let mut container = ResourceContainer::confidential(XTR, None, Amount::zero()); - if !arg.amount.is_zero() { + let mut container = ResourceContainer::stealth(XTR, Amount::zero()); + if arg.amount.is_positive() { let withdrawn = vault_mut.withdraw(arg.amount)?; container.deposit(withdrawn)?; } - if let Some(proof) = arg.proof { - let revealed = vault_mut.withdraw_confidential(proof, view_key.as_ref())?; - container.deposit(revealed)?; + if let Some(statement) = arg.statement { + if let Some(revealed) = + state_mut.execute_stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS.into(), statement, None)? + { + container.deposit(revealed)?; + } } if container.amount().is_zero() { return Err(RuntimeError::InvalidArgument { @@ -1700,7 +1576,7 @@ impl> RuntimeInte }); } - state_mut.pay_fee(container, vault_id)?; + state_mut.pay_fee(container, Some(vault_id))?; state_mut.unlock_substate(resource_lock)?; state_mut.unlock_substate(vault_lock)?; @@ -2419,19 +2295,19 @@ impl> RuntimeInte Ok(()) } - fn claim_burn(&self, claim: ConfidentialClaim) -> Result<(), RuntimeError> { - let ConfidentialClaim { - public_key: diffie_hellman_public_key, + fn claim_burn(&self, claim: TariStealthClaim) -> Result<(), RuntimeError> { + let TariStealthClaim { + burn_public_key, output_address, range_proof, proof_of_knowledge, - withdraw_proof, + transfer_statement, } = claim; // 1. Must exist let unclaimed_output = self.tracker.take_unclaimed_confidential_output(output_address)?; // 2. owner_sig must be valid - // NOTE: .as_bytes() used because the tari_crypto borsh implementations serialize as variable length bytes - // (always 32) + // NOTE: .as_bytes() used because the tari_crypto borsh implementations serialize fixed length bytes as variable + // length bytes of size 32 let message = ownership_proof_hasher64(self.network) .chain(&proof_of_knowledge.public_nonce().as_bytes()) .chain(&unclaimed_output.commitment.as_bytes()) @@ -2465,29 +2341,34 @@ impl> RuntimeInte return Err(RuntimeError::InvalidRangeProof); } - // 4. Create the confidential resource - let mut resource = ResourceContainer::confidential( - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - Some((unclaimed_output.commitment, PrivateOutput { - public_nonce: diffie_hellman_public_key, - encrypted_data: unclaimed_output.encrypted_data, - minimum_value_promise: 0, - viewable_balance: None, - })), - Amount::zero(), - ); + self.tracker.write_with(|state_mut| { + // 4. Create the stealth UTXO + let address = UtxoAddress::new(XTR, unclaimed_output.commitment.into()); + let utxo = Utxo::new(UtxoOutput { + output: PrivateOutput { + public_nonce: burn_public_key, + encrypted_data: unclaimed_output.encrypted_data, + minimum_value_promise: 0, + viewable_balance: None, + }, + owner_public_key: self.transaction_signer_public_key, + tag: UtxoTagByte::new(0), + }); - // If a withdraw proof is provided, we execute it and deposit back into the resource - // This allows some funds to be revealed and/or reblinded within a single instruction - if let Some(proof) = withdraw_proof { - let withdraw = resource.withdraw_confidential(proof, None)?; - resource.deposit(withdraw)?; - } + state_mut.new_substate(address, utxo)?; - self.tracker.write_with(|state| { - let bucket_id = state.new_bucket_id(); - state.new_bucket(bucket_id, resource)?; - state.set_last_instruction_output(IndexedValue::from_type(&bucket_id)?); + // If a transfer statement is provided, we execute it + // This allows claimed funds to be revealed and/or reblinded within a single instruction + let maybe_container = transfer_statement + .map(|transfer| state_mut.execute_stealth_transfer(XTR.into(), transfer, None)) + .transpose()? + .flatten(); + + if let Some(container) = maybe_container { + let bucket_id = state_mut.new_bucket_id(); + state_mut.new_bucket(bucket_id, container)?; + state_mut.set_last_instruction_output(IndexedValue::from_type(&bucket_id)?); + } Ok::<_, RuntimeError>(()) })?; @@ -2543,7 +2424,7 @@ impl> RuntimeInte finalized.fee_receipt.total_fees_charged(), finalized.fee_receipt.total_fees_paid() )); - finalized.result = if let Some(accept) = finalized.result.accept() { + finalized.result = if let Some(accept) = finalized.result.any_accept() { TransactionResult::AcceptFeeRejectRest(accept.clone(), reason) } else { TransactionResult::Reject(reason) @@ -2770,7 +2651,38 @@ impl> RuntimeInte statement: StealthTransferStatement, revealed_funds_bucket_id: Option, ) -> Result, RuntimeError> { - self.execute_stealth_transfer(resource_address, statement, revealed_funds_bucket_id) + self.tracker.write_with(|state_mut| { + let Some(container) = + state_mut.execute_stealth_transfer(resource_address, statement, revealed_funds_bucket_id)? + else { + return Ok(None); + }; + let bucket_id = state_mut.new_bucket_id(); + state_mut.new_bucket(bucket_id, container)?; + Ok(Some(bucket_id)) + }) + } + + fn pay_fee( + &self, + statement: StealthTransferStatement, + revealed_funds_bucket: Option, + ) -> Result<(), RuntimeError> { + self.tracker.write_with(|state_mut| { + let Some(container) = state_mut.execute_stealth_transfer( + STEALTH_TARI_RESOURCE_ADDRESS.into(), + statement, + revealed_funds_bucket, + )? + else { + return Err(RuntimeError::NoFeesPaid { + details: "Stealth transfer did not reveal any funds to pay fees".to_string(), + }); + }; + // No refunds + state_mut.pay_fee(container, None)?; + Ok(()) + }) } } diff --git a/crates/engine/src/runtime/mod.rs b/crates/engine/src/runtime/mod.rs index c944136e99..dcca3f1c89 100644 --- a/crates/engine/src/runtime/mod.rs +++ b/crates/engine/src/runtime/mod.rs @@ -57,7 +57,7 @@ use tari_bor::decode_exact; use tari_engine_types::{ commit_result::FinalizeResult, component::ComponentHeader, - confidential::ConfidentialClaim, + confidential::TariStealthClaim, indexed_value::IndexedValue, limits, lock::LockFlag, @@ -163,7 +163,7 @@ pub trait RuntimeInterface: Send + Sync { fn set_last_instruction_output(&self, value: IndexedValue) -> Result<(), RuntimeError>; - fn claim_burn(&self, claim: ConfidentialClaim) -> Result<(), RuntimeError>; + fn claim_burn(&self, claim: TariStealthClaim) -> Result<(), RuntimeError>; fn claim_validator_fees(&self, address: ValidatorFeePoolAddress) -> Result<(), RuntimeError>; @@ -205,6 +205,12 @@ pub trait RuntimeInterface: Send + Sync { statement: StealthTransferStatement, revealed_funds_bucket: Option, ) -> Result, RuntimeError>; + + fn pay_fee( + &self, + statement: StealthTransferStatement, + revealed_funds_bucket: Option, + ) -> Result<(), RuntimeError>; } #[derive(Clone)] diff --git a/crates/engine/src/runtime/state_store.rs b/crates/engine/src/runtime/state_store.rs index 0210d9c6e2..50e2550b0c 100644 --- a/crates/engine/src/runtime/state_store.rs +++ b/crates/engine/src/runtime/state_store.rs @@ -193,7 +193,7 @@ impl WorkingStateStore { })?; const EXPECT: &str = "invariant: substate found at utxo address is not a UTXO"; if let Some(value) = self.new_substates.shift_remove(substate_id) { - self.downed_utxos.insert(address); + // No need to down it, we simply won't create it by removing it from the new substates return Ok(value.into_utxo().expect(EXPECT)); } if let Some(substate) = self.loaded_substates.remove(substate_id) { @@ -228,14 +228,14 @@ impl WorkingStateStore { }) } - pub(super) fn get_unmodified_substate(&self, address: &SubstateId) -> Result<&Substate, RuntimeError> { - match self.loaded_substates.get(address) { + pub(super) fn get_unmodified_substate(&self, id: &SubstateId) -> Result<&Substate, RuntimeError> { + match self.loaded_substates.get(id) { Some(substate) => Ok(substate), None => self .state_store - .get_state(address) + .get_state(id) .optional()? - .ok_or_else(|| RuntimeError::SubstateNotFound { id: address.clone() }), + .ok_or_else(|| RuntimeError::SubstateNotFound { id: id.clone() }), } } } diff --git a/crates/engine/src/runtime/tracker.rs b/crates/engine/src/runtime/tracker.rs index d6e60230d7..511f9a8349 100644 --- a/crates/engine/src/runtime/tracker.rs +++ b/crates/engine/src/runtime/tracker.rs @@ -270,7 +270,7 @@ impl StateTracker { self.write_with(|state| { debug!(target: LOG_TARGET, "Add fee: source: {:?}, amount: {}", source, amount); - state.fee_state_mut().fee_charges.insert(source, amount); + state.fee_state_mut().add_charge(source, amount); }) } @@ -327,7 +327,8 @@ impl StateTracker { self.write_with(|state| { let fee_state = state.fee_state().clone(); *state = checkpoint; - // Preserve fee state across resets + // Preserve fee state across resets so that we can charge for fees incurred during execution before the + // failure *state.fee_state_mut() = fee_state; }); Ok(()) diff --git a/crates/engine/src/runtime/working_state.rs b/crates/engine/src/runtime/working_state.rs index 619262d025..3e2b6f6f26 100644 --- a/crates/engine/src/runtime/working_state.rs +++ b/crates/engine/src/runtime/working_state.rs @@ -9,6 +9,7 @@ use std::{ use indexmap::{IndexMap, IndexSet}; use log::*; +use tari_crypto::ristretto::RistrettoPublicKey; use tari_engine_types::{ bucket::Bucket, component::ComponentHeader, @@ -24,11 +25,15 @@ use tari_engine_types::{ proof::{ContainerRef, LockedResource, Proof}, resource::Resource, resource_container::{ResourceContainer, ResourceError}, + stealth, substate::{Substate, SubstateDiff, SubstateId, SubstateValue}, transaction_receipt::TransactionReceipt, vault::Vault, virtual_substate::{VirtualSubstate, VirtualSubstateId, VirtualSubstates}, + FromByteType, ResourceAddressRef, + ToByteType, + Utxo, UtxoAddress, ValidatorFeePoolAddress, ValidatorFeeWithdrawal, @@ -36,7 +41,8 @@ use tari_engine_types::{ use tari_ootle_common_types::{optional::Optional, Epoch}; use tari_template_lib::{ args::{MintArg, ResourceDiscriminator, VaultFreezeFlags}, - constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + auth::ResourceAuthAction, + constants::STEALTH_TARI_RESOURCE_ADDRESS, models::{ AddressAllocationId, BucketId, @@ -45,6 +51,7 @@ use tari_template_lib::{ ProofId, ResourceAddress, StealthInputsStatement, + StealthTransferStatement, UnclaimedConfidentialOutputAddress, VaultId, }, @@ -61,6 +68,7 @@ use crate::{ scope::{CallFrame, CallScope}, state_store::WorkingStateStore, tracker_auth::Authorization, + validation::check_stealth_transfer_limits, ActionIdent, NativeAction, RuntimeError, @@ -850,15 +858,8 @@ impl WorkingState { .ok_or(RuntimeError::AddressAllocationNotUsed { id }) } - pub fn pay_fee(&mut self, resource: ResourceContainer, return_vault: VaultId) -> Result<(), RuntimeError> { - let amount = resource.amount(); - if !self.fee_state.add_fee_payment(resource, return_vault) { - return Err(RuntimeError::InvalidAmount { - amount, - reason: "Payed an invalid amount. Amount must be positive and not overflow".to_string(), - }); - } - Ok(()) + pub fn pay_fee(&mut self, resource: ResourceContainer, return_vault: Option) -> Result<(), RuntimeError> { + self.fee_state.add_fee_payment_checked(resource, return_vault) } pub fn withdraw_all_fees_from_pool( @@ -978,35 +979,68 @@ impl WorkingState { let total_fee_payment = self.fee_state.total_payments(); - let mut fee_resource = - ResourceContainer::confidential(CONFIDENTIAL_TARI_RESOURCE_ADDRESS, None, Amount::zero()); + let mut fee_resource = ResourceContainer::stealth(STEALTH_TARI_RESOURCE_ADDRESS, Amount::zero()); // Collect the fee let mut remaining_fees = total_fees; - for (resx, _) in &mut self.fee_state.fee_payments { - if remaining_fees == 0 { - break; - } + let mut total_fee_overcharge = 0; + // 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 fee_amount = resx.amount().to_u64_checked().expect("invalid fee entry in fee state"); + let paid_amount = resx.amount().to_u64_checked().expect("invalid fee entry in fee state"); + + debug!( + target: LOG_TARGET, + "Collecting {} of non-refundable fees", resx.amount() + ); + + // If there is no refund vault, we must take the entire amount to avoid destroying funds + fee_resource.deposit(resx.withdraw(resx.amount())?)?; + if remaining_fees < paid_amount { + total_fee_overcharge += paid_amount - remaining_fees; + } + + remaining_fees = remaining_fees.saturating_sub(paid_amount); + } + + if remaining_fees > 0 { + for (resx, _) in self.fee_state.refundable_fee_payments_iter_mut() { + if remaining_fees == 0 { + break; + } + + debug!( + target: LOG_TARGET, + "Collecting {} of refundable fees", resx.amount() + ); - let amount_to_withdraw = cmp::min(fee_amount, remaining_fees); - remaining_fees -= amount_to_withdraw; - fee_resource.deposit(resx.withdraw(amount_to_withdraw.into())?)?; + // PANIC: this is checked by FeeState + let paid_amount = resx.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); + fee_resource.deposit(resx.withdraw(amount_to_withdraw.into())?)?; + remaining_fees = remaining_fees.saturating_sub(amount_to_withdraw); + } } - // Refund the remaining payments if any - for (mut resx, refund_vault) in self.fee_state.fee_payments.drain(..) { + // 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()); continue; } + debug!( + target: LOG_TARGET, + "Refunding {} of fees to vault {}", resx.amount(), refund_vault + ); let vault_mut = substates_to_persist .get_mut(&SubstateId::Vault(refund_vault)) .expect("invariant: vault that made fee payment not in changeset") .as_vault_mut() .expect("invariant: substate substate_id for fee refund is not a vault"); - vault_mut.resource_container_mut().deposit(resx.recall_all()?)?; + vault_mut.resource_container_mut().deposit(resx.withdraw_all()?)?; } Ok(TransactionReceipt { @@ -1019,7 +1053,8 @@ impl WorkingState { .amount() .to_u64_checked() .expect("FeeState guarantees that the total fee payments fit in a u64"), - cost_breakdown: mem::take(&mut self.fee_state.fee_charges), + total_fee_overcharge, + cost_breakdown: self.fee_state.take_fee_charges(), }, }) } @@ -1387,4 +1422,111 @@ impl WorkingState { Ok(()) } + + pub fn execute_stealth_transfer( + &mut self, + resource_address: ResourceAddressRef, + statement: StealthTransferStatement, + revealed_funds_bucket_id: Option, + ) -> Result, RuntimeError> { + check_stealth_transfer_limits(&limits::STEALTH_LIMITS, &statement)?; + let resource_address = self.resolve_resource_address_ref(resource_address)?; + + let resource_lock = self.read_lock_substate(&SubstateId::Resource(resource_address))?; + { + let resource = self.get_resource(&resource_lock)?; + if !resource.resource_type().is_stealth() { + return Err(ResourceError::OperationNotAllowed(format!( + "Stealth transfer is only allowed for stealth resources: {}", + resource_address + )) + .into()); + } + + // Authorize transfer + self.authorization().check_resource_access_rules( + // TODO: specific auth action for stealth transfer? Technically this is a withdraw and deposit, but + // a separate AccessRule may be excessive/not useful. + ResourceAuthAction::Withdraw, + resource.as_ownership(), + resource.access_rules(), + )?; + } + + let revealed_funds_bucket = revealed_funds_bucket_id.map(|id| self.take_bucket(id)).transpose()?; + if let Some(ref bucket) = revealed_funds_bucket { + if *bucket.resource_address() != resource_address { + return Err(RuntimeError::InvalidArgument { + argument: "revealed_funds_bucket", + reason: format!( + "Revealed funds bucket resource address ({}) does not match the statement's resource address \ + ({})", + bucket.resource_address(), + resource_address + ), + }); + } + } + + match revealed_funds_bucket { + Some(ref bucket) => { + if bucket.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(), + statement.inputs_statement.revealed_amount + ), + }); + } + }, + None => { + if !statement.inputs_statement.revealed_amount.is_zero() { + return Err(RuntimeError::InvalidArgument { + argument: "revealed_funds_bucket", + reason: format!( + "An input bucket is required but not provided for stealth transfers with revealed input \ + amount ({})", + statement.inputs_statement.revealed_amount + ), + }); + } + }, + } + + self.spend_stealth_utxos(resource_address, &statement.inputs_statement)?; + + let resource = self.get_resource(&resource_lock)?; + let view_key = resource + .view_key() + .map(RistrettoPublicKey::try_from_byte_type) + .transpose() + .map_err(|e| { + warn!(target: LOG_TARGET, "Stealth transfer failed - malformed view key: {}", e); + RuntimeError::InvalidArgument { + argument: "view_key", + reason: "Malformed RistrettoPublicKeyBytes".to_string(), + } + })?; + + let valid_transfer = stealth::validate_transfer(&statement, view_key.as_ref())?; + + for output in valid_transfer.outputs { + let address = UtxoAddress::new(resource_address, output.output.commitment.to_byte_type().into()); + let value = Utxo::new(output.to_utxo_output()); + self.new_substate(address, value)?; + } + + self.unlock_substate(resource_lock)?; + + if valid_transfer.revealed_output_amount.is_zero() { + return Ok(None); + } + + let container = ResourceContainer::stealth(resource_address, valid_transfer.revealed_output_amount); + + Ok(Some(container)) + } } diff --git a/crates/engine/src/state_store/bootstrap.rs b/crates/engine/src/state_store/bootstrap.rs index 8f65159404..2b7e33fa22 100644 --- a/crates/engine/src/state_store/bootstrap.rs +++ b/crates/engine/src/state_store/bootstrap.rs @@ -7,7 +7,7 @@ use tari_engine_types::{ }; use tari_template_lib::{ auth::ResourceAccessRules, - constants::{CONFIDENTIAL_TARI_RESOURCE_ADDRESS, PUBLIC_IDENTITY_RESOURCE_ADDRESS}, + constants::{PUBLIC_IDENTITY_RESOURCE_ADDRESS, STEALTH_TARI_RESOURCE_ADDRESS}, models::Metadata, prelude::{OwnerRule, ResourceType}, resource::TOKEN_SYMBOL, @@ -48,7 +48,7 @@ fn add_global_resources(state_db: &mut T) -> Result<(), StateSto )?; // Create the second layer tari resource - let id = SubstateId::Resource(CONFIDENTIAL_TARI_RESOURCE_ADDRESS); + let id = SubstateId::Resource(STEALTH_TARI_RESOURCE_ADDRESS); let mut metadata = Metadata::new(); metadata.insert(TOKEN_SYMBOL, "XTR".to_string()); state_db.set_state( @@ -56,7 +56,7 @@ fn add_global_resources(state_db: &mut T) -> Result<(), StateSto Substate::new( 0, Resource::new( - ResourceType::Confidential, + ResourceType::Stealth, None, OwnerRule::None, ResourceAccessRules::new(), @@ -64,7 +64,7 @@ fn add_global_resources(state_db: &mut T) -> Result<(), StateSto None, None, 6, - false, + true, ), ), )?; diff --git a/crates/engine/src/transaction/processor.rs b/crates/engine/src/transaction/processor.rs index bdc9ed0ec3..458099dd1c 100644 --- a/crates/engine/src/transaction/processor.rs +++ b/crates/engine/src/transaction/processor.rs @@ -217,7 +217,7 @@ impl + 'static> T finalize.result = TransactionResult::AcceptFeeRejectRest( finalize .result - .accept() + .any_accept() .cloned() .expect("The fee transaction should be there"), RejectReason::ExecutionFailure(err.to_string()), @@ -322,10 +322,33 @@ impl + 'static> T statement, revealed_input_bucket, } => Self::stealth_transfer(runtime, resource_address, statement, revealed_input_bucket), + Instruction::PayFee { + statement, + revealed_input_bucket, + } => Self::pay_fee(runtime, statement, revealed_input_bucket), } } - pub fn stealth_transfer( + fn pay_fee( + runtime: &Runtime, + statement: StealthTransferStatement, + revealed_funds_bucket: Option, + ) -> Result { + let revealed_funds_bucket = revealed_funds_bucket + .map(|id| { + runtime.resolve_workspace_id(&id).and_then(|r| { + r.decode().map_err(|e| RuntimeError::InvalidArgument { + argument: "revealed_funds_bucket", + reason: format!("Expected workspace id {id} to be a BucketId: {e}"), + }) + }) + }) + .transpose()?; + runtime.interface().pay_fee(statement, revealed_funds_bucket)?; + Ok(InstructionResult::empty()) + } + + fn stealth_transfer( runtime: &Runtime, resource_address: ResourceAddressRef, statement: StealthTransferStatement, @@ -350,14 +373,14 @@ impl + 'static> T Ok(InstructionResult::empty()) } - pub fn put_output_on_workspace_with_name(runtime: &Runtime, key: WorkspaceId) -> Result<(), TransactionError> { + fn put_output_on_workspace_with_name(runtime: &Runtime, key: WorkspaceId) -> Result<(), TransactionError> { runtime .interface() .workspace_invoke(WorkspaceAction::PutLastInstructionOutput, invoke_args![key].into())?; Ok(()) } - pub fn drop_all_proofs_in_workspace(runtime: &Runtime) -> Result<(), TransactionError> { + fn drop_all_proofs_in_workspace(runtime: &Runtime) -> Result<(), TransactionError> { runtime .interface() .workspace_invoke(WorkspaceAction::DropAllProofs, invoke_args![].into())?; @@ -365,7 +388,7 @@ impl + 'static> T } /// Allocating a new address for the given [`AllocatableAddressType`]. - pub fn allocate_address( + fn allocate_address( runtime: &Runtime, substate_type: AllocatableAddressType, workspace_id: WorkspaceId, @@ -392,7 +415,7 @@ impl + 'static> T } /// Load, validate template binary and adds it to TemplateProvider. - pub fn publish_template( + fn publish_template( config: &TransactionProcessorConfig, runtime: &Runtime, binary: Vec, @@ -412,7 +435,7 @@ impl + 'static> T Ok(InstructionResult::empty()) } - pub fn create_account( + fn create_account( template_provider: &TTemplateProvider, runtime: &Runtime, public_key_address: &RistrettoPublicKeyBytes, @@ -477,7 +500,7 @@ impl + 'static> T Ok(result) } - pub fn call_function( + pub(crate) fn call_function( template_provider: &TTemplateProvider, runtime: &Runtime, template_address: &TemplateAddress, @@ -522,7 +545,7 @@ impl + 'static> T Ok(result) } - pub fn call_method( + pub(crate) fn call_method( template_provider: &TTemplateProvider, runtime: &Runtime, call: ComponentCall, diff --git a/crates/engine/tests/access_rules.rs b/crates/engine/tests/access_rules.rs index aad20baca8..9f50b9a0f7 100644 --- a/crates/engine/tests/access_rules.rs +++ b/crates/engine/tests/access_rules.rs @@ -498,7 +498,7 @@ mod resource_access_rules { let badge_resource = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r))) @@ -578,7 +578,7 @@ mod resource_access_rules { let token_resource = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r))) @@ -650,7 +650,7 @@ mod resource_access_rules { let badge_resource = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r))) @@ -736,7 +736,7 @@ mod resource_access_rules { let badge_resource = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r))) @@ -875,7 +875,7 @@ mod resource_access_rules { let token_resource = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .filter_map(|(addr, s)| s.substate_value().as_resource().map(|r| (addr, r))) diff --git a/crates/engine/tests/account.rs b/crates/engine/tests/account.rs index 87763cb436..60ab878aad 100644 --- a/crates/engine/tests/account.rs +++ b/crates/engine/tests/account.rs @@ -215,20 +215,22 @@ fn gasless() { let fee_account_pk = RistrettoPublicKey::from_secret_key(&fee_account_sk); - let result = test.execute_expect_success( + test.execute_expect_success( Transaction::builder() .fee_transaction_pay_from_component(fee_account, 1000) .call_method(user_account, "withdraw", args![XTR, Amount(100)]) .put_last_instruction_output_on_workspace("b") .call_method(user2_account, "deposit", args![Workspace("b")]) - .call_method(user2_account, "get_balances", args![]) .add_signature(&fee_account_pk.to_byte_type(), &user_account_sk) .build_and_seal(&fee_account_sk), vec![fee_account_proof, user_account_proof], ); - let balance = result.expect_return::>(3); - assert_eq!(balance[0].1, 100u64); + let vaults = test + .read_only_state_store() + .get_vaults_for_account(user2_account) + .unwrap(); + assert_eq!(vaults.get(&XTR).unwrap().balance(), 100u64); } #[test] @@ -243,7 +245,6 @@ fn custom_access_rules() { .add_method_rule("get_balances", rule!(allow_all)) .add_method_rule("deposit", rule!(allow_all)) .add_method_rule("deposit_all", rule!(allow_all)) - .add_method_rule("get_non_fungible_ids", rule!(allow_all)) // We are going to make it so anyone can withdraw .default(rule!(allow_all)); diff --git a/crates/engine/tests/address_allocation.rs b/crates/engine/tests/address_allocation.rs index 6ae0be3224..4304062066 100644 --- a/crates/engine/tests/address_allocation.rs +++ b/crates/engine/tests/address_allocation.rs @@ -21,7 +21,7 @@ fn it_allocates_addresses_in_template_code() { let component_addr = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .find_map(|(k, _)| k.as_component_address()) @@ -36,7 +36,7 @@ fn it_allocates_addresses_in_template_code() { let actual = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .find_map(|(k, _)| k.as_resource_address()) @@ -45,7 +45,7 @@ fn it_allocates_addresses_in_template_code() { let component_state = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .find_map(|(_, substate)| substate.substate_value().component()) @@ -125,7 +125,7 @@ fn it_allocates_an_address_using_instructions() { let actual_comp = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .find_map(|(k, _)| k.as_component_address()) @@ -137,7 +137,7 @@ fn it_allocates_an_address_using_instructions() { let actual_resx = result .finalize .result - .accept() + .any_accept() .unwrap() .up_iter() .find_map(|(k, _)| k.as_resource_address()) diff --git a/crates/engine/tests/confidential.rs b/crates/engine/tests/confidential.rs index ef1d7f063b..5a47798023 100644 --- a/crates/engine/tests/confidential.rs +++ b/crates/engine/tests/confidential.rs @@ -516,7 +516,7 @@ fn mint_with_view_key() { vec![user_proof], ); - let diff = result.finalize.result.accept().unwrap(); + let diff = result.finalize.result.any_accept().unwrap(); let faucet_vault = diff .up_iter() .find(|(addr, _)| addr.is_vault() && addr.as_vault_id().unwrap().entity_id() == faucet_entity_id) diff --git a/crates/engine/tests/fees.rs b/crates/engine/tests/fees.rs index 6e2e3dea0f..db82115bf9 100644 --- a/crates/engine/tests/fees.rs +++ b/crates/engine/tests/fees.rs @@ -4,12 +4,7 @@ use std::iter; use tari_engine_types::commit_result::RejectReason; -use tari_template_lib::{ - call_args, - constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - models::ComponentAddress, - types::Amount, -}; +use tari_template_lib::{call_args, 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_transaction::{args, Transaction}; @@ -18,12 +13,7 @@ fn deducts_fees_from_payments_and_refunds_the_rest() { let mut test = TemplateTest::new(["tests/templates/state"]); let (account, owner_token, private_key) = test.create_funded_account(); - let orig_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let orig_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); test.enable_fees(); @@ -39,12 +29,7 @@ fn deducts_fees_from_payments_and_refunds_the_rest() { // Check difference was refunded let payment = result.finalize.fee_receipt; - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_eq!(new_balance, orig_balance - payment.total_fees_charged().into()); assert_eq!(payment.total_refunded(), 1000 - payment.total_fees_charged()); assert!(payment.is_paid_in_full()); @@ -55,12 +40,7 @@ fn deducts_fees_when_transaction_fails() { let mut test = TemplateTest::new(["tests/templates/state"]); let (account, owner_token, private_key) = test.create_funded_account(); - let orig_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let orig_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); test.enable_fees(); @@ -79,12 +59,7 @@ fn deducts_fees_when_transaction_fails() { // Check the fee was still paid let payment = result.finalize.fee_receipt; - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_ne!(payment.total_fees_paid(), 0); assert_eq!(orig_balance - new_balance, payment.total_fees_paid()); } @@ -114,12 +89,7 @@ fn deposit_from_faucet_then_pay() { test.disable_fees(); let payment = result.finalize.fee_receipt; - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + 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() @@ -130,12 +100,13 @@ fn deposit_from_faucet_then_pay() { fn another_account_pays_partially_for_fees() { let mut test = TemplateTest::new(iter::empty::<&str>()); - let (account, owner_token, private_key) = test.create_empty_account(); + let (account, _, _) = test.create_empty_account(); let (account_fee, owner_token_fee, _) = test.create_funded_account(); + let (account_fee2, owner_token_fee2, _) = test.create_funded_account(); let orig_balance: Amount = test.call_method( account_fee, "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], + call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![], ); @@ -143,41 +114,37 @@ fn another_account_pays_partially_for_fees() { let result = test.execute_expect_success( Transaction::builder() - // Faucet pays a little - .fee_transaction_pay_from_component(test_faucet_component(), Amount::from(200)) - // Account pays the rest - .fee_transaction_pay_from_component(account_fee, Amount::from(1000)) - .call_method(test_faucet_component(), "take_free_coins", args![]) - .put_last_instruction_output_on_workspace("bucket") - .call_method(account, "deposit", args![Workspace("bucket")]) - .build_and_seal(&private_key), - vec![owner_token_fee, owner_token], + // Faucet pays a little + .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![]) + .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 + .build_and_seal(test.secret_key()), + vec![owner_token_fee, owner_token_fee2], ); test.disable_fees(); // Check difference was refunded let payment = result.finalize.fee_receipt; - let new_balance: Amount = test.call_method( - account_fee, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let vaults = test + .read_only_state_store() + .get_vaults_for_account(account_fee2) + .unwrap(); + let vault = vaults.get(&STEALTH_TARI_RESOURCE_ADDRESS).unwrap(); + assert_eq!( - new_balance, + vault.balance(), orig_balance + Amount::from(200) - Amount::from(payment.total_fees_charged()) ); // Check that this test is charging more than just the faucet's portion assert!(Amount::from(200) < payment.total_fees_charged()); // Check the rest of the transaction was committed - let balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_eq!(balance, Amount::from(1000)); } @@ -186,12 +153,8 @@ fn failed_fee_transaction() { let mut test = TemplateTest::new(["tests/templates/state"]); let (account, owner_token, private_key) = test.create_funded_account(); - let initial_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let initial_balance: Amount = + test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); test.enable_fees(); let result = test @@ -214,12 +177,7 @@ fn failed_fee_transaction() { assert!(matches!(reason, RejectReason::ExecutionFailure(_))); test.disable_fees(); - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_eq!(new_balance, initial_balance); } @@ -229,12 +187,7 @@ fn fail_partial_paid_fees() { let (account, owner_token, private_key) = test.create_funded_account(); let (account2, owner_token2, _) = test.create_funded_account(); - let orig_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let orig_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); test.enable_fees(); let result = test.execute_expect_commit( @@ -243,7 +196,7 @@ fn fail_partial_paid_fees() { .fee_transaction_pay_from_component(account, Amount::ONE_HUNDRED) // These instructions should not be applied .call_method(account2, "withdraw", args![ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + STEALTH_TARI_RESOURCE_ADDRESS, Amount(500) ]) .put_last_instruction_output_on_workspace("bucket") @@ -263,12 +216,7 @@ fn fail_partial_paid_fees() { // Check that the fee paid was deducted let payment = result.finalize.fee_receipt; assert!(!payment.is_paid_in_full()); - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_eq!(new_balance, orig_balance - Amount::ONE_HUNDRED); } @@ -278,12 +226,7 @@ fn fail_pay_less_fees_than_fee_transaction() { let (account, owner_token, private_key) = test.create_funded_account(); let (account2, owner_token2, _) = test.create_funded_account(); - let orig_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let orig_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); let state: ComponentAddress = test.call_function("State", "new", args![], vec![]); test.enable_fees(); @@ -308,7 +251,7 @@ fn fail_pay_less_fees_than_fee_transaction() { }) // These instructions should not be applied .call_method(account2, "withdraw", args![ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + STEALTH_TARI_RESOURCE_ADDRESS, Amount(500) ]) .put_last_instruction_output_on_workspace("bucket") @@ -330,12 +273,7 @@ fn fail_pay_less_fees_than_fee_transaction() { ); // Fee was not deducted - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_eq!(new_balance, orig_balance); // State was not updated @@ -349,12 +287,7 @@ fn fail_pay_too_little_no_fee_instruction() { let (account, owner_token, private_key) = test.create_funded_account(); let (account2, owner_token2, _) = test.create_funded_account(); - let orig_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let orig_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); test.enable_fees(); @@ -364,7 +297,7 @@ fn fail_pay_too_little_no_fee_instruction() { builder // These instructions should not be applied .call_method(account2, "withdraw", args![ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + STEALTH_TARI_RESOURCE_ADDRESS, Amount(500) ]) .put_last_instruction_output_on_workspace("bucket") @@ -383,12 +316,7 @@ fn fail_pay_too_little_no_fee_instruction() { ); // Fee was not deducted - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_eq!(new_balance, orig_balance); } @@ -398,12 +326,7 @@ fn success_pay_fee_in_main_instructions() { let (account, owner_token, private_key) = test.create_funded_account(); let (account2, owner_token2, _) = test.create_funded_account(); - let orig_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let orig_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); test.enable_fees(); @@ -411,10 +334,7 @@ fn success_pay_fee_in_main_instructions() { Transaction::builder() .with_fee_instructions_builder(|builder| { builder - .call_method(account2, "withdraw", args![ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - Amount(500) - ]) + .call_method(account2, "withdraw", args![STEALTH_TARI_RESOURCE_ADDRESS, Amount(500)]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) .call_method(account, "pay_fee", args![Amount(1000)]) @@ -428,12 +348,7 @@ fn success_pay_fee_in_main_instructions() { let fees = result.expect_fees_paid_in_full(); // Fee was deducted - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_eq!( new_balance, orig_balance + Amount::from(500) - fees.total_fees_charged().into() @@ -445,22 +360,14 @@ fn dangling_bucket_pay_fees() { let mut test = TemplateTest::new(["tests/templates/state"]); let (account, owner_token, private_key) = test.create_funded_account(); - let orig_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let orig_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); test.enable_fees(); let result = test.execute_and_commit_on_success( Transaction::builder() .fee_transaction_pay_from_component(account, Amount::from(500)) - .call_method(account, "withdraw", args![ - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - Amount(10) - ]) + .call_method(account, "withdraw", args![STEALTH_TARI_RESOURCE_ADDRESS, Amount(10)]) .put_last_instruction_output_on_workspace("dangling_bucket") .build_and_seal(&private_key), vec![owner_token], @@ -478,12 +385,7 @@ fn dangling_bucket_pay_fees() { // Check the fee was still paid let payment = result.finalize.fee_receipt; - let new_balance: Amount = test.call_method( - account, - "balance", - call_args![CONFIDENTIAL_TARI_RESOURCE_ADDRESS], - vec![], - ); + let new_balance: Amount = test.call_method(account, "balance", call_args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]); assert_ne!(payment.total_fees_paid(), 0); assert_eq!(orig_balance - new_balance, payment.total_fees_paid()); } diff --git a/crates/engine/tests/stealth.rs b/crates/engine/tests/stealth.rs index d3fd11f4db..3a7cb22d0d 100644 --- a/crates/engine/tests/stealth.rs +++ b/crates/engine/tests/stealth.rs @@ -106,7 +106,7 @@ fn basic_transfer() { vec![], ); - let diff = result.finalize.accept().unwrap(); + let diff = result.finalize.any_accept().unwrap(); let utxos = diff .up_iter() .filter_map(|(_, substate)| substate.substate_value().as_utxo()) @@ -142,7 +142,7 @@ fn programmatic_transfer() { vec![], ); - let diff = result.finalize.accept().unwrap(); + let diff = result.finalize.any_accept().unwrap(); let utxos = diff .up_iter() .filter_map(|(_, substate)| substate.substate_value().as_utxo()) @@ -178,7 +178,7 @@ fn transfer_with_revealed_outputs() { vec![], ); - let diff = result.finalize.accept().unwrap(); + let diff = result.finalize.any_accept().unwrap(); let utxos = diff .up_iter() .filter_map(|(_, substate)| substate.substate_value().as_utxo()) @@ -228,7 +228,7 @@ fn transfer_revealed_between_accounts() { vec![alice_proof], ); - let diff = result.finalize.accept().unwrap(); + let diff = result.finalize.any_accept().unwrap(); let utxos = diff .up_iter() .filter_map(|(_, substate)| substate.substate_value().as_utxo()) @@ -376,7 +376,7 @@ fn many_outputs() { vec![], ); - let diff = result.finalize.accept().unwrap(); + let diff = result.finalize.any_accept().unwrap(); let utxos = diff .up_iter() .filter_map(|(_, substate)| substate.substate_value().as_utxo()) @@ -433,7 +433,7 @@ fn mint_with_view_key() { vec![], ); - let diff = result.finalize.result.accept().unwrap(); + let diff = result.finalize.result.any_accept().unwrap(); let utxos = diff .up_iter() .filter_map(|(addr, substate)| { diff --git a/crates/engine/tests/templates/shenanigans/src/lib.rs b/crates/engine/tests/templates/shenanigans/src/lib.rs index cf916b438f..d7434021f0 100644 --- a/crates/engine/tests/templates/shenanigans/src/lib.rs +++ b/crates/engine/tests/templates/shenanigans/src/lib.rs @@ -18,12 +18,12 @@ mod template { impl Shenanigans { pub fn dangling_vault() -> Self { - let _vault = Vault::new_empty(CONFIDENTIAL_TARI_RESOURCE_ADDRESS); + let _vault = Vault::new_empty(STEALTH_TARI_RESOURCE_ADDRESS); Self::default() } pub fn return_vault() -> Vault { - Vault::new_empty(CONFIDENTIAL_TARI_RESOURCE_ADDRESS) + Vault::new_empty(STEALTH_TARI_RESOURCE_ADDRESS) } pub fn new() -> Self { @@ -31,7 +31,7 @@ mod template { } pub fn with_vault() -> Self { - let vault = Vault::new_empty(CONFIDENTIAL_TARI_RESOURCE_ADDRESS); + let vault = Vault::new_empty(STEALTH_TARI_RESOURCE_ADDRESS); Self { vault: Some(vault), ..Default::default() @@ -72,7 +72,7 @@ mod template { } pub fn with_vault_copy() -> Self { - let vault = Vault::new_empty(CONFIDENTIAL_TARI_RESOURCE_ADDRESS); + let vault = Vault::new_empty(STEALTH_TARI_RESOURCE_ADDRESS); let vault_copy = Vault::for_test(vault.vault_id()); Self { vault: Some(vault), diff --git a/crates/engine/tests/test.rs b/crates/engine/tests/test.rs index 1d8bf74767..065cce15b6 100644 --- a/crates/engine/tests/test.rs +++ b/crates/engine/tests/test.rs @@ -36,7 +36,7 @@ use tari_ootle_common_types::substate_type::SubstateType; use tari_template_builtin::{ACCOUNT_TEMPLATE_ADDRESS, NFT_FAUCET_TEMPLATE_ADDRESS}; use tari_template_lib::{ call_args, - models::{ComponentAddress, NonFungible, NonFungibleAddress, NonFungibleId, ResourceAddress}, + models::{ComponentAddress, NonFungible, NonFungibleAddress, ResourceAddress}, types::{crypto::RistrettoPublicKeyBytes, TemplateAddress}, }; use tari_template_test_tooling::{support::assert_error::assert_reject_reason, TemplateTest}; @@ -639,10 +639,6 @@ mod basic_nft { template_test .execute_and_commit_manifest( r#" - let account = var!["account"]; - let sparkle_nft_resource = var!["nft_resx"]; - account.get_non_fungible_ids(sparkle_nft_resource); - let sparkle_nft = var!["nft"]; let sparkle_nft_id = var!["nft_id"]; sparkle_nft.inc_brightness(sparkle_nft_id, 10u32); @@ -1011,7 +1007,6 @@ mod emoji_id { mod tickets { use tari_template_lib::{ - call_args, constants::{XTR, XTR_FAUCET_COMPONENT_ADDRESS}, types::Amount, }; @@ -1026,17 +1021,17 @@ mod tickets { #[test] #[allow(clippy::too_many_lines)] fn buy_and_redeem_ticket() { - let mut template_test = TemplateTest::new(vec!["tests/templates/nft/tickets"]); + let mut test = TemplateTest::new(vec!["tests/templates/nft/tickets"]); // create an account - let (account_address, owner_proof, secret) = template_test.create_funded_account(); + let (account_address, owner_proof, secret) = test.create_funded_account(); // initialize the ticket seller - let ticket_template = template_test.get_template_address("TicketSeller"); + let ticket_template = test.get_template_address("TicketSeller"); let initial_supply = 10; let price = Amount::from(20); let event_description = "My music festival".to_string(); - let result = template_test.execute_expect_success( + let result = test.execute_expect_success( Transaction::builder() .call_function(ticket_template, "new", args![initial_supply, price, event_description]) .build_and_seal(&secret), @@ -1052,11 +1047,11 @@ mod tickets { .unwrap(); // at the beginning we have the initial supply of tickets - let total_supply: Amount = template_test.call_method(ticket_seller, "total_supply", args![], vec![]); + 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 - template_test.execute_expect_success( + test.execute_expect_success( Transaction::builder() .call_method(XTR_FAUCET_COMPONENT_ADDRESS, "take_free_coins", args![]) .put_last_instruction_output_on_workspace("coins") @@ -1066,7 +1061,7 @@ mod tickets { ); // buy a ticket - template_test.execute_expect_success( + test.execute_expect_success( Transaction::builder() .call_method(account_address, "withdraw", args![XTR, Amount(20)]) .put_last_instruction_output_on_workspace("payment") @@ -1078,12 +1073,12 @@ mod tickets { ); // redeem a ticket - let ticket_ids: Vec = template_test.call_method( - account_address, - "get_non_fungible_ids", - call_args![ticket_resource], - vec![], - ); + let vaults = test + .read_only_state_store() + .get_vaults_for_account(account_address) + .unwrap(); + let vault = vaults.get(&ticket_resource).unwrap(); + let ticket_ids = vault.get_non_fungible_ids(); assert_eq!(ticket_ids.len(), 1); let ticket_id = ticket_ids.first().unwrap().clone(); @@ -1095,19 +1090,18 @@ mod tickets { ("ticket_addr", ManifestValue::NonFungibleId(ticket_id.clone())), ]; - template_test - .execute_and_commit_manifest( - r#" + test.execute_and_commit_manifest( + r#" let account = var!["account"]; let ticket_seller = var!["ticket_seller"]; let ticket_addr = var!["ticket_addr"]; ticket_seller.redeem_ticket(ticket_addr); "#, - vars.clone(), - vec![], - ) - .unwrap(); + vars.clone(), + vec![], + ) + .unwrap(); #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Ticket { @@ -1115,7 +1109,7 @@ mod tickets { } let ticket_substate_addr = SubstateId::NonFungible(NonFungibleAddress::new(ticket_resource, ticket_id)); - let ticket_nft = template_test + let ticket_nft = test .read_only_state_store() .get_substate(&ticket_substate_addr) .unwrap() diff --git a/crates/engine_types/src/commit_result.rs b/crates/engine_types/src/commit_result.rs index 583b71cae4..2deff0b64a 100644 --- a/crates/engine_types/src/commit_result.rs +++ b/crates/engine_types/src/commit_result.rs @@ -172,8 +172,8 @@ impl FinalizeResult { /// Returns the accept diff if the transaction was accepted, otherwise None. /// Acceptance includes fee-only acceptance. - pub fn accept(&self) -> Option<&SubstateDiff> { - self.result.accept() + pub fn any_accept(&self) -> Option<&SubstateDiff> { + self.result.any_accept() } pub fn into_accept(self) -> Option { @@ -230,7 +230,7 @@ impl TransactionResult { matches!(self, Self::Reject(_)) } - pub fn accept(&self) -> Option<&SubstateDiff> { + pub fn any_accept(&self) -> Option<&SubstateDiff> { match self { Self::Accept(substate_diff) => Some(substate_diff), Self::AcceptFeeRejectRest(substate_diff, _) => Some(substate_diff), diff --git a/crates/engine_types/src/confidential/claim.rs b/crates/engine_types/src/confidential/claim.rs index 0d94244116..12e6b7c28e 100644 --- a/crates/engine_types/src/confidential/claim.rs +++ b/crates/engine_types/src/confidential/claim.rs @@ -3,17 +3,18 @@ use serde::{Deserialize, Serialize}; use tari_template_lib::{ - models::{ConfidentialWithdrawProof, UnclaimedConfidentialOutputAddress}, - prelude::RistrettoPublicKeyBytes, + models::UnclaimedConfidentialOutputAddress, + prelude::{RistrettoPublicKeyBytes, StealthTransferStatement}, types::crypto::{CommitmentSignatureBytes, RangeProofBytes}, }; #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub struct ConfidentialClaim { - pub public_key: RistrettoPublicKeyBytes, +pub struct TariStealthClaim { + /// This is typically the public nonce that the UTXO was burnt with + pub burn_public_key: RistrettoPublicKeyBytes, pub output_address: UnclaimedConfidentialOutputAddress, pub range_proof: RangeProofBytes, pub proof_of_knowledge: CommitmentSignatureBytes, - pub withdraw_proof: Option, + pub transfer_statement: Option, } diff --git a/crates/engine_types/src/fees.rs b/crates/engine_types/src/fees.rs index ea92888b34..246cd8fa79 100644 --- a/crates/engine_types/src/fees.rs +++ b/crates/engine_types/src/fees.rs @@ -11,6 +11,9 @@ pub struct FeeReceipt { pub total_fee_payment: u64, /// Total fees paid after refunds pub total_fees_paid: u64, + /// The amount of non-refundable fees which the user overpaid. Fees cannot be refunded when paying purely with a + /// stealth reveal (since we do not know the account/vault to refund). + pub total_fee_overcharge: u64, /// Breakdown of fee costs pub cost_breakdown: FeeBreakdown, } @@ -75,7 +78,7 @@ pub struct FeeBreakdown { } impl FeeBreakdown { - pub fn insert(&mut self, source: FeeSource, amount: u64) { + pub fn add(&mut self, source: FeeSource, amount: u64) { match self.breakdown.entry(source) { Entry::Occupied(entry) => { *entry.into_mut() += amount; diff --git a/crates/engine_types/src/hashing.rs b/crates/engine_types/src/hashing.rs index b476483b7f..f74b807c50 100644 --- a/crates/engine_types/src/hashing.rs +++ b/crates/engine_types/src/hashing.rs @@ -37,6 +37,7 @@ use tari_crypto::{ use tari_hashing::TariEngineHashDomain; use tari_template_lib::types::Hash; +// TODO: switch all hashes to borsh pub fn engine_hasher64(label: EngineHashDomainLabel) -> TariHasher64 { TariHasher64::new_with_label::(label.as_label()) } diff --git a/crates/engine_types/src/instruction.rs b/crates/engine_types/src/instruction.rs index d930c5cb0b..f249bf8b6a 100644 --- a/crates/engine_types/src/instruction.rs +++ b/crates/engine_types/src/instruction.rs @@ -14,7 +14,7 @@ use tari_template_lib::{ use crate::{ component_call::ComponentCall, - confidential::ConfidentialClaim, + confidential::TariStealthClaim, resource_address_ref::ResourceAddressRef, ValidatorFeePoolAddress, }; @@ -40,6 +40,8 @@ pub enum Instruction { CallMethod { call: ComponentCall, method: String, + // TODO: remove this as it causes tricky issues that are hard to track down (typically Signature errors). + // Rather have clients provide raw arguments using CBOR. #[serde(deserialize_with = "crate::argument_parser::json_deserialize")] // Argument parser takes an array of strings as input #[cfg_attr(feature = "ts", ts(type = "Array"))] @@ -53,7 +55,7 @@ pub enum Instruction { message: String, }, ClaimBurn { - claim: Box, + claim: Box, }, ClaimValidatorFees { #[cfg_attr(feature = "ts", ts(type = "string"))] @@ -77,6 +79,10 @@ pub enum Instruction { statement: StealthTransferStatement, revealed_input_bucket: Option, }, + PayFee { + statement: StealthTransferStatement, + revealed_input_bucket: Option, + }, } impl Instruction { @@ -100,6 +106,7 @@ impl Instruction { } impl Display for Instruction { + #[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::CreateAccount { @@ -173,10 +180,10 @@ impl Display for Instruction { key, resource_address, min_amount ) }, - Instruction::PublishTemplate { .. } => { + Self::PublishTemplate { .. } => { write!(f, "PublishTemplate") }, - Instruction::AllocateAddress { + Self::AllocateAddress { allocatable_type: substate_type, workspace_id, } => { @@ -185,19 +192,38 @@ impl Display for Instruction { "AllocateAddress {{ substate_type: {substate_type:?}, workspace ID: {workspace_id} }}" ) }, - Instruction::StealthTransfer { + Self::StealthTransfer { resource_address_ref: resource_address, statement, revealed_input_bucket: bucket, } => { write!( f, - "StealthTransfer {{ resource_address: {}, output(s): {}, rp-size: {}, bucket: {:?} }}", + "StealthTransfer {{ resource_address: {}, output(s): {}, rp-size: {}", resource_address, statement.outputs_statement.outputs.len(), statement.outputs_statement.agg_range_proof.len(), - bucket - ) + )?; + match bucket { + Some(id) => write!(f, ", revealed_input_bucket: Some({}) }}", id), + None => write!(f, ", revealed_input_bucket: None }}"), + } + }, + Self::PayFee { + statement, + revealed_input_bucket: bucket, + } => { + write!( + f, + "PayFee {{ revealed_input: {}, output(s): {}, rp-size: {}", + statement.inputs_statement.revealed_amount, + statement.outputs_statement.outputs.len(), + statement.outputs_statement.agg_range_proof.len(), + )?; + match bucket { + Some(id) => write!(f, ", revealed_input_bucket: Some({}) }}", id), + None => write!(f, ", revealed_input_bucket: None }}"), + } }, } } diff --git a/crates/engine_types/src/resource_container.rs b/crates/engine_types/src/resource_container.rs index ef108b9ce1..def8ec8dc3 100644 --- a/crates/engine_types/src/resource_container.rs +++ b/crates/engine_types/src/resource_container.rs @@ -277,8 +277,8 @@ impl ResourceContainer { (this, other) => { return Err(ResourceError::ResourceTypeMismatch { operate: "deposit", - expected: this.resource_type(), given: other.resource_type(), + expected: this.resource_type(), }) }, } @@ -353,7 +353,7 @@ impl ResourceContainer { } } - pub fn recall_all(&mut self) -> Result { + pub fn withdraw_all(&mut self) -> Result { match self { Self::Fungible { .. } | Self::NonFungible { .. } | Self::Stealth { .. } => self.withdraw(self.amount()), Self::Confidential { @@ -866,7 +866,7 @@ impl ResourceContainer { #[derive(Debug, thiserror::Error)] pub enum ResourceError { - #[error("Attempted to {operate} a {expected} resource, but the resource type is {given}")] + #[error("Attempted to {operate} a {given} resource, but the container resource type is {expected}")] ResourceTypeMismatch { operate: &'static str, expected: ResourceType, diff --git a/crates/engine_types/src/validator_fee.rs b/crates/engine_types/src/validator_fee.rs index 38366868fd..3603416489 100644 --- a/crates/engine_types/src/validator_fee.rs +++ b/crates/engine_types/src/validator_fee.rs @@ -14,7 +14,7 @@ use tari_template_lib::{ auth::{OwnerRule, Ownership}, constants::XTR, models::BinaryTag, - types::{crypto::RistrettoPublicKeyBytes, Hash, KeyParseError, ObjectKey}, + types::{crypto::RistrettoPublicKeyBytes, Amount, Hash, KeyParseError, ObjectKey}, }; use crate::resource_container::{ResourceContainer, ResourceError}; @@ -168,12 +168,10 @@ impl ValidatorFeePool { } let amount = self.amount; self.amount = 0; - Ok(ResourceContainer::Confidential { + Ok(ResourceContainer::Stealth { address: XTR, - commitments: Default::default(), revealed_amount: amount.into(), - locked_commitments: Default::default(), - locked_revealed_amount: Default::default(), + locked_amount: Amount::zero(), }) } } diff --git a/crates/engine_types/src/vault.rs b/crates/engine_types/src/vault.rs index 6c268d3d29..227c41c4d5 100644 --- a/crates/engine_types/src/vault.rs +++ b/crates/engine_types/src/vault.rs @@ -79,7 +79,7 @@ impl Vault { } pub fn recall_all(&mut self) -> Result { - self.resource_container.recall_all() + self.resource_container.withdraw_all() } pub fn recall_confidential>( diff --git a/crates/indexer_lib/Cargo.toml b/crates/indexer_lib/Cargo.toml index e69b99800e..c49a31ce18 100644 --- a/crates/indexer_lib/Cargo.toml +++ b/crates/indexer_lib/Cargo.toml @@ -13,7 +13,6 @@ tari_epoch_manager = { workspace = true } tari_engine_types = { workspace = true } tari_validator_node_rpc = { workspace = true } -async-trait = { workspace = true } log = { workspace = true } serde = { workspace = true, default-features = true } thiserror = { workspace = true } diff --git a/crates/indexer_lib/src/substate_cache.rs b/crates/indexer_lib/src/substate_cache.rs index 860c08f451..d8ead57868 100644 --- a/crates/indexer_lib/src/substate_cache.rs +++ b/crates/indexer_lib/src/substate_cache.rs @@ -20,7 +20,8 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use async_trait::async_trait; +use std::future::Future; + use serde::{Deserialize, Serialize}; use tari_validator_node_rpc::client::SubstateResult; @@ -34,8 +35,14 @@ pub struct SubstateCacheEntry { pub substate_result: SubstateResult, } -#[async_trait] pub trait SubstateCache: Send + Sync { - async fn read(&self, address: String) -> Result, SubstateCacheError>; - async fn write(&self, address: String, entry: &SubstateCacheEntry) -> Result<(), SubstateCacheError>; + fn read( + &self, + address: String, + ) -> impl Future, SubstateCacheError>> + Send; + fn write( + &self, + address: String, + entry: &SubstateCacheEntry, + ) -> impl Future> + Send; } diff --git a/crates/p2p/proto/transaction.proto b/crates/p2p/proto/transaction.proto index d963c7d7bb..5242926c27 100644 --- a/crates/p2p/proto/transaction.proto +++ b/crates/p2p/proto/transaction.proto @@ -41,6 +41,7 @@ message Instruction { PUBLISH_TEMPLATE = 9; ALLOCATE_ADDRESS = 10; STEALTH_TRANSFER = 11; + PAY_FEE = 12; } InstructionType instruction_type = 1; @@ -67,7 +68,7 @@ message Instruction { bytes claim_burn_range_proof = 11; CommitmentSignature claim_burn_proof_of_knowledge = 12; bytes claim_burn_public_key = 13; - ConfidentialWithdrawProof claim_burn_withdraw_proof = 14; + StealthTransferStatement claim_burn_transfer = 14; // ClaimValidatorFees bytes claim_validator_fees_address = 15; @@ -94,6 +95,9 @@ message Instruction { ResourceAddressRef stealth_transfer_resource_address = 26; StealthTransferStatement stealth_transfer_statement = 27; WorkspaceOffsetId stealth_transfer_revealed_input_bucket = 28; + + StealthTransferStatement pay_fee_stealth_transfer_statement = 29; + WorkspaceOffsetId pay_fee_revealed_input_bucket = 30; } message ComponentCall { diff --git a/crates/p2p/src/conversions/transaction.rs b/crates/p2p/src/conversions/transaction.rs index fa6f09f6d4..f59c196be7 100644 --- a/crates/p2p/src/conversions/transaction.rs +++ b/crates/p2p/src/conversions/transaction.rs @@ -27,7 +27,7 @@ use std::{ use anyhow::{anyhow, Context}; use tari_engine_types::{ - confidential::ConfidentialClaim, + confidential::TariStealthClaim, instruction::Instruction, substate::SubstateId, ComponentCall, @@ -250,8 +250,8 @@ impl TryFrom for Instruction { message: request.log_message, }, InstructionType::ClaimBurn => Instruction::ClaimBurn { - claim: Box::new(ConfidentialClaim { - public_key: request + claim: Box::new(TariStealthClaim { + burn_public_key: request .claim_burn_public_key .as_slice() .try_into() @@ -268,7 +268,7 @@ impl TryFrom for Instruction { .ok_or_else(|| anyhow!("claim_burn_proof_of_knowledge not provided"))? .try_into() .map_err(|e| anyhow!("claim_burn_proof_of_knowledge: {}", e))?, - withdraw_proof: request.claim_burn_withdraw_proof.map(TryInto::try_into).transpose()?, + transfer_statement: request.claim_burn_transfer.map(TryInto::try_into).transpose()?, }), }, InstructionType::ClaimValidatorFees => Instruction::ClaimValidatorFees { @@ -314,6 +314,17 @@ impl TryFrom for Instruction { .map(TryInto::try_into) .transpose()?, }, + InstructionType::PayFee => Instruction::PayFee { + statement: request + .pay_fee_stealth_transfer_statement + .ok_or_else(|| anyhow!("pay_fee_stealth_transfer_statement not provided"))? + .try_into() + .context("pay_fee_stealth_transfer_statement conversion failed")?, + revealed_input_bucket: request + .pay_fee_revealed_input_bucket + .map(TryInto::try_into) + .transpose()?, + }, }; Ok(instruction) @@ -367,8 +378,8 @@ impl From for proto::transaction::Instruction { result.claim_burn_commitment_address = claim.output_address.as_bytes().to_vec(); result.claim_burn_range_proof = claim.range_proof.into_vec(); result.claim_burn_proof_of_knowledge = Some(claim.proof_of_knowledge.into()); - result.claim_burn_public_key = claim.public_key.to_vec(); - result.claim_burn_withdraw_proof = claim.withdraw_proof.map(Into::into); + result.claim_burn_public_key = claim.burn_public_key.to_vec(); + result.claim_burn_transfer = claim.transfer_statement.map(Into::into); }, Instruction::ClaimValidatorFees { address } => { result.instruction_type = InstructionType::ClaimValidatorFees as i32; @@ -410,6 +421,14 @@ impl From for proto::transaction::Instruction { result.stealth_transfer_statement = Some(statement.into()); result.stealth_transfer_revealed_input_bucket = revealed_input_bucket.map(Into::into); }, + Instruction::PayFee { + statement, + revealed_input_bucket, + } => { + result.instruction_type = InstructionType::PayFee as i32; + result.pay_fee_stealth_transfer_statement = Some(statement.into()); + result.pay_fee_revealed_input_bucket = revealed_input_bucket.map(Into::into); + }, } result } diff --git a/crates/state_store_tests/src/transactions.rs b/crates/state_store_tests/src/transactions.rs index b55fdd4bcf..9117c66953 100644 --- a/crates/state_store_tests/src/transactions.rs +++ b/crates/state_store_tests/src/transactions.rs @@ -282,6 +282,7 @@ mod transaction_execution_operations { FeeReceipt { total_fee_payment: 0, total_fees_paid: 0, + total_fee_overcharge: 0, cost_breakdown: FeeBreakdown::default(), }, ), @@ -306,6 +307,7 @@ mod transaction_execution_operations { FeeReceipt { total_fee_payment: 0, total_fees_paid: 0, + total_fee_overcharge: 0, cost_breakdown: FeeBreakdown::default(), }, ), diff --git a/crates/template_builtin/templates/account/src/lib.rs b/crates/template_builtin/templates/account/src/lib.rs index 05130d8b55..eccb063386 100644 --- a/crates/template_builtin/templates/account/src/lib.rs +++ b/crates/template_builtin/templates/account/src/lib.rs @@ -55,7 +55,6 @@ mod account_template { .add_method_rule("get_balances", rule!(allow_all)) .add_method_rule("deposit", rule!(allow_all)) .add_method_rule("deposit_all", rule!(allow_all)) - .add_method_rule("get_non_fungible_ids", rule!(allow_all)) // By default, only the owner of the token will be able to withdraw funds from the account .default(rule!(deny_all)), ); @@ -73,7 +72,6 @@ mod account_template { .create() } - // #[access_rule(allow_all)] pub fn balance(&self, resource: ResourceAddress) -> Amount { self.vaults .get(&resource) @@ -81,11 +79,11 @@ mod account_template { .unwrap_or_else(Amount::zero) } + /// Only applies to confidential resources. Returns the number of commitments in the vault. pub fn confidential_commitment_count(&self, resource: ResourceAddress) -> u32 { self.get_vault(resource).commitment_count() } - // #[access_rule(requires(owner_badge))] pub fn withdraw(&mut self, resource: ResourceAddress, amount: Amount) -> Bucket { emit_event("withdraw", [ ("amount", amount.to_string()), @@ -95,7 +93,6 @@ mod account_template { v.withdraw(amount) } - // #[access_rules(requires(owner_badge))] pub fn withdraw_non_fungible(&mut self, resource: ResourceAddress, nf_id: NonFungibleId) -> Bucket { emit_event("withdraw_non_fungible", [ ("id", nf_id.to_string()), @@ -117,7 +114,6 @@ mod account_template { v.withdraw_non_fungibles(nf_ids) } - // #[access_rules(requires(owner_badge))] pub fn withdraw_confidential( &mut self, resource: ResourceAddress, @@ -132,11 +128,11 @@ mod account_template { v.withdraw_confidential(withdraw_proof) } - // #[access_rules(allow_all)] pub fn deposit(&mut self, bucket: Bucket) { emit_event("deposit", [ ("amount", bucket.amount().to_string()), ("resource", bucket.resource_address().to_string()), + ("resource_type", bucket.resource_type().to_string()), ]); let resource_address = bucket.resource_address(); let vault_mut = self @@ -152,12 +148,6 @@ mod account_template { } } - // #[access_rules(require(owner_badge))] - pub fn get_non_fungible_ids(&self, resource: ResourceAddress) -> Vec { - let v = self.get_vault(resource); - v.get_non_fungible_ids() - } - fn get_vault(&self, resource: ResourceAddress) -> &Vault { self.vaults .get(&resource) @@ -189,17 +179,20 @@ mod account_template { // Fee methods. These are used to pay fees and satisfy a "duck-typed" interface. - /// Pay fees from previously revealed confidential resource. + /// Pay fees from previously revealed stealth resource. pub fn pay_fee(&mut self, amount: Amount) { emit_event("pay_fee", [("amount", amount.to_string())]); - self.get_vault_mut(CONFIDENTIAL_TARI_RESOURCE_ADDRESS).pay_fee(amount); + self.get_vault_mut(STEALTH_TARI_RESOURCE_ADDRESS).pay_fee(amount); } - /// Reveal confidential tokens and return the revealed bucket to pay fees. - pub fn pay_fee_confidential(&mut self, proof: ConfidentialWithdrawProof) { - emit_event("pay_fee_confidential", [("num_inputs", proof.inputs.len().to_string())]); - self.get_vault_mut(CONFIDENTIAL_TARI_RESOURCE_ADDRESS) - .pay_fee_confidential(proof); + /// Reveal stealth tokens and return the revealed bucket to pay fees. + pub fn pay_fee_stealth(&mut self, transfer: StealthTransferStatement) { + emit_event("pay_fee", [ + ("stealth", "true".to_string()), + ("num_inputs", transfer.inputs_statement.inputs.len().to_string()), + ]); + self.get_vault_mut(STEALTH_TARI_RESOURCE_ADDRESS) + .pay_fee_stealth(transfer); } pub fn create_proof_for_resource(&mut self, resource: ResourceAddress) -> Proof { @@ -229,11 +222,5 @@ mod account_template { let v = self.get_vault_mut(resource); v.create_proof_by_amount(amount) } - - /// Utility function to allow bucket NFT content to be inspected. An empty vec is returned if the bucket does - /// not contain any NFTs or does not contain a NonFungible resource. - pub fn get_non_fungible_ids_for_bucket(bucket: Bucket) -> Vec { - bucket.get_non_fungible_ids() - } } } diff --git a/crates/template_builtin/templates/faucet/src/lib.rs b/crates/template_builtin/templates/faucet/src/lib.rs index 2378275327..86750972fc 100644 --- a/crates/template_builtin/templates/faucet/src/lib.rs +++ b/crates/template_builtin/templates/faucet/src/lib.rs @@ -5,11 +5,11 @@ use tari_template_lib::prelude::*; #[template] mod template { - use super::*; pub struct XtrFaucet { vault: Vault, + resource_manager: ResourceManager, } impl XtrFaucet { @@ -23,11 +23,16 @@ mod template { pub fn take_confidential( &self, amount: Amount, - output: ConfidentialOutputStatement, + output: StealthOutputsStatement, balance_proof: BalanceProofSignature, - ) -> Bucket { - // Withdraws revealed funds into the given confidential output - let proof = ConfidentialWithdrawProof::revealed_to_confidential(amount, output, balance_proof); + ) -> Option { + let revealed_bucket = self.vault.withdraw(amount); + let transfer = StealthTransferStatement { + inputs_statement: StealthInputsStatement::new_revealed(amount), + outputs_statement: output, + balance_proof, + }; + debug!("Withdrawing {} coins from faucet into confidential output", amount); let signer = CallerContext::transaction_signer_public_key(); emit_event("take", [ @@ -35,7 +40,9 @@ mod template { ("confidential", "true".to_string()), ("signer", signer.to_string()), ]); - self.vault.withdraw_confidential(proof) + + self.resource_manager + .stealth_transfer_with_opt_input_bucket(transfer, Some(revealed_bucket)) } } } diff --git a/crates/template_builtin/tests/nft_faucet.rs b/crates/template_builtin/tests/nft_faucet.rs index 2f2c9faaea..8c80526a9e 100644 --- a/crates/template_builtin/tests/nft_faucet.rs +++ b/crates/template_builtin/tests/nft_faucet.rs @@ -3,8 +3,8 @@ use tari_engine_types::commit_result::ExecuteResult; use tari_template_lib::{ - constants::NFT_FAUCET_COMPONENT_ADDRESS, - models::{ComponentAddress, NonFungibleAddress, NonFungibleId}, + constants::{NFT_FAUCET_COMPONENT_ADDRESS, NFT_FAUCET_RESOURCE_ADDRESS}, + models::{ComponentAddress, NonFungibleAddress}, prelude::Metadata, resource::TOKEN_SYMBOL, }; @@ -25,12 +25,16 @@ fn basic_nft_mint() { metadata.insert("name", "my_custom_nft"); metadata.insert("brightness", "100"); - let result = mint_faucet_nft(&mut test, owner_component_address, owner_token.clone(), metadata); - assert!(result.finalize.result.is_accept()); + mint_faucet_nft(&mut test, owner_component_address, owner_token.clone(), metadata).expect_success(); - let bucket_nfts = result.finalize.execution_results[2] - .decode::>() + let vault = test + .read_only_state_store() + .get_vaults_for_account(owner_component_address) + .unwrap() + .get(&NFT_FAUCET_RESOURCE_ADDRESS) + .cloned() .unwrap(); + let bucket_nfts = vault.get_non_fungible_ids(); assert_eq!(bucket_nfts.len(), 1); } @@ -71,11 +75,6 @@ fn mint_faucet_nft( Transaction::builder() .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![Amount(1), metadata]) .put_last_instruction_output_on_workspace("my_nft") - .call_function( - test.get_template_address("Account"), - "get_non_fungible_ids_for_bucket", - args![Workspace("my_nft")], - ) .call_method(account, "deposit", args![Workspace("my_nft")]), vec![owner_token], ) diff --git a/crates/template_lib/src/args/types.rs b/crates/template_lib/src/args/types.rs index 6cdd3eff91..d22b3b8793 100644 --- a/crates/template_lib/src/args/types.rs +++ b/crates/template_lib/src/args/types.rs @@ -418,7 +418,7 @@ pub enum VaultWithdrawArg { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PayFeeArg { pub amount: Amount, - pub proof: Option, + pub statement: Option, } // -------------------------------- Bucket -------------------------------- // diff --git a/crates/template_lib/src/constants.rs b/crates/template_lib/src/constants.rs index f95271e221..5abaa1a636 100644 --- a/crates/template_lib/src/constants.rs +++ b/crates/template_lib/src/constants.rs @@ -6,7 +6,6 @@ use tari_template_lib_types::ObjectKey; use crate::models::{ComponentAddress, ResourceAddress, VaultId}; - // TODO: This is set pretty arbitrarily. /// Resource address for all public identity-based non-fungible tokens. @@ -18,11 +17,11 @@ pub const PUBLIC_IDENTITY_RESOURCE_ADDRESS: ResourceAddress = ResourceAddress::n /// The Tari network native resource address, used for paying network fees /// resource_0101010101010101010101010101010101010101010101010101010101010101 -pub const CONFIDENTIAL_TARI_RESOURCE_ADDRESS: ResourceAddress = +pub const STEALTH_TARI_RESOURCE_ADDRESS: ResourceAddress = ResourceAddress::new(ObjectKey::from_array([1u8; ObjectKey::LENGTH])); -/// Shorthand version of the `CONFIDENTIAL_TARI_RESOURCE_ADDRESS` constant -pub const XTR: ResourceAddress = CONFIDENTIAL_TARI_RESOURCE_ADDRESS; +/// Shorthand version of the `STEALTH_TARI_RESOURCE_ADDRESS` constant +pub const XTR: ResourceAddress = STEALTH_TARI_RESOURCE_ADDRESS; /// Address of testnet faucet component pub const XTR_FAUCET_COMPONENT_ADDRESS: ComponentAddress = ComponentAddress::new(ObjectKey::from_array([ diff --git a/crates/template_lib/src/models/resource.rs b/crates/template_lib/src/models/resource.rs index a8ab6f45f7..7490ea3424 100644 --- a/crates/template_lib/src/models/resource.rs +++ b/crates/template_lib/src/models/resource.rs @@ -29,7 +29,7 @@ use tari_template_abi::rust::{ use tari_template_lib_types::{EntityId, KeyParseError, ObjectKey}; use super::BinaryTag; -use crate::{newtype_struct_serde_impl, prelude::CONFIDENTIAL_TARI_RESOURCE_ADDRESS}; +use crate::{newtype_struct_serde_impl, prelude::STEALTH_TARI_RESOURCE_ADDRESS}; const TAG: u64 = BinaryTag::ResourceAddress.as_u64(); @@ -61,7 +61,7 @@ impl ResourceAddress { } pub fn is_tari(&self) -> bool { - *self == CONFIDENTIAL_TARI_RESOURCE_ADDRESS + *self == STEALTH_TARI_RESOURCE_ADDRESS } } diff --git a/crates/template_lib/src/models/stealth.rs b/crates/template_lib/src/models/stealth.rs index 6276d34f28..e6ec418386 100644 --- a/crates/template_lib/src/models/stealth.rs +++ b/crates/template_lib/src/models/stealth.rs @@ -45,6 +45,24 @@ pub struct StealthInputsStatement { pub revealed_amount: Amount, } +impl StealthInputsStatement { + pub fn new(inputs: Vec, revealed_amount: Amount) -> Self { + assert!(!revealed_amount.is_negative(), "Revealed amount must be positive"); + assert!( + !inputs.is_empty() || !revealed_amount.is_zero(), + "At least one input or a revealed amount must be provided" + ); + Self { + inputs, + revealed_amount, + } + } + + pub fn new_revealed(amount: Amount) -> Self { + Self::new(vec![], amount) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub struct StealthTransferStatement { diff --git a/crates/template_lib/src/models/vault.rs b/crates/template_lib/src/models/vault.rs index e279cc3514..27af998085 100644 --- a/crates/template_lib/src/models/vault.rs +++ b/crates/template_lib/src/models/vault.rs @@ -43,6 +43,7 @@ use super::{ Proof, ProofAuth, ResourceAddress, + StealthTransferStatement, }; use crate::{ args::{ @@ -377,7 +378,7 @@ impl Vault { ResourceManager::get(self.resource_address()).resource_type() } - /// Pay a transaction fee with the funds present in the vault. + /// Pay a transaction fee with revealed funds present in the vault. /// Note that the vault must hold native Tari tokens to perform this operation. pub fn pay_fee>(&self, amount: A) { let _resp: InvokeResult = call_engine(EngineOp::VaultInvoke, &VaultInvokeArg { @@ -385,21 +386,22 @@ impl Vault { action: VaultAction::PayFee, args: invoke_args![PayFeeArg { amount: amount.into(), - proof: None + statement: None }], }); } - /// Pay a transaction fee with the confidential funds present in the vault. - /// Note that the vault must hold native Tari tokens to perform this operation - /// The withdraw proof must result in a non-zero amount of revealed funds. - pub fn pay_fee_confidential(&self, proof: ConfidentialWithdrawProof) { + /// Pay a transaction fee with stealth funds. + /// Note that the vault must hold native XTR tokens to perform this operation + /// The transfer statement must result in a positive amount of revealed funds (from this vault and/or from consumed + /// utxos). + pub fn pay_fee_stealth(&self, transfer: StealthTransferStatement) { let _resp: InvokeResult = call_engine(EngineOp::VaultInvoke, &VaultInvokeArg { vault_ref: self.vault_ref(), action: VaultAction::PayFee, args: invoke_args![PayFeeArg { amount: Amount::zero(), - proof: Some(proof) + statement: Some(transfer) }], }); } diff --git a/crates/template_lib/src/prelude.rs b/crates/template_lib/src/prelude.rs index ce81887fdd..24fdc0d737 100644 --- a/crates/template_lib/src/prelude.rs +++ b/crates/template_lib/src/prelude.rs @@ -46,7 +46,7 @@ pub use crate::{ caller_context::CallerContext, component::{Component, ComponentManager}, consensus::Consensus, - constants::{CONFIDENTIAL_TARI_RESOURCE_ADDRESS, PUBLIC_IDENTITY_RESOURCE_ADDRESS, XTR}, + constants::{PUBLIC_IDENTITY_RESOURCE_ADDRESS, STEALTH_TARI_RESOURCE_ADDRESS, XTR}, debug, error, events::emit_event, @@ -69,6 +69,7 @@ pub use crate::{ ProofId, ResourceAddress, ResourceAddressAllocation, + StealthInputsStatement, StealthOutputsStatement, StealthTransferStatement, Vault, diff --git a/crates/template_lib_types/src/amount/amount.rs b/crates/template_lib_types/src/amount/amount.rs index b933ad23ab..751db8d0b3 100644 --- a/crates/template_lib_types/src/amount/amount.rs +++ b/crates/template_lib_types/src/amount/amount.rs @@ -52,10 +52,6 @@ impl Amount { Self(amount) } - pub fn from_u64(amount: u64) -> Self { - Self::new(I192::from(amount)) - } - /// A value of zero. pub const fn zero() -> Self { Self::ZERO diff --git a/crates/template_test_tooling/src/builtin_component_state.rs b/crates/template_test_tooling/src/builtin_component_state.rs index c07e8992d6..dbfc2a2e12 100644 --- a/crates/template_test_tooling/src/builtin_component_state.rs +++ b/crates/template_test_tooling/src/builtin_component_state.rs @@ -15,7 +15,7 @@ use tari_engine_types::{ use tari_template_builtin::NFT_FAUCET_TEMPLATE_ADDRESS; use tari_template_lib::{ auth::{ComponentAccessRules, OwnerRule, ResourceAccessRules}, - constants::{CONFIDENTIAL_TARI_RESOURCE_ADDRESS, NFT_FAUCET_COMPONENT_ADDRESS, NFT_FAUCET_RESOURCE_ADDRESS}, + constants::{NFT_FAUCET_COMPONENT_ADDRESS, NFT_FAUCET_RESOURCE_ADDRESS, STEALTH_TARI_RESOURCE_ADDRESS}, prelude::{ResourceType, RistrettoPublicKeyBytes, TemplateAddress}, rule, types::{Amount, EntityId, Hash}, @@ -33,9 +33,8 @@ pub fn initialize_builtin_faucet_state( 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::confidential( - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - vec![], + let vault = Vault::new(ResourceContainer::stealth( + STEALTH_TARI_RESOURCE_ADDRESS, initial_supply, )); store diff --git a/crates/template_test_tooling/src/template_test.rs b/crates/template_test_tooling/src/template_test.rs index c992d08c24..82b959c21b 100644 --- a/crates/template_test_tooling/src/template_test.rs +++ b/crates/template_test_tooling/src/template_test.rs @@ -38,7 +38,7 @@ use tari_ootle_common_types::{ Network, SubstateRequirement, }; -use tari_template_builtin::{ACCOUNT_TEMPLATE_ADDRESS, NFT_FAUCET_TEMPLATE_ADDRESS}; +use tari_template_builtin::{ACCOUNT_TEMPLATE_ADDRESS, NFT_FAUCET_TEMPLATE_ADDRESS, XTR_FAUCET_TEMPLATE_ADDRESS}; use tari_template_lib::{ args::InstructionArg, constants::{NFT_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS}, @@ -57,7 +57,7 @@ use crate::{ Package, }; -pub fn test_faucet_component() -> ComponentAddress { +pub const fn test_faucet_component() -> ComponentAddress { XTR_FAUCET_COMPONENT_ADDRESS } @@ -110,6 +110,7 @@ impl TemplateTest { // 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); // Add the faucet template for fungible tokens builder.add_template(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/faucet")); @@ -559,7 +560,7 @@ impl TemplateTest { proofs: Vec, ) -> ExecuteResult { let result = self.try_execute(transaction, proofs).unwrap(); - if let Some(diff) = result.finalize.result.accept() { + if let Some(diff) = result.finalize.result.any_accept() { self.commit_diff(diff); } @@ -568,6 +569,7 @@ impl TemplateTest { /// Executes a transaction. Panics if the transaction is not finalized (fee transaction fails). Does not panic if /// the main instructions fails (use execute_expect_success for that). + #[track_caller] pub fn execute_expect_commit( &mut self, transaction: Transaction, @@ -587,6 +589,7 @@ impl TemplateTest { } /// Executes a transaction. Panics if the transaction fails. + #[track_caller] pub fn execute_expect_success( &mut self, transaction: Transaction, @@ -598,6 +601,7 @@ impl TemplateTest { } /// Executes a transaction. Panics if the transaction succeeds. + #[track_caller] pub fn execute_expect_failure( &mut self, transaction: Transaction, @@ -622,7 +626,7 @@ impl TemplateTest { proofs: Vec, ) -> anyhow::Result { let result = self.try_execute_instructions(fee_instructions, instructions, proofs)?; - let diff = result.finalize.result.accept().ok_or_else(|| { + let diff = result.finalize.result.any_accept().ok_or_else(|| { anyhow!( "Transaction was rejected: {}", result.finalize.result.fee_reject().unwrap() diff --git a/crates/template_test_tooling/templates/faucet/src/lib.rs b/crates/template_test_tooling/templates/faucet/src/lib.rs index 64fefc7594..9342dbc4c5 100644 --- a/crates/template_test_tooling/templates/faucet/src/lib.rs +++ b/crates/template_test_tooling/templates/faucet/src/lib.rs @@ -57,11 +57,6 @@ mod faucet_template { self.vault.withdraw(amount) } - pub fn take_free_coins_confidential(&mut self, proof: ConfidentialWithdrawProof) -> Bucket { - debug!("Withdrawing coins from faucet"); - self.vault.withdraw_confidential(proof) - } - pub fn burn_coins(&mut self, amount: Amount) { let mut bucket = self.vault.withdraw(amount); bucket.burn(); @@ -70,15 +65,5 @@ mod faucet_template { pub fn total_supply(&self) -> Amount { ResourceManager::get(self.vault.resource_address()).total_supply() } - - pub fn pay_fee(&mut self, amount: Amount) { - debug!("Paying fee from faucet"); - self.vault.pay_fee(amount); - } - - pub fn pay_fee_confidential(&mut self, proof: ConfidentialWithdrawProof) { - debug!("Paying fee from faucet"); - self.vault.pay_fee_confidential(proof); - } } } diff --git a/crates/transaction/src/builder/mod.rs b/crates/transaction/src/builder/mod.rs index ee94996ad7..327e16a27a 100644 --- a/crates/transaction/src/builder/mod.rs +++ b/crates/transaction/src/builder/mod.rs @@ -11,7 +11,7 @@ mod workspace_ids; pub use named_component_call::*; use tari_common_types::types::PrivateKey; use tari_engine_types::{ - confidential::ConfidentialClaim, + confidential::TariStealthClaim, instruction::Instruction, substate::SubstateId, ComponentCall, @@ -23,7 +23,7 @@ use tari_template_lib::{ args::{AllocatableAddressType, InstructionArg, WorkspaceOffsetId}, auth::OwnerRule, call_args, - models::{ConfidentialWithdrawProof, ResourceAddress, StealthTransferStatement}, + models::{ResourceAddress, StealthTransferStatement}, prelude::AccessRules, types::{crypto::RistrettoPublicKeyBytes, Amount, TemplateAddress}, }; @@ -84,6 +84,12 @@ impl TransactionBuilder { self } + /// Pays fees using a stealth transfer statement. The statement must reveal sufficient funds to cover the fee. + /// NOTE: fees paid are not refunded, so any overpayment is kept by validators. + pub fn fee_transaction_pay_fees_stealth(self, statement: StealthTransferStatement) -> Self { + self.with_fee_instructions_builder(|builder| builder.pay_fee_stealth(statement)) + } + /// Adds a fee instruction that calls the "take_fee" method on a component. /// This method must exist and return a Bucket with containing revealed confidential XTR resource. /// This allows the fee to originate from sources other than the transaction sender's account. @@ -100,18 +106,18 @@ impl TransactionBuilder { }) } - /// Adds a fee instruction that calls the "take_fee_confidential" method on a component. - /// This method must exist and return a Bucket with containing revealed confidential XTR resource. - /// This allows the fee to originate from sources other than the transaction sender's account. - pub fn fee_transaction_pay_from_component_confidential>( + /// Adds a fee instruction that calls the "pay_fee_stealth" method on a component. + /// This method should call either `Vault::pay_fee_stealth` or `ResourceManager::pay_fee_stealth` and result in a + /// sufficient amount of revealed funds used to pay fees. + pub fn fee_transaction_pay_fees_stealth_from_component>( self, call: A, - proof: ConfidentialWithdrawProof, + statement: StealthTransferStatement, ) -> Self { self.add_fee_instruction(Instruction::CallMethod { call: call.into(), - method: "pay_fee_confidential".to_string(), - args: call_args![proof], + method: "pay_fee_stealth".to_string(), + args: call_args![statement], }) } @@ -211,6 +217,30 @@ impl TransactionBuilder { }) } + pub fn pay_fee_stealth(self, statement: StealthTransferStatement) -> Self { + self.pay_fee_stealth_with_opt_input_bucket(statement, None::) + } + + pub fn pay_fee_stealth_with_input_bucket>( + self, + statement: StealthTransferStatement, + input_bucket: B, + ) -> Self { + self.pay_fee_stealth_with_opt_input_bucket(statement, Some(input_bucket)) + } + + pub fn pay_fee_stealth_with_opt_input_bucket>( + self, + statement: StealthTransferStatement, + input_bucket: Option, + ) -> Self { + let revealed_input_bucket = input_bucket.map(|bucket| self.get_workspace_offset_id_from_named_arg(bucket)); + self.add_instruction(Instruction::PayFee { + statement, + revealed_input_bucket, + }) + } + pub fn drop_all_proofs_in_workspace(self) -> Self { self.add_instruction(Instruction::DropAllProofsInWorkspace) } @@ -239,7 +269,7 @@ impl TransactionBuilder { self.add_instruction(Instruction::PublishTemplate { binary }) } - pub fn claim_burn(self, claim: ConfidentialClaim) -> Self { + pub fn claim_burn(self, claim: TariStealthClaim) -> Self { self.add_instruction(Instruction::ClaimBurn { claim: Box::new(claim) }) } @@ -264,6 +294,7 @@ impl TransactionBuilder { } pub fn with_fee_instructions_builder TransactionBuilder>(mut self, f: F) -> Self { + // TODO: pass in a fee builder type (probably TransactionBuilder which has applicable methods) let builder = f(TransactionBuilder::new()); self.unsigned_transaction .fee_instructions_mut() @@ -372,11 +403,7 @@ impl TransactionBuilder { } pub fn build(self) -> UnsealedTransactionV1 { - self.unsigned_transaction.build(self.signatures) - } - - pub fn build_and_seal(self, secret_key: &PrivateKey) -> Transaction { - self.then(|builder| { + let builder = self.then(|builder| { // This is so that we dont have to add this in a lot of places - TODO: this is an assumption that may not // apply to all transactions if builder.signatures.is_empty() { @@ -384,9 +411,13 @@ impl TransactionBuilder { } else { builder } - }) - .build() - .seal(secret_key) + }); + + builder.unsigned_transaction.build(builder.signatures) + } + + pub fn build_and_seal(self, secret_key: &PrivateKey) -> Transaction { + self.build().seal(secret_key) } fn resolve_call(&self, call: NamedComponentCall) -> ComponentCall { diff --git a/crates/transaction/src/transaction.rs b/crates/transaction/src/transaction.rs index 69efec259d..ed03345fed 100644 --- a/crates/transaction/src/transaction.rs +++ b/crates/transaction/src/transaction.rs @@ -13,7 +13,7 @@ use tari_engine_types::{ substate::SubstateId, }; use tari_ootle_common_types::{committee::CommitteeInfo, Epoch, SubstateRequirement, SubstateRequirementRef}; -use tari_template_lib::models::ComponentAddress; +use tari_template_lib::{models::ComponentAddress, prelude::TemplateAddress}; use crate::{ builder::TransactionBuilder, @@ -195,6 +195,19 @@ impl Transaction { } } + pub fn referenced_templates_iter(&self) -> impl Iterator + '_ { + self.instructions() + .iter() + .chain(self.fee_instructions()) + .filter_map(|instruction| { + if let Instruction::CallFunction { address, .. } = instruction { + Some(address) + } else { + None + } + }) + } + /// Returns all substates addresses referenced by this transaction pub fn to_referenced_substates(&self) -> Result, IndexedValueError> { match self { diff --git a/crates/transaction/src/v1/signature.rs b/crates/transaction/src/v1/signature.rs index 2b725f4c58..c2d872cd78 100644 --- a/crates/transaction/src/v1/signature.rs +++ b/crates/transaction/src/v1/signature.rs @@ -141,6 +141,7 @@ struct TransactionSignatureFields<'a> { min_epoch: Option, max_epoch: Option, is_seal_signer_authorized: bool, + dry_run: bool, } impl<'a> From<&'a UnsignedTransactionV1> for TransactionSignatureFields<'a> { @@ -153,6 +154,7 @@ impl<'a> From<&'a UnsignedTransactionV1> for TransactionSignatureFields<'a> { min_epoch: transaction.min_epoch, max_epoch: transaction.max_epoch, is_seal_signer_authorized: transaction.is_seal_signer_authorized, + dry_run: transaction.dry_run, } } } diff --git a/crates/transaction/src/v1/transaction.rs b/crates/transaction/src/v1/transaction.rs index 9b1d987c73..c475f9aebb 100644 --- a/crates/transaction/src/v1/transaction.rs +++ b/crates/transaction/src/v1/transaction.rs @@ -13,15 +13,11 @@ use tari_engine_types::{ published_template::PublishedTemplateAddress, substate::SubstateId, }; -use tari_ootle_common_types::{ - Epoch, - NumPreshards, - ShardGroup, - SubstateAddress, - SubstateRequirement, - SubstateRequirementRef, +use tari_ootle_common_types::{Epoch, SubstateRequirement, SubstateRequirementRef}; +use tari_template_lib::{ + args::InstructionArg, + models::{ComponentAddress, StealthTransferStatement}, }; -use tari_template_lib::{args::InstructionArg, models::ComponentAddress}; use crate::{ v1::signature::TransactionSignature, @@ -120,16 +116,6 @@ impl TransactionV1 { self.inputs().iter().map(Into::into) } - pub fn to_all_involved_shards(&self, num_shards: NumPreshards, num_committees: u32) -> HashSet { - self.all_inputs_iter() - .map(|id| { - // version doesnt affect shard - let addr = SubstateAddress::from_substate_id(id.substate_id(), 0); - addr.to_shard_group(num_shards, num_committees) - }) - .collect() - } - pub fn all_published_templates_iter(&self) -> impl Iterator + '_ { let sealed_pk = self.seal_signature.public_key(); self.instructions() @@ -206,13 +192,16 @@ fn calc_instruction_weight(instruction: &Instruction) -> u64 { Instruction::AssertBucketContains { .. } => 1, Instruction::PublishTemplate { binary } => binary.len() as u64 / BINARY_WEIGHT_DIVISOR, Instruction::AllocateAddress { .. } => 1, - Instruction::StealthTransfer { statement, .. } => { - // TODO: weight inputs and outputs accordingly - currently outputs cost 2x inputs - statement.inputs_statement.inputs.len() as u64 + (statement.outputs_statement.outputs.len() as u64 * 2) - }, + Instruction::StealthTransfer { statement, .. } => calc_stealth_statement_weight(statement), + Instruction::PayFee { statement, .. } => calc_stealth_statement_weight(statement), } } +fn calc_stealth_statement_weight(statement: &StealthTransferStatement) -> u64 { + // TODO: weight inputs and outputs accordingly - currently outputs cost 2x inputs + statement.inputs_statement.inputs.len() as u64 + (statement.outputs_statement.outputs.len() as u64 * 2) +} + fn calc_args_weight(args: &[InstructionArg]) -> u64 { const FROM_WORKSPACE_WEIGHT: u64 = 1; // Default weight for args that are not literal bytes args.iter() diff --git a/crates/wallet/crypto/src/bullet_proof.rs b/crates/wallet/crypto/src/bullet_proof.rs index cec717635c..a1eb43cf6d 100644 --- a/crates/wallet/crypto/src/bullet_proof.rs +++ b/crates/wallet/crypto/src/bullet_proof.rs @@ -56,6 +56,8 @@ pub fn generate_extended_bullet_proof<'a, I: IntoIterator( + inputs: Inputs, revealed_input_amount: Amount, - output_statements: &[UnblindedStealthOutputStatement], + output_statements: Outputs, revealed_output_amount: Amount, -) -> Result { +) -> Result +where + Inputs: IntoIterator, + Outputs: IntoIterator + Clone, +{ if revealed_input_amount.is_negative() { return Err(WalletCryptoError::InvalidArgument { name: "revealed_input_amount", @@ -47,8 +51,13 @@ pub fn create_transfer_statement( }); } - let (inputs_to_spend, agg_input_mask) = inputs.iter().try_fold( - (Vec::with_capacity(inputs.len()), RistrettoSecretKey::default()), + let mut input_iter = inputs.into_iter(); + let num_inputs_estimate = { + let (lower, upper) = input_iter.size_hint(); + upper.unwrap_or(lower) + }; + let (inputs_to_spend, agg_input_mask) = input_iter.try_fold( + (Vec::with_capacity(num_inputs_estimate), RistrettoSecretKey::default()), |(mut inputs, agg_input), input| { let commitment = input @@ -75,7 +84,8 @@ pub fn create_transfer_statement( )?; let agg_output_mask = output_statements - .iter() + .clone() + .into_iter() .map(|stmt| &stmt.statement.mask) .fold(RistrettoSecretKey::default(), |agg, mask| agg + mask); @@ -98,12 +108,13 @@ pub fn create_transfer_statement( }) } -pub fn create_outputs_statement( - output_statements: &[UnblindedStealthOutputStatement], +pub fn create_outputs_statement<'a, Outputs: IntoIterator + Clone>( + output_statements: Outputs, revealed_output_amount: Amount, ) -> Result { let outputs = output_statements - .iter() + .clone() + .into_iter() .map(|output_stmt| { let unblinded_stmt = &output_stmt.statement; let commitment = output_stmt @@ -133,7 +144,7 @@ pub fn create_outputs_statement( }) .collect::, _>>()?; - let output_range_proof = generate_extended_bullet_proof(output_statements.iter().map(|o| &o.statement))?; + let output_range_proof = generate_extended_bullet_proof(output_statements.into_iter().map(|o| &o.statement))?; Ok(StealthOutputsStatement { outputs, diff --git a/crates/wallet/sdk/src/apis/confidential_outputs.rs b/crates/wallet/sdk/src/apis/confidential_outputs.rs index 0843ece01c..109deef96f 100644 --- a/crates/wallet/sdk/src/apis/confidential_outputs.rs +++ b/crates/wallet/sdk/src/apis/confidential_outputs.rs @@ -16,7 +16,7 @@ use crate::{ confidential_crypto::{ConfidentialCryptoApi, ConfidentialCryptoApiError}, key_manager::{KeyManagerApi, KeyManagerApiError}, }, - models::{Account, ConfidentialOutputModel, OutputLockId, OutputStatus}, + models::{Account, ConfidentialOutputModel, OutputStatus, WalletLockId}, storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; @@ -45,7 +45,7 @@ where TStore: WalletStore pub fn lock_outputs_by_amount>( &self, - lock_id: OutputLockId, + lock_id: WalletLockId, vault_id: &VaultId, amount: A, ) -> Result<(Vec, Amount), ConfidentialOutputsApiError> { @@ -58,7 +58,7 @@ where TStore: WalletStore reason: "Amount must be non-negative".to_string(), })?; self.store.with_write_tx(|tx| { - let (outputs, total_output_amount) = self.lock_outputs_internal(tx, vault_id, amount, lock_id)?; + let (outputs, total_output_amount) = self.lock_outputs_internal(tx, lock_id, vault_id, amount)?; if total_output_amount < amount { return Err(ConfidentialOutputsApiError::InsufficientFunds); @@ -70,20 +70,20 @@ where TStore: WalletStore pub fn lock_outputs_until_partial_amount( &self, + locked_id: WalletLockId, vault_id: &VaultId, amount: Amount, - locked_by_proof_id: OutputLockId, ) -> Result<(Vec, Amount), ConfidentialOutputsApiError> { self.store - .with_write_tx(|tx| self.lock_outputs_internal(tx, vault_id, amount, locked_by_proof_id)) + .with_write_tx(|tx| self.lock_outputs_internal(tx, locked_id, vault_id, amount)) } fn lock_outputs_internal( &self, tx: &mut TTx, + lock_id: WalletLockId, vault_id: &VaultId, amount: Amount, - locked_by_proof_id: OutputLockId, ) -> Result<(Vec, Amount), ConfidentialOutputsApiError> { if amount.is_negative() { return Err(ConfidentialOutputsApiError::InvalidParameter { @@ -94,9 +94,7 @@ where TStore: WalletStore let mut total_output_amount = Amount::zero(); let mut outputs = Vec::new(); while total_output_amount < amount { - let output = tx - .outputs_lock_smallest_amount(vault_id, locked_by_proof_id) - .optional()?; + let output = tx.outputs_lock_smallest_amount(vault_id, lock_id).optional()?; match output { Some(output) => { total_output_amount += output.value; @@ -118,25 +116,34 @@ where TStore: WalletStore Ok(()) } - pub fn add_output_lock(&self, vault_id: &VaultId) -> Result { - let mut tx = self.store.create_write_tx()?; - let lock_id = tx.output_locks_insert_for_vault(vault_id)?; - tx.commit()?; + pub fn create_lock(&self) -> Result { + let lock_id = self.store.with_write_tx(|tx| tx.locks_create())?; Ok(lock_id) } - pub fn release_locked_outputs(&self, lock_id: OutputLockId) -> Result<(), ConfidentialOutputsApiError> { + pub fn lock_vault_revealed_funds( + &self, + lock_id: WalletLockId, + vault_id: &VaultId, + amount: Amount, + ) -> Result<(), ConfidentialOutputsApiError> { + self.store + .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount))?; + Ok(()) + } + + pub fn release_locked_outputs(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { let mut tx = self.store.create_write_tx()?; - tx.output_locks_delete(lock_id)?; tx.outputs_release_by_lock_id(lock_id)?; + tx.locks_delete(lock_id)?; tx.commit()?; Ok(()) } - pub fn finalize_outputs_for_lock(&self, lock_id: OutputLockId) -> Result<(), ConfidentialOutputsApiError> { + pub fn finalize_outputs_for_lock(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { let mut tx = self.store.create_write_tx()?; - tx.output_locks_delete(lock_id)?; tx.outputs_finalize_by_lock_id(lock_id)?; + tx.locks_delete(lock_id)?; tx.commit()?; Ok(()) } @@ -187,19 +194,7 @@ where TStore: WalletStore Ok(outputs_with_masks) } - pub fn lock_revealed_funds( - &self, - lock_id: OutputLockId, - amount_to_lock: Amount, - ) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.vaults_lock_revealed_funds(lock_id, amount_to_lock)?; - tx.commit()?; - - Ok(()) - } - - pub fn finalize_locked_revealed_funds(&self, lock_id: OutputLockId) -> Result<(), ConfidentialOutputsApiError> { + pub fn finalize_locked_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { let mut tx = self.store.create_write_tx()?; tx.vaults_finalized_locked_revealed_funds(lock_id)?; tx.commit()?; @@ -207,9 +202,9 @@ where TStore: WalletStore Ok(()) } - pub fn release_revealed_funds(&self, lock_id: OutputLockId) -> Result<(), ConfidentialOutputsApiError> { + pub fn release_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), ConfidentialOutputsApiError> { let mut tx = self.store.create_write_tx()?; - tx.vaults_unlock_revealed_funds(lock_id)?; + tx.vaults_release_lock_revealed_funds(lock_id)?; tx.commit()?; Ok(()) @@ -324,14 +319,13 @@ where TStore: WalletStore }) } - pub fn locks_set_transaction_hash( + pub fn locks_set_transaction_id( &self, - lock_id: OutputLockId, + lock_id: WalletLockId, transaction_id: TransactionId, ) -> Result<(), ConfidentialOutputsApiError> { - let mut tx = self.store.create_write_tx()?; - tx.output_locks_set_params(lock_id, Some(transaction_id), None)?; - tx.commit()?; + self.store + .with_write_tx(|tx| tx.locks_link_transaction(lock_id, transaction_id))?; Ok(()) } } diff --git a/crates/wallet/sdk/src/apis/confidential_transfer.rs b/crates/wallet/sdk/src/apis/confidential_transfer.rs index fda0abf4de..49c0f6f41c 100644 --- a/crates/wallet/sdk/src/apis/confidential_transfer.rs +++ b/crates/wallet/sdk/src/apis/confidential_transfer.rs @@ -11,7 +11,6 @@ use tari_engine_types::{FromByteType, ToByteType}; use tari_ootle_common_types::{optional::IsNotFoundError, SubstateRequirement}; use tari_ootle_wallet_crypto::{MaskAndValue, UnblindedOutputStatement}; use tari_template_lib::{ - constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, models::{ComponentAddress, ResourceAddress, VaultId}, prelude::RistrettoPublicKeyBytes, types::Amount, @@ -27,7 +26,7 @@ use crate::{ key_manager::{KeyBranch, KeyManagerApi, KeyManagerApiError}, substate::{SubstateApiError, SubstatesApi}, }, - models::{ConfidentialOutputModel, OutputLockId, OutputStatus}, + models::{ConfidentialOutputModel, OutputStatus, WalletLockId}, network::WalletNetworkInterface, storage::{WalletStorageError, WalletStore}, }; @@ -70,6 +69,7 @@ where #[allow(clippy::too_many_lines)] fn resolved_inputs_for_transfer( &self, + lock_id: WalletLockId, from_account: ComponentAddress, resource_address: ResourceAddress, spend_amount: Amount, @@ -81,8 +81,6 @@ where let available_revealed_funds = src_vault.available_revealed_balance(); - let lock_id = self.outputs_api.add_output_lock(&src_vault.id)?; - match &input_selection { ConfidentialTransferInputSelection::ConfidentialOnly => { let (confidential_inputs, _) = @@ -108,7 +106,8 @@ where return Err(ConfidentialTransferApiError::InsufficientFunds); } - self.outputs_api.lock_revealed_funds(lock_id, spend_amount)?; + self.outputs_api + .lock_vault_revealed_funds(lock_id, &src_vault.id, spend_amount)?; info!( target: LOG_TARGET, @@ -134,7 +133,8 @@ where src_vault.id, ); - self.outputs_api.lock_revealed_funds(lock_id, revealed_to_spend)?; + self.outputs_api + .lock_vault_revealed_funds(lock_id, &src_vault.id, revealed_to_spend)?; return Ok(InputsToSpend { confidential: vec![], @@ -150,7 +150,8 @@ where let total_confidential_spent = confidential_inputs.iter().map(|i| i.value).sum::(); - self.outputs_api.lock_revealed_funds(lock_id, revealed_to_spend)?; + self.outputs_api + .lock_vault_revealed_funds(lock_id, &src_vault.id, revealed_to_spend)?; info!( target: LOG_TARGET, @@ -170,10 +171,9 @@ where }) }, ConfidentialTransferInputSelection::PreferConfidential => { - let lock_id = self.outputs_api.add_output_lock(&src_vault.id)?; let (confidential_inputs, amount_locked) = self.outputs_api - .lock_outputs_until_partial_amount(&src_vault.id, spend_amount, lock_id)?; + .lock_outputs_until_partial_amount(lock_id, &src_vault.id, spend_amount)?; let revealed_to_spend = spend_amount .saturating_sub_positive(amount_locked) @@ -183,7 +183,8 @@ where return Err(ConfidentialTransferApiError::InsufficientFunds); } - self.outputs_api.lock_revealed_funds(lock_id, revealed_to_spend)?; + self.outputs_api + .lock_vault_revealed_funds(lock_id, &src_vault.id, revealed_to_spend)?; let confidential_inputs = self.outputs_api.resolve_output_masks(confidential_inputs)?; @@ -243,63 +244,15 @@ where } // Reserve and lock input funds for fees - let max_fee = params.max_fee.into(); - let fee_inputs_to_spend = self.resolved_inputs_for_transfer( - *from_account.address(), - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - max_fee, - ConfidentialTransferInputSelection::PreferRevealed, - )?; + let max_fee = params.max_fee; let account_secret = self.key_manager_api.derive_account_key(account.key_index())?; let account_public_key = PublicKey::from_secret_key(&account_secret.key); - // Generate fee proof - let fee_not_paid_by_revealed = max_fee - .checked_sub_positive(fee_inputs_to_spend.revealed) - .expect("BUG: PreferRevealed did not pay <= the max_fee in revealed fees"); - let confidential_change = fee_inputs_to_spend.total_confidential_amount() - fee_not_paid_by_revealed; - let maybe_fee_change_statement = if confidential_change.is_zero() { - // No change necessary - None - } else { - let statement = self.create_confidential_proof_statement(&account_public_key, confidential_change, None)?; - - self.outputs_api.add_output(ConfidentialOutputModel { - account_address: *account.address(), - vault_id: src_vault.id, - commitment: statement - .to_commitment() - .expect("BUG: to_commitment negative amount") - .to_byte_type(), - value: confidential_change, - sender_public_nonce: Some(statement.sender_public_nonce.to_byte_type()), - encryption_secret_key_index: account_secret.key_index, - encrypted_data: statement.encrypted_data.clone(), - public_asset_tag: None, - // TODO: We could technically spend this output in the main transaction, however, we cannot mark it - // as unspent e.g. in the case of tx failure. We should allow spending of LockedUnconfirmed if - // the locking transaction is the same. - status: OutputStatus::LockedUnconfirmed, - lock_id: Some(fee_inputs_to_spend.lock_id), - })?; - - Some(statement) - }; - - let fee_withdraw_proof = self.crypto_api.generate_withdraw_proof( - fee_inputs_to_spend.confidential.as_slice(), - fee_inputs_to_spend.revealed, - None, - params.max_fee.into(), - maybe_fee_change_statement.as_ref(), - // We always withdraw the exact amount of revealed required - Amount::zero(), - )?; - // Reserve and lock input funds - // TODO: preserve atomicity across api calls - needed in many places + let lock_id = self.outputs_api.create_lock()?; let inputs_to_spend = match self.resolved_inputs_for_transfer( + lock_id, params.from_account, params.resource_address, params.amount, @@ -308,17 +261,6 @@ where Ok(inputs) => inputs, Err(e) => { warn!(target: LOG_TARGET, "Unlocking fee fund locks after error: {}", e); - // This is a hack that addresses the case where input locking fails after the fee transaction. However - // any error after this point do not undo locking. This is a limitation of the current - // design - the db transaction should be passed in and automatically rolled back on error. - if let Err(err) = self.outputs_api.release_locked_outputs(fee_inputs_to_spend.lock_id) { - error!( - target: LOG_TARGET, - "Failed to release fee inputs for transfer: {}", - err - ); - } - return Err(e); }, }; @@ -360,9 +302,7 @@ where }); let change_confidential_amount = inputs_to_spend.total_confidential_amount() - remaining_left_to_pay; - let maybe_change_statement = if change_confidential_amount.is_zero() { - None - } else { + let maybe_change_statement = if change_confidential_amount.is_positive() { let statement = self.create_confidential_proof_statement( &account_public_key, change_confidential_amount, @@ -371,7 +311,7 @@ where let change_value = statement.amount; - if !statement.amount.is_zero() { + if change_value.is_positive() { self.outputs_api.add_output(ConfidentialOutputModel { account_address: *account.address(), vault_id: src_vault.id, @@ -390,6 +330,8 @@ where } Some(statement) + } else { + None }; let proof = self.crypto_api.generate_withdraw_proof( @@ -405,7 +347,8 @@ where let transaction = Transaction::builder() .for_network(network.as_byte()) .with_dry_run(params.is_dry_run) - .fee_transaction_pay_from_component_confidential(*from_account.address(), fee_withdraw_proof) + // TODO: we assume that from_account has XTR + .fee_transaction_pay_from_component(*from_account.address(), max_fee) .then(|builder| { if dest_account_exists { builder @@ -440,13 +383,10 @@ where let tx_id = transaction.calculate_id(); self.outputs_api - .locks_set_transaction_hash(inputs_to_spend.lock_id, tx_id)?; - self.outputs_api - .locks_set_transaction_hash(fee_inputs_to_spend.lock_id, tx_id)?; + .locks_set_transaction_id(inputs_to_spend.lock_id, tx_id)?; Ok(TransferOutput { transaction, - fee_transaction_proof_id: fee_inputs_to_spend.lock_id, transaction_proof_id: inputs_to_spend.lock_id, }) } @@ -494,8 +434,7 @@ where pub struct TransferOutput { pub transaction: Transaction, - pub fee_transaction_proof_id: OutputLockId, - pub transaction_proof_id: OutputLockId, + pub transaction_proof_id: WalletLockId, } #[derive(Debug)] @@ -556,7 +495,7 @@ pub enum ConfidentialTransferInputSelection { #[derive(Debug)] pub struct InputsToSpend { pub confidential: Vec, - pub lock_id: OutputLockId, + pub lock_id: WalletLockId, pub revealed: Amount, } diff --git a/crates/wallet/sdk/src/apis/key_manager.rs b/crates/wallet/sdk/src/apis/key_manager.rs index a33cc5c6a1..69dcffec2b 100644 --- a/crates/wallet/sdk/src/apis/key_manager.rs +++ b/crates/wallet/sdk/src/apis/key_manager.rs @@ -33,6 +33,8 @@ pub enum KeyBranch { StealthMasks, /// The confidential masks branch, used to derive masks for confidential transactions. ConfidentialMasks, + /// Used to generate nonces that need to be recreated later, e.g. to derive the DH secret for claim burn + Nonce, } impl KeyBranch { @@ -43,6 +45,7 @@ impl KeyBranch { Self::ViewKey => "view_key", Self::StealthMasks => "stealth_masks", Self::ConfidentialMasks => "confidential_masks", + Self::Nonce => "nonce", } } } @@ -109,6 +112,15 @@ impl<'a, TStore: WalletStore> KeyManagerApi<'a, TStore> { Ok(key) } + pub fn derive_keypair>(&self, branch: B, index: u64) -> Result { + let key = self.derive_key(branch, index)?; + let public_key = RistrettoPublicKey::from_secret_key(&key.key); + Ok(KeyPair { + public_key, + secret_key: key, + }) + } + pub fn derive_account_keypair( &self, index: u64, diff --git a/crates/wallet/sdk/src/apis/stealth_crypto.rs b/crates/wallet/sdk/src/apis/stealth_crypto.rs index 134abe5206..f496c04944 100644 --- a/crates/wallet/sdk/src/apis/stealth_crypto.rs +++ b/crates/wallet/sdk/src/apis/stealth_crypto.rs @@ -3,13 +3,16 @@ use std::ops::RangeInclusive; -use tari_common_types::types::PrivateKey; -use tari_crypto::ristretto::RistrettoPublicKey; +use log::*; +use tari_crypto::{ + ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSecretKey}, + signatures::CommitmentSignature, +}; use tari_engine_types::{ - crypto::{ElgamalVerifiableBalance, PrivateOutput, ValueLookupTable}, + crypto::{get_commitment_factory, ElgamalVerifiableBalance, PrivateOutput, ValueLookupTable}, FromByteType, }; -use tari_ootle_common_types::Network; +use tari_ootle_common_types::{base_layer_hashing::ownership_proof_hasher64, Network}; use tari_ootle_wallet_crypto::{ confidential, encrypted_data::{encrypt_value_and_mask, extract_value_and_mask, unblind_output}, @@ -24,10 +27,12 @@ use tari_ootle_wallet_crypto::{ }; use tari_template_lib::{ models::{ConfidentialOutputStatement, EncryptedData, StealthTransferStatement}, - prelude::{PedersenCommitmentBytes, RistrettoPublicKeyBytes}, + prelude::{crypto::CommitmentSignatureBytes, PedersenCommitmentBytes, RistrettoPublicKeyBytes}, types::{crypto::UtxoTagByte, Amount}, }; +const LOG_TARGET: &str = "tari::ootle::wallet::sdk::stealth_crypto"; + pub struct StealthCryptoApi; impl StealthCryptoApi { @@ -38,18 +43,23 @@ impl StealthCryptoApi { pub fn derive_encrypted_data_key_for_receiver( &self, public_nonce: &RistrettoPublicKey, - private_key: &PrivateKey, - ) -> PrivateKey { + private_key: &RistrettoSecretKey, + ) -> RistrettoSecretKey { kdfs::encrypted_data_dh_kdf_aead(private_key, public_nonce) } - pub fn generate_transfer_statement>( + pub fn generate_transfer_statement<'a, A, Inputs, Outputs>( &self, - inputs: &[UnblindedStealthInputStatement], + inputs: Inputs, input_revealed_amount: A, - output_statements: &[UnblindedStealthOutputStatement], + output_statements: Outputs, output_revealed_amount: A, - ) -> Result { + ) -> Result + where + A: Into, + Inputs: IntoIterator, + Outputs: IntoIterator + Clone, + { let stmt = stealth::create_transfer_statement( inputs, input_revealed_amount @@ -73,12 +83,30 @@ impl StealthCryptoApi { kdfs::derive_stealth_output_tag(network, dest_public_key) } + pub fn derive_stealth_owner_public_key( + &self, + network: Network, + public_key: &RistrettoPublicKey, + nonce_secret: &RistrettoSecretKey, + ) -> RistrettoPublicKey { + kdfs::owner_stealth_dh_stealth_address(network, public_key, nonce_secret) + } + + pub fn derive_stealth_owner_secret( + &self, + network: Network, + secret_key: &RistrettoSecretKey, + public_nonce: &RistrettoPublicKey, + ) -> RistrettoSecretKey { + kdfs::owner_stealth_dh_secret(network, secret_key, public_nonce) + } + pub fn encrypt_value_and_mask( &self, amount: u64, - mask: &PrivateKey, + mask: &RistrettoSecretKey, public_nonce: &RistrettoPublicKey, - secret: &PrivateKey, + secret: &RistrettoSecretKey, ) -> Result { let data = encrypt_value_and_mask(amount, mask, public_nonce, secret)?; Ok(data) @@ -86,10 +114,10 @@ impl StealthCryptoApi { pub fn extract_value_and_mask( &self, - encryption_key: &PrivateKey, + encryption_key: &RistrettoSecretKey, commitment: &PedersenCommitmentBytes, encrypted_data: &EncryptedData, - ) -> Result<(u64, PrivateKey), StealthCryptoApiError> { + ) -> Result<(u64, RistrettoSecretKey), StealthCryptoApiError> { let value_and_mask = extract_value_and_mask(encryption_key, commitment, encrypted_data)?; Ok(value_and_mask) } @@ -112,7 +140,7 @@ impl StealthCryptoApi { &self, output_commitment: &PedersenCommitmentBytes, output_encrypted_value: &EncryptedData, - claim_secret: &PrivateKey, + claim_secret: &RistrettoSecretKey, reciprocal_public_key: &RistrettoPublicKey, ) -> Result { let unmasked_output = unblind_output( @@ -126,7 +154,7 @@ impl StealthCryptoApi { pub fn try_brute_force_commitment_balances<'a, TLookup, TOutputsIter>( &self, - secret_view_key: &PrivateKey, + secret_view_key: &RistrettoSecretKey, outputs: TOutputsIter, value_range: RangeInclusive, lookup: &mut TLookup, @@ -156,6 +184,39 @@ impl StealthCryptoApi { Ok(results) } + + pub fn validate_burn_claim_ownership_proof( + &self, + network: Network, + ownership_proof: &CommitmentSignatureBytes, + commitment: &PedersenCommitmentBytes, + account_owner_pk: &RistrettoPublicKeyBytes, + ) -> bool { + // NOTE: .as_bytes() used because the tari_crypto borsh implementations serialize fixed length bytes as variable + // length bytes of size 32 + let message = ownership_proof_hasher64(network) + .chain(&ownership_proof.public_nonce().as_bytes()) + .chain(&commitment.as_bytes()) + .chain(&account_owner_pk.as_bytes()) + .finalize(); + + let Ok(commitment) = PedersenCommitment::try_from_byte_type(commitment) else { + warn!(target: LOG_TARGET, "Claim burn failed - malformed commitment"); + return false; + }; + + let Ok(proof_of_knowledge) = CommitmentSignature::try_from_byte_type(ownership_proof) else { + warn!(target: LOG_TARGET, "Claim burn failed - malformed proof of knowledge"); + return false; + }; + + if !proof_of_knowledge.verify_challenge(&commitment, &message, get_commitment_factory()) { + warn!(target: LOG_TARGET, "Claim burn failed - signature verification failed"); + return false; + } + + true + } } #[derive(Debug, thiserror::Error)] diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs index 1c34c396b1..739975070d 100644 --- a/crates/wallet/sdk/src/apis/stealth_outputs.rs +++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs @@ -32,8 +32,9 @@ use crate::{ config::{ConfigApi, ConfigApiError}, key_manager::{KeyBranch, KeyManagerApi, KeyManagerApiError}, stealth_crypto::{StealthCryptoApi, StealthCryptoApiError}, + stealth_transfer::InputToSpend, }, - models::{AccountWithPublicKey, KeyPair, OutputLockId, OutputStatus, StealthBalance, StealthOutputModel}, + models::{Account, KeyPair, OutputStatus, StealthBalance, StealthOutputModel, WalletLockId}, storage::{WalletStorageError, WalletStore, WalletStoreReader, WalletStoreWriter}, }; @@ -61,10 +62,13 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { } } - pub fn lock_outputs_in_account_by_amount>( + /// Locks as many outputs required to reach at least the specified amount. If there are insufficient funds, an + /// `InsufficientFunds` error is returned and no outputs are locked. + pub fn lock_outputs_for_at_least_amount>( &self, account_address: &ComponentAddress, - lock_id: OutputLockId, + resource_address: &ResourceAddress, + lock_id: WalletLockId, amount: A, ) -> Result<(Vec, Amount), StealthOutputsApiError> { let amount = amount @@ -75,7 +79,8 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { reason: "Amount must be non-negative".to_string(), })?; self.store.with_write_tx(|tx| { - let (outputs, total_output_amount) = self.lock_outputs_internal(tx, account_address, amount, lock_id)?; + let (outputs, total_output_amount) = + self.lock_outputs_internal(tx, account_address, resource_address, amount, lock_id)?; if total_output_amount < amount { return Err(StealthOutputsApiError::InsufficientFunds); @@ -85,22 +90,36 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { }) } + pub fn locks_set_transaction_id( + &self, + lock_id: WalletLockId, + transaction_id: TransactionId, + ) -> Result<(), StealthOutputsApiError> { + self.store + .with_write_tx(|tx| tx.locks_link_transaction(lock_id, transaction_id))?; + Ok(()) + } + + /// Locks as many outputs required to reach at least the specified amount. If there are insufficient funds, all + /// available outputs will be locked and returned along with the total amount locked. pub fn lock_outputs_until_partial_amount( &self, account_address: &ComponentAddress, + resource_address: &ResourceAddress, amount: Amount, - locked_by_id: OutputLockId, + locked_by_id: WalletLockId, ) -> Result<(Vec, Amount), StealthOutputsApiError> { self.store - .with_write_tx(|tx| self.lock_outputs_internal(tx, account_address, amount, locked_by_id)) + .with_write_tx(|tx| self.lock_outputs_internal(tx, account_address, resource_address, amount, locked_by_id)) } fn lock_outputs_internal( &self, tx: &mut TTx, account_address: &ComponentAddress, + resource_address: &ResourceAddress, amount: Amount, - locked_by_id: OutputLockId, + locked_by_id: WalletLockId, ) -> Result<(Vec, Amount), StealthOutputsApiError> { if amount.is_negative() { return Err(StealthOutputsApiError::InvalidParameter { @@ -112,7 +131,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { let mut outputs = Vec::new(); while total_output_amount < amount { let output = tx - .stealth_outputs_lock_smallest_amount(account_address, locked_by_id) + .stealth_outputs_lock_smallest_amount(account_address, resource_address, locked_by_id) .optional()?; match output { Some(output) => { @@ -141,48 +160,47 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(()) } - pub fn create_lock_for_vault(&self, vault_id: &VaultId) -> Result { - let mut tx = self.store.create_write_tx()?; - let lock_id = tx.output_locks_insert_for_vault(vault_id)?; - tx.commit()?; - Ok(lock_id) + pub fn lock_funds_in_vault>( + &self, + lock_id: WalletLockId, + vault_id: &VaultId, + amount_to_lock: A, + ) -> Result<(), StealthOutputsApiError> { + self.store + .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount_to_lock.into()))?; + Ok(()) } - pub fn create_lock_for_resource( - &self, - resource_address: &ResourceAddress, - ) -> Result { - let lock_id = self - .store - .with_write_tx(|tx| tx.output_locks_insert(resource_address))?; + pub fn create_lock(&self) -> Result { + let lock_id = self.store.with_write_tx(|tx| tx.locks_create())?; Ok(lock_id) } - pub fn release_locked_outputs(&self, lock_id: OutputLockId) -> Result<(), StealthOutputsApiError> { + pub fn release_locked_outputs(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { self.store.with_write_tx(|tx| { - tx.output_locks_delete(lock_id)?; tx.outputs_release_by_lock_id(lock_id)?; + tx.locks_delete(lock_id)?; Ok(()) }) } - pub fn finalize_outputs(&self, lock_id: OutputLockId) -> Result<(), StealthOutputsApiError> { + pub fn finalize_outputs(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { self.store.with_write_tx(|tx| { - tx.output_locks_delete(lock_id)?; tx.stealth_outputs_finalize_by_lock_id(lock_id)?; + tx.locks_delete(lock_id)?; Ok(()) }) } pub fn resolve_output_masks_for_spending( &self, - owner_account: &AccountWithPublicKey, + owner_account: &Account, outputs: Vec, - ) -> Result, StealthOutputsApiError> { + ) -> Result, StealthOutputsApiError> { let network = self.config_api.get_network()?; // Derive owner secret - the sender does not know the owner secret let owner_key_part = self.key_manager_api.derive_account_key(owner_account.key_index())?; - let mut outputs_with_masks = Vec::with_capacity(outputs.len()); + let mut inputs_with_masks = Vec::with_capacity(outputs.len()); for output in outputs { // Derive the account secret, of which the public key is used by senders to encrypt the value and mask. let decryption_key_part = self @@ -205,32 +223,38 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { &output.encrypted_data, )?; - let stealth_secret = kdfs::owner_stealth_dh_secret(network, &owner_key_part.key, &nonce); - - outputs_with_masks.push(UnblindedStealthInputStatement { - mask_and_value: MaskAndValue { - value: output.value, - mask, + let stealth_secret = self + .crypto_api + .derive_stealth_owner_secret(network, &owner_key_part.key, &nonce); + + inputs_with_masks.push(InputToSpend { + statement: UnblindedStealthInputStatement { + mask_and_value: MaskAndValue { + value: output.value, + mask, + }, + owner_secret: stealth_secret, + public_nonce: nonce, }, - owner_secret: stealth_secret, - public_nonce: nonce, + is_on_chain: output.is_on_chain, }); } - Ok(outputs_with_masks) + Ok(inputs_with_masks) } - pub fn lock_revealed_funds( + pub fn lock_revealed_funds>( &self, - lock_id: OutputLockId, - amount_to_lock: Amount, + lock_id: WalletLockId, + vault_id: &VaultId, + amount_to_lock: A, ) -> Result<(), StealthOutputsApiError> { self.store - .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, amount_to_lock))?; + .with_write_tx(|tx| tx.vaults_lock_revealed_funds(lock_id, vault_id, amount_to_lock.into()))?; Ok(()) } - pub fn finalize_locked_revealed_funds(&self, lock_id: OutputLockId) -> Result<(), StealthOutputsApiError> { + pub fn finalize_locked_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { let mut tx = self.store.create_write_tx()?; tx.vaults_finalized_locked_revealed_funds(lock_id)?; tx.commit()?; @@ -238,9 +262,9 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(()) } - pub fn release_revealed_funds(&self, lock_id: OutputLockId) -> Result<(), StealthOutputsApiError> { + pub fn release_revealed_funds(&self, lock_id: WalletLockId) -> Result<(), StealthOutputsApiError> { let mut tx = self.store.create_write_tx()?; - tx.vaults_unlock_revealed_funds(lock_id)?; + tx.vaults_release_lock_revealed_funds(lock_id)?; tx.commit()?; Ok(()) @@ -458,7 +482,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { ); let (value, status) = match unblinded_result { Ok(mask_and_value) => { - let stealth_secret = kdfs::owner_stealth_dh_secret( + let stealth_secret = self.crypto_api.derive_stealth_owner_secret( network, &wallet_key.secret_key.key, &output_stealth_public_nonce, @@ -514,32 +538,13 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { status, is_burnt, is_frozen, + is_on_chain: true, lock_id: None, })); } Ok(None) } - - pub fn set_transaction_hash_for_lock( - &self, - lock_id: OutputLockId, - transaction_id: TransactionId, - ) -> Result<(), StealthOutputsApiError> { - self.store - .with_write_tx(|tx| tx.output_locks_set_params(lock_id, Some(transaction_id), None))?; - Ok(()) - } - - pub fn set_vault_id_for_lock( - &self, - lock_id: OutputLockId, - vault_id: VaultId, - ) -> Result<(), StealthOutputsApiError> { - self.store - .with_write_tx(|tx| tx.output_locks_set_params(lock_id, None, Some(vault_id)))?; - Ok(()) - } } #[derive(Debug, thiserror::Error)] diff --git a/crates/wallet/sdk/src/apis/stealth_transfer.rs b/crates/wallet/sdk/src/apis/stealth_transfer.rs index 6106e76d34..67d33a67f4 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer.rs @@ -13,14 +13,13 @@ use tari_ootle_common_types::{ SubstateRequirement, }; use tari_ootle_wallet_crypto::{ - kdfs, MaskAndValue, UnblindedOutputStatement, UnblindedStealthInputStatement, UnblindedStealthOutputStatement, }; use tari_template_lib::{ - models::{Account, ComponentAddress, ResourceAddress, VaultId}, + models::{Account as BuiltinAccount, ComponentAddress, ResourceAddress, StealthTransferStatement, VaultId}, prelude::{RistrettoPublicKeyBytes, XTR}, types::Amount, }; @@ -36,7 +35,7 @@ use crate::{ stealth_outputs::{StealthOutputsApi, StealthOutputsApiError}, substate::{SubstateApiError, SubstatesApi, ValidatorScanResult}, }, - models::{AccountWithPublicKey, OutputLockId, OutputStatus, StealthOutputModel}, + models::{Account, AccountWithPublicKey, OutputStatus, StealthOutputModel, WalletLockId}, network::WalletNetworkInterface, storage::{WalletStorageError, WalletStore}, }; @@ -76,10 +75,25 @@ where } } + fn resolve_fee_inputs( + &self, + lock_id: WalletLockId, + params: &StealthTransferParams, + ) -> Result { + self.resolved_inputs_for_transfer( + lock_id, + params.owner_account.account(), + XTR, + params.max_fee.into(), + ConfidentialTransferInputSelection::PreferRevealed, + ) + } + #[allow(clippy::too_many_lines)] fn resolved_inputs_for_transfer( &self, - from_account: &AccountWithPublicKey, + lock_id: WalletLockId, + owner_account: &Account, resource_address: ResourceAddress, spend_amount: Amount, input_selection: ConfidentialTransferInputSelection, @@ -93,7 +107,7 @@ where let maybe_src_vault = self .accounts_api - .get_vault_by_resource(from_account.address(), &resource_address) + .get_vault_by_resource(owner_account.address(), &resource_address) .optional()?; let available_revealed_funds = maybe_src_vault @@ -103,25 +117,25 @@ where match input_selection { ConfidentialTransferInputSelection::ConfidentialOnly => { - let lock_id = self.outputs_api.create_lock_for_resource(&resource_address)?; - let (input_models, _) = self.outputs_api.lock_outputs_in_account_by_amount( - from_account.address(), + let (input_models, total_locked) = self.outputs_api.lock_outputs_for_at_least_amount( + owner_account.address(), + &resource_address, lock_id, spend_amount, )?; let inputs = self .outputs_api - .resolve_output_masks_for_spending(from_account, input_models)?; + .resolve_output_masks_for_spending(owner_account, input_models)?; info!( target: LOG_TARGET, - "ConfidentialOnly: Locked {} confidential inputs for transfer", + "ConfidentialOnly: Locked {} confidential inputs for transfer worth {}", inputs.len(), + total_locked ); Ok(InputsToSpend { inputs, - lock_id, revealed: Amount::zero(), }) }, @@ -137,12 +151,12 @@ where details: format!( "No vault found for resource {} in account {}", resource_address, - from_account.address() + owner_account.address() ), })?; - let lock_id = self.outputs_api.create_lock_for_vault(&src_vault.id)?; - self.outputs_api.lock_revealed_funds(lock_id, spend_amount)?; + self.outputs_api + .lock_funds_in_vault(lock_id, &src_vault.id, spend_amount)?; info!( target: LOG_TARGET, @@ -153,28 +167,23 @@ where Ok(InputsToSpend { inputs: vec![], - lock_id, revealed: spend_amount, }) }, ConfidentialTransferInputSelection::PreferRevealed => { - let maybe_vault_lock_id = maybe_src_vault - .as_ref() - .map(|v| self.outputs_api.create_lock_for_vault(&v.id)) - .transpose()?; let revealed_to_spend = cmp::min(available_revealed_funds, spend_amount); let utxo_amount_to_spend = spend_amount - revealed_to_spend; - if let Some(lock_id) = maybe_vault_lock_id { + if let Some(ref src_vault) = maybe_src_vault { if utxo_amount_to_spend.is_zero() { info!( target: LOG_TARGET, "PreferRevealed: Spending {} revealed balance (available: {}) for transfer from {}", revealed_to_spend, available_revealed_funds, - maybe_src_vault.as_ref().map(|v| v.id).display() + src_vault.id ); - self.outputs_api.lock_revealed_funds(lock_id, revealed_to_spend) + self.outputs_api.lock_funds_in_vault(lock_id, &src_vault.id, revealed_to_spend) .inspect_err(|_| { // TODO: atomic rollback will help with this if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { @@ -184,32 +193,30 @@ where return Ok(InputsToSpend { inputs: vec![], - lock_id, revealed: revealed_to_spend, }); } } - let lock_id = match maybe_vault_lock_id { - Some(lock_id) => lock_id, - None => { - if revealed_to_spend.is_positive() { - // No vault containing revealed funds was found - return Err(StealthTransferApiError::InsufficientRevealedFunds { - details: format!( - "PreferRevealed: No vault found for resource {} in account {}. Need to spend {} \ - revealed funds", - resource_address, - from_account.address(), - revealed_to_spend - ), - }); - } - self.outputs_api.create_lock_for_resource(&resource_address)? - }, - }; + if maybe_src_vault.is_none() && revealed_to_spend.is_positive() { + // No vault containing revealed funds was found + return Err(StealthTransferApiError::InsufficientRevealedFunds { + details: format!( + "PreferRevealed: No vault found for resource {} in account {}. Need to spend {} revealed \ + funds", + resource_address, + owner_account.address(), + revealed_to_spend + ), + }); + } - let (inputs, _) = self.outputs_api.lock_outputs_in_account_by_amount(from_account.address(), lock_id, utxo_amount_to_spend) + let (inputs, _) = self.outputs_api.lock_outputs_for_at_least_amount( + owner_account.address(), + &resource_address, + lock_id, + utxo_amount_to_spend, + ) .inspect_err(|_| { // TODO: atomic rollback will help with this if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { @@ -218,7 +225,7 @@ where })?; let inputs = self .outputs_api - .resolve_output_masks_for_spending(from_account, inputs) + .resolve_output_masks_for_spending(owner_account, inputs) .inspect_err(|_| { // TODO: atomic rollback will help with this if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { @@ -226,12 +233,12 @@ where } })?; - let total_confidential_spent = Amount::sum_from_positive(inputs.iter().map(|i| i.mask_and_value.value)) + let total_confidential_spent = Amount::sum_from_positive(inputs.iter().map(|i| i.value())) // The wallet has somehow stored a negative amount, which should not happen. .expect("BUG: an unblinded input amount was negative"); - if revealed_to_spend.is_positive() { - self.outputs_api.lock_revealed_funds(lock_id, revealed_to_spend) + if let Some(ref src_vault) = maybe_src_vault { + self.outputs_api.lock_revealed_funds(lock_id, &src_vault.id, revealed_to_spend) .inspect_err(|_| { // TODO: atomic rollback will help with this if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { @@ -253,14 +260,14 @@ where Ok(InputsToSpend { inputs, - lock_id, revealed: revealed_to_spend, }) }, ConfidentialTransferInputSelection::PreferConfidential => { - let lock_id = self.outputs_api.create_lock_for_resource(&resource_address)?; + let lock_id = self.outputs_api.create_lock()?; let (blinded_inputs, blinded_amount_locked) = self.outputs_api.lock_outputs_until_partial_amount( - from_account.address(), + owner_account.address(), + &resource_address, spend_amount, lock_id, )?; @@ -277,8 +284,8 @@ where if revealed_to_spend.is_positive() { match maybe_src_vault { Some(vault) => { - self.outputs_api.set_vault_id_for_lock(lock_id, vault.id)?; - self.outputs_api.lock_revealed_funds(lock_id, revealed_to_spend)?; + self.outputs_api + .lock_revealed_funds(lock_id, &vault.id, revealed_to_spend)?; }, None => { if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { @@ -289,7 +296,7 @@ where "PreferConfidential: No vault found for resource {} in account {}. Need to spend \ {} revealed funds", resource_address, - from_account.address(), + owner_account.address(), revealed_to_spend ), }); @@ -299,11 +306,10 @@ where let inputs = self .outputs_api - .resolve_output_masks_for_spending(from_account, blinded_inputs)?; + .resolve_output_masks_for_spending(owner_account, blinded_inputs)?; Ok(InputsToSpend { inputs, - lock_id, revealed: revealed_to_spend, }) }, @@ -313,38 +319,15 @@ where #[allow(clippy::too_many_lines)] pub async fn transfer(&self, params: StealthTransferParams) -> Result { params.validate()?; - // TODO: XTR as a stealth resource - let fee_account = self.accounts_api.get_default()?; let destination_account = derive_account_address_from_public_key(¶ms.destination_public_key); // Determine Transaction Inputs - let mut inputs = Vec::new(); - - let fee_account_substate = self.substate_api.get_substate(&fee_account.account.address.into())?; - inputs.push(fee_account_substate.substate_id.into_unversioned_requirement()); - - // Add all versioned account child addresses as inputs - let child_addresses = self - .substate_api - .load_dependent_substates(&[&fee_account.account.address.into()])?; - inputs.extend(child_addresses.into_iter().map(|a| a.into_unversioned())); - - let fee_vault = self - .accounts_api - .get_vault_by_resource(fee_account.address(), &XTR) - .optional()? - .ok_or_else(|| StealthTransferApiError::InvalidParameter { - param: "fee_resource", - reason: format!( - "XTR fee vault not found for fee account {}. Create or fund an XTR vault to pay fees.", - fee_account.address() - ), - })?; - inputs.push(SubstateRequirement::unversioned(fee_vault.id)); + let mut substate_inputs = Vec::new(); + let owner_account = params.owner_account.clone(); // add the input for the resource address to be transferred - inputs.push(SubstateRequirement::unversioned(params.resource_address)); + substate_inputs.push(SubstateRequirement::unversioned(params.resource_address)); let need_to_create_dest_account = if params.revealed_output_amount.is_positive() { match self @@ -354,13 +337,13 @@ where { Some(local_account) => { if local_account.is_confirmed_on_chain() { - inputs.push(SubstateRequirement::unversioned(destination_account)); + substate_inputs.push(SubstateRequirement::unversioned(destination_account)); if let Some(vault) = self .accounts_api .get_vault_by_resource(local_account.address(), ¶ms.resource_address) .optional()? { - inputs.push(SubstateRequirement::unversioned(vault.id)); + substate_inputs.push(SubstateRequirement::unversioned(vault.id)); } false @@ -370,8 +353,8 @@ where }, None => { // TODO: we're just determining if the account exists - symptom of a larger problem/missing - // feature where account is created as needed instead of having to be determined by the client - // side + // feature where account is created as needed by the execution layer instead of having to be + // determined by the client side let to_account_substate = self .substate_api .fetch_substate_from_network(&SubstateId::Component(destination_account), None) @@ -379,7 +362,7 @@ where .optional()?; if let Some(ValidatorScanResult { id: address, substate }) = to_account_substate { - inputs.push(SubstateRequirement::unversioned(destination_account)); + substate_inputs.push(SubstateRequirement::unversioned(destination_account)); let account = substate @@ -390,7 +373,7 @@ where destination_account, address ), })?; - let dest_account = Account::from_value(account.state()).map_err(|e| { + let dest_account = BuiltinAccount::from_value(account.state()).map_err(|e| { StealthTransferApiError::UnexpectedIndexerResponse { details: format!("Failed to convert component substate to account: {e}"), } @@ -404,7 +387,7 @@ where params.resource_address, destination_account ); - inputs.push(SubstateRequirement::unversioned(vault.vault_id())); + substate_inputs.push(SubstateRequirement::unversioned(vault.vault_id())); } else { debug!( target: LOG_TARGET, @@ -451,10 +434,66 @@ where }) .transpose()?; + // Resolve fee inputs + let lock_id = self.outputs_api.create_lock()?; + let fee_inputs_to_spend = self.resolve_fee_inputs(lock_id, ¶ms)?; + + // TODO: use single db transaction across calls + // --- Any error from here can result in funds staying locked --- + + let fee_change = fee_inputs_to_spend + .total_stealth_input_amount() + .saturating_sub(params.max_fee.into()); + + // Generate fee change outputs if required + let fee_change_output_statement = fee_change + .is_positive() + .then(|| self.create_output_statement(&owner_account.to_ristretto_public_key(), fee_change, None)) + .transpose() + .inspect_err(|e| { + warn!(target: LOG_TARGET, "Unlocking fee fund locks after error: {}", e); + // This is a hack that addresses the case where output creation fails after the fee transaction. + // However, any error after this point do not undo locking. This is a limitation + // of the current design - the db transaction should be passed in and + // automatically rolled back on error. + if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { + error!( + target: LOG_TARGET, + "Failed to release fee inputs for transfer: {}", + err + ); + } + })?; + // Generate fee reveal statement + let fee_transfer_statement = self.crypto_api.generate_transfer_statement( + fee_inputs_to_spend.statements_iter(), + fee_inputs_to_spend.revealed, + fee_change_output_statement.iter(), + Amount::from(params.max_fee), + )?; + + if let Some(ref fee_change) = fee_change_output_statement { + self.add_unconfirmed_output_from_statement( + lock_id, + ¶ms.owner_account, + params.resource_address, + fee_change, + )?; + } + + substate_inputs.extend( + fee_inputs_to_spend + .inputs + .iter() + .filter(|i| i.is_on_chain) + .map(|i| &i.statement) + .map(|i| SubstateRequirement::unversioned(to_utxo_address(XTR, &i.mask_and_value))), + ); + // Reserve and lock input funds - // TODO: preserve atomicity across api calls - needed in many places let inputs_to_spend = match self.resolved_inputs_for_transfer( - ¶ms.owner_account, + lock_id, + params.owner_account.account(), params.resource_address, params.total_output_amount(), params.input_selection, @@ -466,44 +505,96 @@ where // However, any error after this point do not undo locking. This is a limitation // of the current design - the db transaction should be passed in and // automatically rolled back on error. - // if let Err(err) = self.outputs_api.release_proof_outputs(fee_inputs_to_spend.proof_id) { - // error!( - // target: LOG_TARGET, - // "Failed to release fee inputs for transfer: {}", - // err - // ); - // } + if let Err(err) = self.outputs_api.release_locked_outputs(lock_id) { + error!( + target: LOG_TARGET, + "Failed to release fee inputs for transfer: {}", + err + ); + } return Err(e); }, }; + // If we're spending from the owner account, add the inputs + if inputs_to_spend.revealed.is_positive() { + substate_inputs.push(SubstateRequirement::unversioned(*owner_account.address())); + + // Add the vaults for XTR (fees) and the spending resource if different + if let Some(vault) = self + .accounts_api + .get_vault_by_resource(owner_account.address(), &XTR) + .optional()? + { + substate_inputs.push(SubstateRequirement::unversioned(vault.id)); + substate_inputs.push(SubstateRequirement::unversioned(vault.resource_address)); + } + if params.resource_address != XTR { + if let Some(vault) = self + .accounts_api + .get_vault_by_resource(owner_account.address(), ¶ms.resource_address) + .optional()? + { + substate_inputs.push(SubstateRequirement::unversioned(vault.id)); + substate_inputs.push(SubstateRequirement::unversioned(vault.resource_address)); + } + } + } + // Add all input UTXO substates to transaction inputs - inputs.extend( + substate_inputs.extend( inputs_to_spend .inputs .iter() + .filter(|i| i.is_on_chain) + .map(|i| &i.statement) .map(|i| SubstateRequirement::unversioned(to_utxo_address(params.resource_address, &i.mask_and_value))), ); - let result = self.generate_transfer_transaction( - params, - fee_account, - inputs, + // Any change outputs? + let maybe_change_statement = self.generate_change_statement( + lock_id, + ¶ms.owner_account, + params.resource_address, resource_view_key, - output_statement, &inputs_to_spend, + params.total_output_amount(), + )?; + + let outputs = output_statement + .filter(|o| o.statement.amount.is_positive()) + .into_iter() + .chain(maybe_change_statement) + .collect::>(); + + let transfer_statement = self.crypto_api.generate_transfer_statement( + inputs_to_spend.statements_iter(), + inputs_to_spend.revealed, + &outputs, + params.revealed_output_amount, + )?; + + let result = self.generate_transfer_transaction( + params, + substate_inputs, + fee_transfer_statement, + transfer_statement, need_to_create_dest_account, ); match result { - Ok(transaction) => Ok(TransferOutput { - transaction, - transaction_lock_id: Some(inputs_to_spend.lock_id), - }), + Ok(transaction) => { + let tx_id = transaction.calculate_id(); + self.outputs_api.locks_set_transaction_id(lock_id, tx_id)?; + Ok(TransferOutput { + transaction, + transaction_lock_id: Some(lock_id), + }) + }, Err(err) => { // Unlock inputs - if let Err(e) = self.outputs_api.release_locked_outputs(inputs_to_spend.lock_id) { + if let Err(e) = self.outputs_api.release_locked_outputs(lock_id) { error!(target: LOG_TARGET, "Failed to release inputs lock after error: {}", e); } Err(err) @@ -511,141 +602,154 @@ where } } - #[allow(clippy::too_many_lines)] fn generate_transfer_transaction( &self, params: StealthTransferParams, - fee_account: AccountWithPublicKey, inputs: Vec, - resource_view_key: Option, - maybe_output_statement: Option, - inputs_to_spend: &InputsToSpend, + fee_transfer_statement: StealthTransferStatement, + transfer_statement: StealthTransferStatement, need_to_create_account: bool, ) -> Result { - let change_amount = inputs_to_spend - .total_amount() - .checked_sub_positive(params.total_output_amount()) - .unwrap_or_else(|| { - // This is a bug because the wallet chooses inputs based on the required outputs. This function should - // not have been called if there are insufficient funds. - error!( - target: LOG_TARGET, - "BUG: total_stealth_input_amount or params.total_amount() are negative after validation" - ); - panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); - }); + let network = self.config_api.get_network()?; + let revealed_input_amount = transfer_statement.inputs_statement.revealed_amount; + let revealed_output_amount = transfer_statement.outputs_statement.revealed_output_amount; - let maybe_change_statement = if change_amount.is_zero() { - None + let signer_secret = if revealed_input_amount.is_positive() { + self.key_manager_api + .derive_account_key(params.owner_account.key_index())? } else { - let change_public_key = RistrettoPublicKey::try_from_byte_type(¶ms.owner_account.owner_public_key) - .map_err(|e| StealthTransferApiError::InvalidParameter { - param: "owner_public_key", - reason: format!("Invalid owner public key: {e}"), - })?; - - let change = self.create_output_statement(&change_public_key, change_amount, resource_view_key)?; - - let change_value = change.statement.amount; - - if !change.statement.amount.is_zero() { - self.outputs_api.add_output(&StealthOutputModel { - owner_account: *params.owner_account.address(), - resource_address: params.resource_address, - commitment: change - .statement - .to_commitment() - .expect("BUG: to_commitment negative amount") - .to_byte_type(), - value: change_value, - sender_public_nonce: change.statement.sender_public_nonce.to_byte_type(), - encryption_secret_key_index: params.owner_account.key_index(), - encrypted_data: change.statement.encrypted_data.clone(), - status: OutputStatus::LockedUnconfirmed, - tag_byte: change.tag, - lock_id: Some(inputs_to_spend.lock_id), - is_burnt: false, - is_frozen: false, - })?; - } - - Some(change) + // Since we don't require account auth, use a throw away nonce to sign the transaction + self.key_manager_api.next_key(KeyBranch::Nonce)? }; - let outputs = maybe_output_statement - .filter(|o| o.statement.amount.is_positive()) - .into_iter() - .chain(maybe_change_statement) - .collect::>(); - - let transfer_statement = self.crypto_api.generate_transfer_statement( - &inputs_to_spend.inputs, - inputs_to_spend.revealed, - &outputs, - params.revealed_output_amount, - )?; - - let fee_account_secret = self.key_manager_api.derive_account_key(fee_account.key_index())?; - let src_account_secret = self - .key_manager_api - .derive_account_key(params.owner_account.key_index())?; - - let network = self.config_api.get_network()?; let transaction = Transaction::builder() .for_network(network.as_byte()) .with_dry_run(params.is_dry_run) - // TODO: pay fees using stealth XTR when that is implemented - .fee_transaction_pay_from_component(*fee_account.address(), params.max_fee) + .with_fee_instructions_builder(|builder| { + if revealed_input_amount.is_positive() { + builder + .call_method(*params.owner_account.address(), "withdraw", args![ + XTR, + fee_transfer_statement.inputs_statement.revealed_amount + ]) + .put_last_instruction_output_on_workspace("fee_input_bucket") + .pay_fee_stealth_with_input_bucket(fee_transfer_statement, "fee_input_bucket") + } else { + builder.pay_fee_stealth(fee_transfer_statement) + } + }) .then(|builder| { - if inputs_to_spend.revealed.is_positive() { + if revealed_input_amount.is_positive() { builder - .call_method(params.owner_account.account.address, "withdraw", args![params.resource_address, inputs_to_spend.revealed]) + .call_method(params.owner_account.account.address, "withdraw", args![ + params.resource_address, + revealed_input_amount + ]) .put_last_instruction_output_on_workspace("input_bucket") .stealth_transfer_with_input_bucket(params.resource_address, transfer_statement, "input_bucket") } else { - builder - .stealth_transfer(params.resource_address, transfer_statement) + builder.stealth_transfer(params.resource_address, transfer_statement) } }) .then(|builder| { // revealed_to_account may be Some, but we only use it if revealed_output_amount is greater than zero. - if params.revealed_output_amount.is_zero() { + if revealed_output_amount.is_zero() { return builder; } // If the transfer creates revealed outputs, deposit the bucket into the destination account. - builder.put_last_instruction_output_on_workspace("output_bucket").then(|builder| { - if need_to_create_account { - builder.create_account_with_bucket( - params.destination_public_key, - "output_bucket", - ) - } else { - builder.call_method( - params.derived_destination_account(), - "deposit", - args![Workspace("output_bucket")], - ) - } - }) + builder + .put_last_instruction_output_on_workspace("output_bucket") + .then(|builder| { + if need_to_create_account { + builder.create_account_with_bucket(params.destination_public_key, "output_bucket") + } else { + builder.call_method(params.derived_destination_account(), "deposit", args![Workspace( + "output_bucket" + )]) + } + }) }) .with_inputs(inputs) - .then(|builder| { - // If the fee account is different from the owner account, we need to add a signature to authorize the revealed funds withdraw. - if inputs_to_spend.revealed.is_positive() && params.owner_account.owner_public_key != *fee_account.owner_public_key() { - builder.add_signature(fee_account.owner_public_key(), &src_account_secret.key) - } else { - builder - } - }) - .build_and_seal(&fee_account_secret.key); + .build_and_seal(&signer_secret.key); - let tx_id = transaction.calculate_id(); - self.outputs_api - .set_transaction_hash_for_lock(inputs_to_spend.lock_id, tx_id)?; Ok(transaction) } + fn generate_change_statement( + &self, + lock_id: WalletLockId, + account: &AccountWithPublicKey, + resource_address: ResourceAddress, + resource_view_key: Option, + inputs_to_spend: &InputsToSpend, + total_output_amount: Amount, + ) -> Result, StealthTransferApiError> { + let change_amount = inputs_to_spend + .total_amount() + .checked_sub_positive(total_output_amount) + .unwrap_or_else(|| { + // This is a bug because the wallet chooses inputs based on the required outputs. This function should + // not have been called if there are insufficient funds. + error!( + target: LOG_TARGET, + "BUG: total_stealth_input_amount or params.total_amount() are negative after validation" + ); + panic!("BUG: total_stealth_input_amount or params.total_amount() are negative after validation"); + }); + + if change_amount.is_zero() { + return Ok(None); + } + + let change_public_key = RistrettoPublicKey::try_from_byte_type(&account.owner_public_key).map_err(|e| { + StealthTransferApiError::InvalidParameter { + param: "owner_public_key", + reason: format!("Invalid owner public key: {e}"), + } + })?; + + let change = self.create_output_statement(&change_public_key, change_amount, resource_view_key)?; + + self.add_unconfirmed_output_from_statement(lock_id, account, resource_address, &change)?; + + Ok(Some(change)) + } + + fn add_unconfirmed_output_from_statement( + &self, + lock_id: WalletLockId, + account: &AccountWithPublicKey, + resource_address: ResourceAddress, + output: &UnblindedStealthOutputStatement, + ) -> Result<(), StealthTransferApiError> { + let output_value = output.statement.amount; + if output_value.is_zero() { + return Ok(()); + } + + self.outputs_api.add_output(&StealthOutputModel { + owner_account: *account.address(), + resource_address, + commitment: output + .statement + .to_commitment() + .expect("BUG: to_commitment negative amount") + .to_byte_type(), + value: output_value, + sender_public_nonce: output.statement.sender_public_nonce.to_byte_type(), + encryption_secret_key_index: account.key_index(), + encrypted_data: output.statement.encrypted_data.clone(), + status: OutputStatus::LockedUnconfirmed, + tag_byte: output.tag, + lock_id: Some(lock_id), + is_burnt: false, + is_frozen: false, + is_on_chain: false, + })?; + Ok(()) + } + fn create_output_statement( &self, dest_public_key: &RistrettoPublicKey, @@ -678,7 +782,9 @@ where )?; // Create stealth address - used during spend time - let output_owner_public_key = kdfs::owner_stealth_dh_stealth_address(network, dest_public_key, &nonce_secret); + let output_owner_public_key = + self.crypto_api + .derive_stealth_owner_public_key(network, dest_public_key, &nonce_secret); let derived_tag = self .crypto_api @@ -701,7 +807,7 @@ where pub struct TransferOutput { pub transaction: Transaction, - pub transaction_lock_id: Option, + pub transaction_lock_id: Option, } #[derive(Debug)] @@ -759,20 +865,35 @@ impl StealthTransferParams { } } +#[derive(Debug)] +pub struct InputToSpend { + pub statement: UnblindedStealthInputStatement, + pub is_on_chain: bool, +} + +impl InputToSpend { + pub fn value(&self) -> Amount { + self.statement.mask_and_value.value + } +} + #[derive(Debug)] pub struct InputsToSpend { - pub inputs: Vec, - pub lock_id: OutputLockId, + pub inputs: Vec, pub revealed: Amount, } impl InputsToSpend { + pub fn statements_iter(&self) -> impl Iterator + '_ { + self.inputs.iter().map(|i| &i.statement) + } + pub fn total_amount(&self) -> Amount { self.total_stealth_input_amount() + self.revealed } pub fn total_stealth_input_amount(&self) -> Amount { - self.inputs.iter().map(|i| i.mask_and_value.value).sum() + self.inputs.iter().map(|i| i.statement.mask_and_value.value).sum() } } diff --git a/crates/wallet/sdk/src/apis/transaction.rs b/crates/wallet/sdk/src/apis/transaction.rs index 0a2faa7601..edfd7f0dc9 100644 --- a/crates/wallet/sdk/src/apis/transaction.rs +++ b/crates/wallet/sdk/src/apis/transaction.rs @@ -12,7 +12,10 @@ use tari_ootle_common_types::{ optional::{IsNotFoundError, Optional}, VersionedSubstateIdRef, }; -use tari_template_lib::{constants::XTR, prelude::ComponentAddress}; +use tari_template_lib::{ + constants::XTR, + prelude::{ComponentAddress, RistrettoPublicKeyBytes}, +}; use tari_transaction::{Transaction, TransactionId}; use crate::{ @@ -60,7 +63,7 @@ where Ok(tx_id) } - pub async fn submit_transaction(&self, transaction_id: TransactionId) -> Result<(), TransactionApiError> { + pub async fn submit_transaction(&self, transaction_id: TransactionId) -> Result { let transaction = self.store.with_read_tx(|tx| tx.transactions_get(transaction_id))?; if !matches!(transaction.status, TransactionStatus::New) { @@ -90,6 +93,7 @@ where .with_invalid_reason(&message), ) })?; + return Ok(false); }, _ => { return Err(err.into()); @@ -97,7 +101,7 @@ where }, } - Ok(()) + Ok(true) } pub async fn submit_dry_run_transaction( @@ -158,9 +162,10 @@ where &self, status: Option, component: Option, + signed_by_public_key: Option, ) -> Result, TransactionApiError> { let mut tx = self.store.create_read_tx()?; - let transactions = tx.transactions_fetch_all(status, component)?; + let transactions = tx.transactions_fetch_all(status, component, signed_by_public_key)?; Ok(transactions) } @@ -221,7 +226,7 @@ where if !transaction.is_dry_run && final_decision.is_commit() { let diff = execution_result .as_ref() - .and_then(|e| e.finalize.result.accept()) + .and_then(|e| e.finalize.result.any_accept()) .ok_or_else(|| TransactionApiError::InvalidTransactionQueryResponse { details: format!( "NEVERHAPPEN: Finalize decision is COMMIT but transaction failed: {:?}", @@ -249,14 +254,23 @@ where // we should make sure that the account's locked outputs // are either set to spent or released, depending if the // transaction was finalized or rejected. Always release for dry runs. - if transaction.is_dry_run || new_status != TransactionStatus::Accepted { - self.release_all_outputs_for_transaction_internal(tx, transaction_id)?; + if transaction.is_dry_run || + !matches!( + new_status, + TransactionStatus::Accepted | TransactionStatus::OnlyFeeAccepted + ) + { + self.release_all_locks_for_transaction_internal(tx, transaction_id)?; } else { - let lock_ids = tx.output_locks_get_by_transaction_id(transaction_id)?; + // TODO: it becomes more complicated if the transaction is Fee accepted, we'll need to finalize + // spends relating to fees and release the rest + let lock_ids = tx.locks_get_by_transaction_id(transaction_id)?; + info!(target: LOG_TARGET, "Finalizing locked outputs for transaction {}: {:?}", transaction_id, lock_ids); for lock_id in lock_ids { tx.outputs_finalize_by_lock_id(lock_id)?; tx.stealth_outputs_finalize_by_lock_id(lock_id)?; - tx.vaults_finalized_locked_revealed_funds(lock_id)?; + tx.vaults_finalized_locked_revealed_funds(lock_id).optional()?; + tx.locks_delete(lock_id)?; } } @@ -269,20 +283,17 @@ where } } - pub fn release_all_outputs_for_transaction( - &self, - transaction_id: TransactionId, - ) -> Result<(), TransactionApiError> { + pub fn release_all_locks_for_transaction(&self, transaction_id: TransactionId) -> Result<(), TransactionApiError> { self.store - .with_write_tx(|tx| self.release_all_outputs_for_transaction_internal(tx, transaction_id)) + .with_write_tx(|tx| self.release_all_locks_for_transaction_internal(tx, transaction_id)) } - fn release_all_outputs_for_transaction_internal( + fn release_all_locks_for_transaction_internal( &self, tx: &mut ::WriteTransaction<'_>, transaction_id: TransactionId, ) -> Result<(), TransactionApiError> { - let lock_ids = tx.output_locks_get_by_transaction_id(transaction_id)?; + let lock_ids = tx.locks_get_by_transaction_id(transaction_id)?; debug!(target: LOG_TARGET, "Releasing {} locks (and associated outputs) for transaction {} that was not committed", lock_ids.len(), transaction_id); for lock_id in lock_ids { @@ -290,7 +301,8 @@ where tx.outputs_release_by_lock_id(lock_id)?; tx.stealth_outputs_release_by_lock_id(lock_id)?; // If the lock locks a vault, we need to release the revealed funds - tx.vaults_unlock_revealed_funds(lock_id)?; + tx.vaults_release_lock_revealed_funds(lock_id).optional()?; + tx.locks_delete(lock_id)?; } Ok(()) diff --git a/crates/wallet/sdk/src/models/account.rs b/crates/wallet/sdk/src/models/account.rs index 12b27e55ac..032beb3f72 100644 --- a/crates/wallet/sdk/src/models/account.rs +++ b/crates/wallet/sdk/src/models/account.rs @@ -4,6 +4,8 @@ use std::fmt::{Display, Formatter}; use tari_bor::{Deserialize, Serialize}; +use tari_crypto::ristretto::RistrettoPublicKey; +use tari_engine_types::FromByteType; use tari_template_lib::{models::ComponentAddress, prelude::RistrettoPublicKeyBytes}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -17,6 +19,28 @@ pub struct Account { pub is_default: bool, } +impl Account { + pub fn address(&self) -> &ComponentAddress { + &self.address + } + + pub fn key_index(&self) -> u64 { + self.key_index + } + + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + pub fn is_confirmed_on_chain(&self) -> bool { + self.is_confirmed_on_chain + } + + pub fn is_default(&self) -> bool { + self.is_default + } +} + impl Display for Account { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.name { @@ -50,6 +74,11 @@ impl AccountWithPublicKey { &self.owner_public_key } + pub fn to_ristretto_public_key(&self) -> RistrettoPublicKey { + RistrettoPublicKey::try_from_byte_type(&self.owner_public_key) + .expect("BUG: Malformed public key bytes in account") + } + pub fn is_confirmed_on_chain(&self) -> bool { self.account.is_confirmed_on_chain } diff --git a/crates/wallet/sdk/src/models/confidential_output.rs b/crates/wallet/sdk/src/models/confidential_output.rs index 88513cb8a1..88c9b5fccb 100644 --- a/crates/wallet/sdk/src/models/confidential_output.rs +++ b/crates/wallet/sdk/src/models/confidential_output.rs @@ -9,7 +9,7 @@ use tari_template_lib::{ types::Amount, }; -use crate::models::OutputLockId; +use crate::models::WalletLockId; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ConfidentialOutputModel { @@ -22,7 +22,7 @@ pub struct ConfidentialOutputModel { pub encrypted_data: EncryptedData, pub public_asset_tag: Option, pub status: OutputStatus, - pub lock_id: Option, + pub lock_id: Option, } #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] diff --git a/crates/wallet/sdk/src/models/key.rs b/crates/wallet/sdk/src/models/key.rs index 4829598f84..47435e1962 100644 --- a/crates/wallet/sdk/src/models/key.rs +++ b/crates/wallet/sdk/src/models/key.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_crypto::ristretto::RistrettoPublicKey; +use tari_crypto::ristretto::{RistrettoPublicKey, RistrettoSecretKey}; use tari_key_manager::key_manager::DerivedKey; #[derive(Clone)] @@ -31,4 +31,12 @@ impl KeyPair { pub fn key_index(&self) -> u64 { self.secret_key.key_index } + + pub fn public_key(&self) -> &RistrettoPublicKey { + &self.public_key + } + + pub fn secret_key(&self) -> &RistrettoSecretKey { + &self.secret_key.key + } } diff --git a/crates/wallet/sdk/src/models/mod.rs b/crates/wallet/sdk/src/models/mod.rs index c12b01895b..f85c0e737f 100644 --- a/crates/wallet/sdk/src/models/mod.rs +++ b/crates/wallet/sdk/src/models/mod.rs @@ -27,4 +27,4 @@ pub use vault::*; pub use wallet_transaction::*; pub use webauthn_registration::*; -pub type OutputLockId = u64; +pub type WalletLockId = i32; diff --git a/crates/wallet/sdk/src/models/stealth_output.rs b/crates/wallet/sdk/src/models/stealth_output.rs index c88778cd48..e09c81231d 100644 --- a/crates/wallet/sdk/src/models/stealth_output.rs +++ b/crates/wallet/sdk/src/models/stealth_output.rs @@ -8,7 +8,7 @@ use tari_template_lib::{ types::{crypto::UtxoTagByte, Amount}, }; -use crate::models::{OutputLockId, OutputStatus}; +use crate::models::{OutputStatus, WalletLockId}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct StealthOutputModel { @@ -23,7 +23,8 @@ pub struct StealthOutputModel { pub status: OutputStatus, pub is_burnt: bool, pub is_frozen: bool, - pub lock_id: Option, + pub is_on_chain: bool, + pub lock_id: Option, } impl StealthOutputModel { diff --git a/crates/wallet/sdk/src/storage.rs b/crates/wallet/sdk/src/storage.rs index c852ad0df6..ce16e49e7f 100644 --- a/crates/wallet/sdk/src/storage.rs +++ b/crates/wallet/sdk/src/storage.rs @@ -37,7 +37,6 @@ use crate::models::{ Config, NewAccountData, NonFungibleToken, - OutputLockId, OutputStatus, ResourceModel, StealthBalance, @@ -45,6 +44,7 @@ use crate::models::{ SubstateModel, TransactionStatus, VaultModel, + WalletLockId, WalletTransaction, WalletTransactionUpdate, }; @@ -156,6 +156,7 @@ pub trait WalletStoreReader { &mut self, status: Option, component: Option, + signed_by_public_key: Option, ) -> Result, WalletStorageError>; // Substates fn substates_get(&mut self, address: &SubstateId) -> Result; @@ -202,7 +203,7 @@ pub trait WalletStoreReader { fn outputs_get_unspent_balance(&mut self, vault_id: &VaultId) -> Result; fn outputs_get_locked_by_lock_id( &mut self, - lock_id: OutputLockId, + lock_id: WalletLockId, ) -> Result, WalletStorageError>; fn outputs_get_by_commitment( &mut self, @@ -229,7 +230,7 @@ pub trait WalletStoreReader { fn stealth_outputs_get_locked_by_lock_id( &mut self, - lock_id: OutputLockId, + lock_id: WalletLockId, ) -> Result, WalletStorageError>; fn stealth_outputs_get_by_commitment( &mut self, @@ -238,10 +239,10 @@ pub trait WalletStoreReader { ) -> Result; // Output Locks - fn output_locks_get_by_transaction_id( + fn locks_get_by_transaction_id( &mut self, transaction_id: TransactionId, - ) -> Result, WalletStorageError>; + ) -> Result, WalletStorageError>; // Non fungible tokens fn non_fungible_token_get_by_nft_id( @@ -370,37 +371,39 @@ pub trait WalletStoreWriter { ) -> Result<(), WalletStorageError>; fn vaults_lock_revealed_funds( &mut self, - lock_id: OutputLockId, + lock_id: WalletLockId, + vault_id: &VaultId, amount_to_lock: Amount, ) -> Result<(), WalletStorageError>; - fn vaults_finalized_locked_revealed_funds(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError>; - fn vaults_unlock_revealed_funds(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError>; + fn vaults_finalized_locked_revealed_funds(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; + fn vaults_release_lock_revealed_funds(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; // Resources fn resources_upsert(&mut self, address: &ResourceAddress, resource: &Resource) -> Result<(), WalletStorageError>; // Confidential Outputs fn outputs_lock_smallest_amount( &mut self, vault_id: &VaultId, - lock_id: OutputLockId, + lock_id: WalletLockId, ) -> Result; fn outputs_insert(&mut self, output: ConfidentialOutputModel) -> Result<(), WalletStorageError>; /// Mark outputs as finalized - fn outputs_finalize_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError>; + fn outputs_finalize_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; /// Release outputs that were locked and remove pending unconfirmed outputs for this proof - fn outputs_release_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError>; + fn outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; // Stealth Outputs fn stealth_outputs_lock_smallest_amount( &mut self, account_addr: &ComponentAddress, - lock_id: OutputLockId, + resource_address: &ResourceAddress, + lock_id: WalletLockId, ) -> Result; fn stealth_outputs_insert(&mut self, output: &StealthOutputModel) -> Result<(), WalletStorageError>; fn stealth_outputs_mark_as_spent(&mut self, address: &UtxoAddress) -> Result<(), WalletStorageError>; /// Mark outputs locked by this lock id as finalized - fn stealth_outputs_finalize_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError>; + fn stealth_outputs_finalize_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; /// Release outputs that were locked and remove pending unconfirmed outputs for this lock - fn stealth_outputs_release_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError>; + fn stealth_outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; fn stealth_outputs_update( &mut self, address: &UtxoAddress, @@ -409,15 +412,14 @@ pub trait WalletStoreWriter { is_frozen: Option, ) -> Result<(), WalletStorageError>; - // Output locks - fn output_locks_insert(&mut self, resource_address: &ResourceAddress) -> Result; - fn output_locks_insert_for_vault(&mut self, vault_id: &VaultId) -> Result; - fn output_locks_delete(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError>; - fn output_locks_set_params( + // Locks + fn locks_create(&mut self) -> Result; + + fn locks_delete(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError>; + fn locks_link_transaction( &mut self, - lock_id: OutputLockId, - transaction_id: Option, - vault_id: Option, + lock_id: WalletLockId, + transaction_id: TransactionId, ) -> Result<(), WalletStorageError>; // Non fungible tokens diff --git a/crates/wallet/sdk/tests/confidential_output_api.rs b/crates/wallet/sdk/tests/confidential_output_api.rs index 477a5600b6..567fd8d585 100644 --- a/crates/wallet/sdk/tests/confidential_output_api.rs +++ b/crates/wallet/sdk/tests/confidential_output_api.rs @@ -15,7 +15,7 @@ use tari_engine_types::{ }; use tari_ootle_common_types::{optional::Optional, shard::Shard, Network, StateVersion, UtxoQueryResponse}; use tari_ootle_wallet_sdk::{ - models::{ConfidentialOutputModel, OutputLockId, OutputStatus}, + models::{ConfidentialOutputModel, OutputStatus, WalletLockId}, network::{SubstateQueryResult, TransactionQueryResult, WalletNetworkInterface}, storage::{WalletStore, WalletStoreReader}, WalletSdk, @@ -24,7 +24,7 @@ use tari_ootle_wallet_sdk::{ use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; use tari_template_abi::TemplateDef; use tari_template_lib::{ - constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + constants::STEALTH_TARI_RESOURCE_ADDRESS, models::{ComponentAddress, EncryptedData, ResourceAddress, VaultId}, prelude::crypto::UtxoTagByte, resource::ResourceType, @@ -40,18 +40,18 @@ fn outputs_locked_and_released() { let commitment_49 = test.add_unspent_output(49); let _commitment_100 = test.add_unspent_output(100); - let proof_id = test.new_proof(); + let lock_id = test.new_lock(); let (inputs, total_value) = test .sdk() .confidential_outputs_api() - .lock_outputs_by_amount(proof_id, &Test::test_vault_address(), 50) + .lock_outputs_by_amount(lock_id, &Test::test_vault_address(), 50) .unwrap(); assert_eq!(total_value, 74); assert_eq!(inputs.len(), 2); let locked = test .store() - .with_read_tx(|tx| tx.outputs_get_locked_by_lock_id(proof_id)) + .with_read_tx(|tx| tx.outputs_get_locked_by_lock_id(lock_id)) .unwrap(); assert!(locked.iter().any(|l| l.commitment == commitment_25)); @@ -60,12 +60,12 @@ fn outputs_locked_and_released() { test.sdk .confidential_outputs_api() - .release_locked_outputs(proof_id) + .release_locked_outputs(lock_id) .unwrap(); let locked = test .store() - .with_read_tx(|tx| tx.outputs_get_locked_by_lock_id(proof_id)) + .with_read_tx(|tx| tx.outputs_get_locked_by_lock_id(lock_id)) .unwrap(); assert_eq!(locked.len(), 0); } @@ -79,7 +79,7 @@ fn outputs_locked_and_finalized() { let commitment_100 = test.add_unspent_output(100); let outputs_api = test.sdk().confidential_outputs_api(); - let proof_id = test.new_proof(); + let proof_id = test.new_lock(); let (inputs, total_value) = outputs_api .lock_outputs_by_amount(proof_id, &Test::test_vault_address(), 50) @@ -164,8 +164,8 @@ impl Test { .add_vault( Test::test_account_address(), Test::test_vault_address(), - CONFIDENTIAL_TARI_RESOURCE_ADDRESS, - ResourceType::Confidential, + STEALTH_TARI_RESOURCE_ADDRESS, + ResourceType::Stealth, Some("TEST".to_string()), 6, ) @@ -214,9 +214,8 @@ impl Test { commitment } - pub fn new_proof(&self) -> OutputLockId { - let outputs_api = self.sdk.confidential_outputs_api(); - outputs_api.add_output_lock(&Self::test_vault_address()).unwrap() + pub fn new_lock(&self) -> WalletLockId { + self.sdk.confidential_outputs_api().create_lock().unwrap() } pub fn get_unspent_balance(&self) -> Amount { diff --git a/crates/wallet/storage_sqlite/Cargo.toml b/crates/wallet/storage_sqlite/Cargo.toml index 103e513a81..8392d2a386 100644 --- a/crates/wallet/storage_sqlite/Cargo.toml +++ b/crates/wallet/storage_sqlite/Cargo.toml @@ -9,7 +9,6 @@ license.workspace = true [dependencies] tari_bor = { workspace = true, features = ["json_encoding"] } -tari_common_types = { workspace = true } tari_ootle_common_types = { workspace = true } tari_ootle_wallet_sdk = { workspace = true } tari_engine_types = { workspace = true } @@ -32,6 +31,7 @@ libsqlite3-sys = { workspace = true, features = ["bundled"] } [dev-dependencies] tari_ootle_common_types = { workspace = true } +tari_crypto = { workspace = true } [package.metadata.cargo-machete] ignored = [ diff --git a/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql b/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql index bc0a082f38..fe5899ce99 100644 --- a/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql +++ b/crates/wallet/storage_sqlite/migrations/2023-02-08-122514_initial/up.sql @@ -30,31 +30,25 @@ CREATE UNIQUE INDEX config_uniq_key on config (key); -- Transaction CREATE TABLE transactions ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - hash TEXT NOT NULL, - network INTEGER NOT NULL, - instructions TEXT NOT NULL, - fee_instructions TEXT NOT NULL, - inputs TEXT NOT NULL, - signatures TEXT NOT NULL, - seal_signature TEXT NOT NULL, - is_seal_signer_authorized BOOLEAN NOT NULL, - result TEXT NULL, - qcs TEXT NULL, - final_fee BIGINT NULL, - status TEXT NOT NULL, - dry_run BOOLEAN NOT NULL, - min_epoch BIGINT NULL, - max_epoch BIGINT NULL, - executed_time_ms BIGINT NULL, - finalized_time DATETIME NULL, - new_account_info TEXT NULL, - invalid_reason TEXT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + transaction_id TEXT NOT NULL, + transaction_json TEXT NOT NULL, + referenced_components TEXT NOT NULL, + signers TEXT NOT NULL, + result TEXT NULL, + qcs TEXT NULL, + final_fee BIGINT NULL, + status TEXT NOT NULL, + dry_run BOOLEAN NOT NULL, + executed_time_ms BIGINT NULL, + finalized_time DATETIME NULL, + new_account_info TEXT NULL, + invalid_reason TEXT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE UNIQUE INDEX transactions_uniq_hash ON transactions (hash); +CREATE UNIQUE INDEX transactions_transaction_id_uniq ON transactions (transaction_id); CREATE INDEX transactions_idx_status ON transactions (status); -- Substates @@ -102,6 +96,7 @@ CREATE TABLE vaults locked_revealed_balance BIGINT NOT NULL DEFAULT 0, token_symbol TEXT NULL, divisibility INTEGER NOT NULL DEFAULT 0, + locked_by INTEGER NULL REFERENCES locks (id) ON DELETE SET NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -153,15 +148,12 @@ CREATE TABLE outputs CREATE UNIQUE INDEX outputs_uniq_commitment ON outputs (commitment); CREATE INDEX outputs_idx_account_status ON outputs (account_id, status); --- Output Locks -CREATE TABLE output_locks +-- Locks +CREATE TABLE locks ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - resource_address TEXT NOT NULL, - vault_id INTEGER NULL REFERENCES vaults (id), - transaction_hash TEXT NULL, - locked_revealed_amount BIGINT NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + transaction_id TEXT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Auth token, we don't store the auth token, the token in this table is the jwt token that is granted when user accepts the auth login request. @@ -238,6 +230,7 @@ CREATE TABLE stealth_outputs tag_byte INTEGER NOT NULL, is_burnt BOOLEAN NOT NULL DEFAULT 0, is_frozen BOOLEAN NOT NULL DEFAULT 0, + is_on_chain BOOLEAN NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/crates/wallet/storage_sqlite/src/models/mod.rs b/crates/wallet/storage_sqlite/src/models/mod.rs index 9c27812924..1769874bb4 100644 --- a/crates/wallet/storage_sqlite/src/models/mod.rs +++ b/crates/wallet/storage_sqlite/src/models/mod.rs @@ -15,7 +15,6 @@ mod vault; mod authored_template; mod non_fungible_tokens; -mod proof; mod resource; mod stealth_output; mod webauthn_registrations; @@ -25,11 +24,9 @@ pub use authored_template::AuthoredTemplate; pub use config::Config; pub use non_fungible_tokens::NonFungibleToken; pub use output::ConfidentialOutput; -// Currently only used internally -pub(crate) use proof::OutputLock; pub use resource::*; pub use stealth_output::*; pub use substate::Substate; -pub use transaction::Transaction; +pub use transaction::TransactionRecord; pub use vault::Vault; pub use webauthn_registrations::*; diff --git a/crates/wallet/storage_sqlite/src/models/output.rs b/crates/wallet/storage_sqlite/src/models/output.rs index 0fd50056e6..1f6111e890 100644 --- a/crates/wallet/storage_sqlite/src/models/output.rs +++ b/crates/wallet/storage_sqlite/src/models/output.rs @@ -76,7 +76,7 @@ impl ConfidentialOutput { item: "output", details: format!("Corrupt db: invalid output status '{}'", self.status), })?, - lock_id: self.locked_by_proof.map(|proof| proof as u64), + lock_id: self.locked_by_proof, }) } } diff --git a/crates/wallet/storage_sqlite/src/models/proof.rs b/crates/wallet/storage_sqlite/src/models/proof.rs deleted file mode 100644 index 15bbc677f1..0000000000 --- a/crates/wallet/storage_sqlite/src/models/proof.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2023 The Tari Project -// SPDX-License-Identifier: BSD-3-Clause - -use diesel::{Identifiable, Queryable}; -use time::PrimitiveDateTime; - -use crate::schema::output_locks; - -#[derive(Debug, Clone, Queryable, Identifiable)] -#[diesel(table_name = output_locks)] -pub struct OutputLock { - pub id: i32, - pub resource_address: String, - pub vault_id: Option, - pub transaction_hash: Option, - pub locked_revealed_amount: i64, - pub created_at: PrimitiveDateTime, -} diff --git a/crates/wallet/storage_sqlite/src/models/stealth_output.rs b/crates/wallet/storage_sqlite/src/models/stealth_output.rs index 3cf29516ae..645c1980b6 100644 --- a/crates/wallet/storage_sqlite/src/models/stealth_output.rs +++ b/crates/wallet/storage_sqlite/src/models/stealth_output.rs @@ -28,6 +28,7 @@ pub struct StealthOutput { pub tag_byte: i32, pub is_burnt: bool, pub is_frozen: bool, + pub is_on_chain: bool, pub created_at: PrimitiveDateTime, pub updated_at: PrimitiveDateTime, } @@ -84,7 +85,8 @@ impl StealthOutput { })?, is_burnt: self.is_burnt, is_frozen: self.is_frozen, - lock_id: self.locked_by_proof.map(|proof| proof as u64), + is_on_chain: self.is_on_chain, + lock_id: self.locked_by_proof, }) } } diff --git a/crates/wallet/storage_sqlite/src/models/transaction.rs b/crates/wallet/storage_sqlite/src/models/transaction.rs index 8b4d805b45..71f6e92d62 100644 --- a/crates/wallet/storage_sqlite/src/models/transaction.rs +++ b/crates/wallet/storage_sqlite/src/models/transaction.rs @@ -3,13 +3,14 @@ use std::{str::FromStr, time::Duration}; +use diesel::Identifiable; use log::*; -use tari_ootle_common_types::{displayable::Displayable, Epoch}; +use tari_ootle_common_types::displayable::Displayable; use tari_ootle_wallet_sdk::{ models::{TransactionStatus, WalletTransaction}, storage::WalletStorageError, }; -use tari_transaction::{UnsealedTransactionV1, UnsignedTransactionV1}; +use tari_transaction::Transaction; use time::PrimitiveDateTime; use crate::{ @@ -21,23 +22,17 @@ const LOG_TARGET: &str = "tari::ootle::wallet::storage_sqlite::models::transacti #[derive(Debug, Clone, Queryable, Identifiable)] #[diesel(table_name = transactions)] -pub struct Transaction { +pub struct TransactionRecord { pub id: i32, - pub hash: String, - pub network: i32, - pub instructions: String, - pub fee_instructions: String, - pub inputs: String, - pub signatures: String, - pub seal_signature: String, - pub is_seal_signer_authorized: bool, + pub transaction_id: String, + pub transaction_json: String, + pub _referenced_components: String, + pub _signers: String, pub result: Option, pub qcs: Option, pub final_fee: Option, pub status: String, - pub is_dry_run: bool, - pub min_epoch: Option, - pub max_epoch: Option, + pub dry_run: bool, pub executed_time_ms: Option, pub finalized_time: Option, pub new_account_info: Option, @@ -46,34 +41,14 @@ pub struct Transaction { pub updated_at: PrimitiveDateTime, } -impl Transaction { +impl TransactionRecord { pub fn try_into_wallet_transaction(self) -> Result { - let signatures = deserialize_json(&self.signatures)?; - let inputs = deserialize_json(&self.inputs)?; - let seal_signature = deserialize_json(&self.seal_signature)?; + let transaction = deserialize_json::(&self.transaction_json)?; + let is_dry_run = transaction.is_dry_run(); Ok(WalletTransaction { - id: deserialize_hex_try_from(&self.hash)?, - transaction: tari_transaction::Transaction::new( - UnsealedTransactionV1::new( - UnsignedTransactionV1 { - network: self.network.try_into().map_err(|_| WalletStorageError::DecodingError { - operation: "transaction_get", - item: "network", - details: format!("Invalid network value {}", self.network), - })?, - fee_instructions: deserialize_json(&self.fee_instructions)?, - instructions: deserialize_json(&self.instructions)?, - inputs, - min_epoch: self.min_epoch.map(|epoch| Epoch(epoch as u64)), - max_epoch: self.max_epoch.map(|epoch| Epoch(epoch as u64)), - is_seal_signer_authorized: true, - dry_run: self.is_dry_run, - }, - signatures, - ), - seal_signature, - ), + id: deserialize_hex_try_from(&self.transaction_id)?, + transaction, status: TransactionStatus::from_str(&self.status).map_err(|e| WalletStorageError::DecodingError { operation: "transaction_get", item: "status", @@ -84,7 +59,7 @@ impl Transaction { qcs: self.qcs.map(|q| deserialize_json(&q)).transpose()?.unwrap_or_default(), new_account_info: self.new_account_info.as_deref().map(deserialize_json).transpose()?, invalid_reason: self.invalid_reason, - is_dry_run: self.is_dry_run, + is_dry_run, execution_time: self .executed_time_ms .map(|t| u64::try_from(t).map(Duration::from_millis).unwrap_or_default()), diff --git a/crates/wallet/storage_sqlite/src/models/vault.rs b/crates/wallet/storage_sqlite/src/models/vault.rs index 6c3a0a4d1a..a9e57b7361 100644 --- a/crates/wallet/storage_sqlite/src/models/vault.rs +++ b/crates/wallet/storage_sqlite/src/models/vault.rs @@ -27,6 +27,7 @@ pub struct Vault { pub locked_revealed_balance: i64, pub token_symbol: Option, pub divisibility: i32, + pub _locked_by: Option, pub created_at: PrimitiveDateTime, pub updated_at: PrimitiveDateTime, } diff --git a/crates/wallet/storage_sqlite/src/reader.rs b/crates/wallet/storage_sqlite/src/reader.rs index 3b78a16291..da85a6dc97 100644 --- a/crates/wallet/storage_sqlite/src/reader.rs +++ b/crates/wallet/storage_sqlite/src/reader.rs @@ -35,7 +35,6 @@ use tari_ootle_wallet_sdk::{ ConfidentialOutputModel, Config, NonFungibleToken, - OutputLockId, OutputStatus, ResourceModel, StealthBalance, @@ -43,6 +42,7 @@ use tari_ootle_wallet_sdk::{ SubstateModel, TransactionStatus, VaultModel, + WalletLockId, WalletTransaction, WebauthnRegistrationPasskeyModel, }, @@ -224,8 +224,8 @@ impl WalletStoreReader for ReadTransaction<'_> { fn transactions_get(&mut self, transaction_id: TransactionId) -> Result { use crate::schema::transactions; let row = transactions::table - .filter(transactions::hash.eq(transaction_id.to_string())) - .first::(self.connection()) + .filter(transactions::transaction_id.eq(serialize_hex(transaction_id))) + .first::(self.connection()) .optional() .map_err(|e| WalletStorageError::general("transaction_get", e))? .ok_or_else(|| WalletStorageError::NotFound { @@ -242,23 +242,32 @@ impl WalletStoreReader for ReadTransaction<'_> { &mut self, status: Option, component: Option, + signed_by_public_key: Option, ) -> Result, WalletStorageError> { use crate::schema::transactions; - let mut rows = transactions::table.into_boxed().filter(transactions::dry_run.eq(false)); + let mut query = transactions::table.into_boxed().filter(transactions::dry_run.eq(false)); if let Some(status) = status { - rows = rows.filter(transactions::status.eq(status.as_key_str())); + query = query.filter(transactions::status.eq(status.as_key_str())); } if let Some(component) = component { - rows = rows.filter( - transactions::instructions - .like(format!("%{}%", component)) - .or(transactions::fee_instructions.like(format!("%{}%", component))), - ); + if let Some(public_key) = signed_by_public_key { + query = query.filter( + transactions::referenced_components + .like(format!("%{}%", component)) + .or(transactions::signers.like(format!("%{}%", serialize_hex(public_key)))), + ); + } else { + query = query.filter(transactions::referenced_components.like(format!("%{}%", component))); + } + } else if let Some(public_key) = signed_by_public_key { + query = query.filter(transactions::signers.like(format!("%{}%", serialize_hex(public_key)))); + } else { + // No filter } - let rows = rows + let rows = query .order(transactions::created_at.desc()) - .load::(self.connection()) + .load::(self.connection()) .map_err(|e| WalletStorageError::general("transactions_fetch_all", e))?; rows.into_iter().map(|row| row.try_into_wallet_transaction()).collect() @@ -656,13 +665,13 @@ impl WalletStoreReader for ReadTransaction<'_> { fn outputs_get_locked_by_lock_id( &mut self, - lock_id: OutputLockId, + lock_id: WalletLockId, ) -> Result, WalletStorageError> { const OPERATION: &str = "outputs_get_locked_by_lock_id"; use crate::schema::{accounts, outputs, vaults}; let rows = outputs::table - .filter(outputs::lock_id.eq(lock_id as i32)) + .filter(outputs::lock_id.eq(lock_id)) .get_results::(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; @@ -843,13 +852,13 @@ impl WalletStoreReader for ReadTransaction<'_> { fn stealth_outputs_get_locked_by_lock_id( &mut self, - lock_id: OutputLockId, + lock_id: WalletLockId, ) -> Result, WalletStorageError> { const OPERATION: &str = "stealth_outputs_get_locked_by_lock_id"; use crate::schema::{accounts, stealth_outputs}; let rows = stealth_outputs::table - .filter(stealth_outputs::lock_id.eq(lock_id as i32)) + .filter(stealth_outputs::lock_id.eq(lock_id)) .get_results::(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; @@ -911,19 +920,19 @@ impl WalletStoreReader for ReadTransaction<'_> { row.try_convert(account_address) } - fn output_locks_get_by_transaction_id( + fn locks_get_by_transaction_id( &mut self, transaction_id: TransactionId, - ) -> Result, WalletStorageError> { - use crate::schema::output_locks; + ) -> Result, WalletStorageError> { + use crate::schema::locks; - let lock_ids = output_locks::table - .filter(output_locks::transaction_hash.eq(transaction_id.to_string())) - .select(output_locks::id) + let lock_ids = locks::table + .filter(locks::transaction_id.eq(serialize_hex(transaction_id))) + .select(locks::id) .get_results::(self.connection()) - .map_err(|e| WalletStorageError::general("output_locks_get_by_transaction_id", e))?; + .map_err(|e| WalletStorageError::general("locks_get_by_transaction_id", e))?; - Ok(lock_ids.into_iter().map(|id| id as u64).collect()) + Ok(lock_ids) } fn non_fungible_token_get_by_nft_id( diff --git a/crates/wallet/storage_sqlite/src/schema.rs b/crates/wallet/storage_sqlite/src/schema.rs index a77224be34..9cf4f9e2e2 100644 --- a/crates/wallet/storage_sqlite/src/schema.rs +++ b/crates/wallet/storage_sqlite/src/schema.rs @@ -59,6 +59,14 @@ diesel::table! { } } +diesel::table! { + locks (id) { + id -> Integer, + transaction_id -> Nullable, + created_at -> Timestamp, + } +} + diesel::table! { non_fungible_tokens (id) { id -> Integer, @@ -73,17 +81,6 @@ diesel::table! { } } -diesel::table! { - output_locks (id) { - id -> Integer, - resource_address -> Text, - vault_id -> Nullable, - transaction_hash -> Nullable, - locked_revealed_amount -> BigInt, - created_at -> Timestamp, - } -} - diesel::table! { outputs (id) { id -> Integer, @@ -150,6 +147,7 @@ diesel::table! { tag_byte -> Integer, is_burnt -> Bool, is_frozen -> Bool, + is_on_chain -> Bool, created_at -> Timestamp, updated_at -> Timestamp, } @@ -171,21 +169,15 @@ diesel::table! { diesel::table! { transactions (id) { id -> Integer, - hash -> Text, - network -> Integer, - instructions -> Text, - fee_instructions -> Text, - inputs -> Text, - signatures -> Text, - seal_signature -> Text, - is_seal_signer_authorized -> Bool, + transaction_id -> Text, + transaction_json -> Text, + referenced_components -> Text, + signers -> Text, result -> Nullable, qcs -> Nullable, final_fee -> Nullable, status -> Text, dry_run -> Bool, - min_epoch -> Nullable, - max_epoch -> Nullable, executed_time_ms -> Nullable, finalized_time -> Nullable, new_account_info -> Nullable, @@ -207,6 +199,7 @@ diesel::table! { locked_revealed_balance -> BigInt, token_symbol -> Nullable, divisibility -> Integer, + locked_by -> Nullable, created_at -> Timestamp, updated_at -> Timestamp, } @@ -232,12 +225,12 @@ diesel::table! { } diesel::joinable!(non_fungible_tokens -> vaults (vault_id)); -diesel::joinable!(output_locks -> vaults (vault_id)); diesel::joinable!(outputs -> accounts (account_id)); diesel::joinable!(outputs -> vaults (vault_id)); diesel::joinable!(shard_state_versions -> accounts (account_id)); diesel::joinable!(shard_state_versions -> resources (resource_id)); diesel::joinable!(vaults -> accounts (account_id)); +diesel::joinable!(vaults -> locks (locked_by)); diesel::joinable!(webauthn_registration_passkeys -> webauthn_registrations (registration_id)); diesel::allow_tables_to_appear_in_same_query!( @@ -246,8 +239,8 @@ diesel::allow_tables_to_appear_in_same_query!( authored_templates, config, key_manager_states, + locks, non_fungible_tokens, - output_locks, outputs, resources, shard_state_versions, diff --git a/crates/wallet/storage_sqlite/src/writer.rs b/crates/wallet/storage_sqlite/src/writer.rs index f4786c8912..b4d13483ce 100644 --- a/crates/wallet/storage_sqlite/src/writer.rs +++ b/crates/wallet/storage_sqlite/src/writer.rs @@ -3,12 +3,20 @@ use std::{ collections::HashSet, - ops::{Add, Deref, DerefMut, Sub}, + iter, + ops::{Deref, DerefMut, Sub}, str::FromStr, sync::MutexGuard, }; -use diesel::{NullableExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SqliteConnection}; +use diesel::{ + BoolExpressionMethods, + NullableExpressionMethods, + OptionalExtension, + QueryDsl, + RunQueryDsl, + SqliteConnection, +}; use log::*; use serde::Serialize; use tari_bor::json_encoding::CborValueJsonSerializeWrapper; @@ -21,12 +29,12 @@ use tari_ootle_wallet_sdk::{ ConfidentialOutputModel, NewAccountData, NonFungibleToken, - OutputLockId, OutputStatus, StealthOutputModel, SubstateModel, TransactionStatus, VaultModel, + WalletLockId, WalletTransactionUpdate, }, storage::{WalletStorageError, WalletStoreReader, WalletStoreWriter}, @@ -66,13 +74,23 @@ impl<'a> WriteTransaction<'a> { } } - fn get_lock(&mut self, lock_id: OutputLockId) -> Result { - use crate::schema::output_locks; + fn ensure_lock_exists(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { + use crate::schema::locks; - output_locks::table - .filter(output_locks::id.eq(lock_id as i32)) - .first(self.connection()) - .map_err(|e| WalletStorageError::general("get_proof", e)) + let count = locks::table + .filter(locks::id.eq(lock_id)) + .limit(1) + .count() + .get_result::(self.connection()) + .map_err(|e| WalletStorageError::general("ensure_lock_exists", e))?; + if count == 0 { + return Err(WalletStorageError::NotFound { + operation: "ensure_lock_exists", + entity: "lock".to_string(), + key: lock_id.to_string(), + }); + } + Ok(()) } } @@ -302,16 +320,22 @@ impl WalletStoreWriter for WriteTransaction<'_> { ) -> Result<(), WalletStorageError> { use crate::schema::transactions; + let ref_components = transaction + .as_referenced_components() + .map(|c| c.to_string()) + .collect::>(); + let signers = transaction + .signatures() + .iter() + .map(|s| s.public_key()) + .chain(iter::once(transaction.seal_signature().public_key())) + .collect::>(); diesel::insert_into(transactions::table) .values(( - transactions::hash.eq(transaction.calculate_id().to_string()), - transactions::network.eq(i32::from(transaction.network())), - transactions::fee_instructions.eq(serialize_json(transaction.fee_instructions())?), - transactions::instructions.eq(serialize_json(transaction.instructions())?), - transactions::signatures.eq(serialize_json(transaction.signatures())?), - transactions::seal_signature.eq(serialize_json(transaction.seal_signature())?), - transactions::is_seal_signer_authorized.eq(transaction.is_seal_signer_authorized()), - transactions::inputs.eq(serialize_json(transaction.inputs())?), + transactions::transaction_id.eq(serialize_hex(transaction.calculate_id())), + transactions::transaction_json.eq(serialize_json(transaction)?), + transactions::referenced_components.eq(serialize_json(&ref_components)?), + transactions::signers.eq(serialize_json(&signers)?), transactions::status.eq(TransactionStatus::New.as_key_str()), transactions::new_account_info.eq(new_account_info.map(serialize_json).transpose()?), transactions::dry_run.eq(is_dry_run), @@ -338,7 +362,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { transactions::invalid_reason.eq(update.invalid_reason), transactions::updated_at.eq(diesel::dsl::now), )) - .filter(transactions::hash.eq(update.transaction_id.to_string())) + .filter(transactions::transaction_id.eq(update.transaction_id.to_string())) .execute(self.connection()) .map_err(|e| WalletStorageError::general("transactions_set_result_and_status", e))?; @@ -615,11 +639,12 @@ impl WalletStoreWriter for WriteTransaction<'_> { fn vaults_lock_revealed_funds( &mut self, - lock_id: OutputLockId, + lock_id: WalletLockId, + vault_id: &VaultId, amount_to_lock: Amount, ) -> Result<(), WalletStorageError> { const OPERATION: &str = "vaults_lock_revealed_funds"; - use crate::schema::{output_locks, vaults}; + use crate::schema::vaults; if amount_to_lock.is_zero() { // No-op @@ -632,49 +657,46 @@ impl WalletStoreWriter for WriteTransaction<'_> { )); } - // TODO: we add using sql, limiting the max to i64::MAX. Amounts should/could be represented as a string, output - // amount is limited by bulletproofs to u64::MAX. - let amount_to_lock = amount_to_lock - .to_u64_checked() - .ok_or_else(|| WalletStorageError::bad_query(OPERATION, "amount to lock is too large"))?; - let amount_to_lock = i64::try_from(amount_to_lock).map_err(|_| { - WalletStorageError::bad_query(OPERATION, "amount to lock is too large, must be less than i64::MAX") - })?; - - let changeset = - output_locks::locked_revealed_amount.eq(output_locks::locked_revealed_amount.add(amount_to_lock)); + self.ensure_lock_exists(lock_id)?; + let vault_str = vault_id.to_string(); - let num_rows = diesel::update(output_locks::table) - .set(changeset) - .filter(output_locks::id.eq(lock_id as i32)) - .execute(self.connection()) + let (db_id, existing_lock_id) = vaults::table + .select((vaults::id, vaults::locked_by)) + .filter(vaults::address.eq(&vault_str)) + .first::<(i32, Option)>(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; - if num_rows == 0 { - return Err(WalletStorageError::NotFound { - operation: "vaults_lock_revealed_funds", - entity: "lock".to_string(), - key: lock_id.to_string(), + // Only one lock per vault (for simplicity, but unlikely to be a real limitation) + if existing_lock_id.is_some_and(|l| l != lock_id) { + return Err(WalletStorageError::BadQuery { + operation: OPERATION, + details: format!("Vault {} is already locked by another lock", vault_id), }); } - let lock = self.get_lock(lock_id)?; - let vault_id = lock.vault_id.ok_or_else(|| WalletStorageError::BadQuery { - operation: "vaults_lock_revealed_funds", - details: format!("lock {} does not lock a vault", lock_id), + // TODO: we're limited to i64::MAX. Could be an issue with resources that have a high + // divisibility. e.g. i64::MAX < 10 ETH. Values could be represented as a string + let amount_to_lock = amount_to_lock + .to_u64_checked() + .ok_or_else(|| WalletStorageError::bad_query(OPERATION, "amount to lock is too large"))?; + let amount_to_lock = i64::try_from(amount_to_lock).map_err(|_| { + WalletStorageError::bad_query(OPERATION, "amount to lock is too large, must be less than i64::MAX") })?; - let changeset = vaults::locked_revealed_balance.eq(vaults::locked_revealed_balance.add(amount_to_lock)); + let changeset = ( + vaults::locked_revealed_balance.eq(amount_to_lock), + vaults::locked_by.eq(lock_id), + ); let num_rows = diesel::update(vaults::table) .set(changeset) - .filter(vaults::id.eq(vault_id)) + .filter(vaults::id.eq(db_id)) .execute(self.connection()) - .map_err(|e| WalletStorageError::general("vaults_lock_revealed_funds", e))?; + .map_err(|e| WalletStorageError::general(OPERATION, e))?; if num_rows == 0 { return Err(WalletStorageError::NotFound { - operation: "vaults_lock_revealed_funds", + operation: OPERATION, entity: "vault".to_string(), key: vault_id.to_string(), }); @@ -683,66 +705,48 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - fn vaults_finalized_locked_revealed_funds(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError> { + fn vaults_finalized_locked_revealed_funds(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { + const OPERATION: &str = "vaults_finalized_locked_funds"; use crate::schema::vaults; - let lock = self.get_lock(lock_id)?; - - let Some(vault_id) = lock.vault_id else { - debug!( - target: LOG_TARGET, - "Lock {} does not lock a vault, skipping vaults_finalized_locked_revealed_funds", - lock_id - ); - // Lock does not lock a vault, therefore, does not lock revealed function = No-op - return Ok(()); - }; - let changeset = ( - vaults::revealed_balance.eq(vaults::revealed_balance.sub(lock.locked_revealed_amount)), - vaults::locked_revealed_balance.eq(vaults::locked_revealed_balance.sub(lock.locked_revealed_amount)), + vaults::revealed_balance.eq(vaults::revealed_balance.sub(vaults::locked_revealed_balance)), + vaults::locked_revealed_balance.eq(0), + vaults::locked_by.eq(None::), ); let num_rows = diesel::update(vaults::table) .set(changeset) - .filter(vaults::id.eq(vault_id)) + .filter(vaults::locked_by.eq(lock_id)) .execute(self.connection()) - .map_err(|e| WalletStorageError::general("vaults_finalized_locked_funds", e))?; + .map_err(|e| WalletStorageError::general(OPERATION, e))?; if num_rows == 0 { return Err(WalletStorageError::NotFound { - operation: "vaults_finalized_locked_funds", - entity: "vault".to_string(), - key: vault_id.to_string(), + operation: OPERATION, + entity: "lock on vault".to_string(), + key: lock_id.to_string(), }); } Ok(()) } - fn vaults_unlock_revealed_funds(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError> { + fn vaults_release_lock_revealed_funds(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { + const OPERATION: &str = "vaults_unlock_revealed_funds"; use crate::schema::vaults; - let lock = self.get_lock(lock_id)?; - let Some(vault_id) = lock.vault_id else { - // Lock does not lock a vault, therefore, does not lock revealed function = No-op - return Ok(()); - }; - - let changeset = - vaults::locked_revealed_balance.eq(vaults::locked_revealed_balance.sub(lock.locked_revealed_amount)); - let num_rows = diesel::update(vaults::table) - .set(changeset) - .filter(vaults::id.eq(vault_id)) + .set((vaults::locked_revealed_balance.eq(0), vaults::locked_by.eq(None::))) + .filter(vaults::locked_by.eq(lock_id)) .execute(self.connection()) - .map_err(|e| WalletStorageError::general("vaults_unlock_revealed_funds", e))?; + .map_err(|e| WalletStorageError::general(OPERATION, e))?; if num_rows == 0 { return Err(WalletStorageError::NotFound { - operation: "vaults_unlock_revealed_funds", - entity: "vault".to_string(), - key: vault_id.to_string(), + operation: OPERATION, + entity: "lock on vault".to_string(), + key: lock_id.to_string(), }); } @@ -807,7 +811,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { fn outputs_lock_smallest_amount( &mut self, vault_id: &VaultId, - lock_id: OutputLockId, + lock_id: WalletLockId, ) -> Result { use crate::schema::{accounts, outputs, vaults}; @@ -839,7 +843,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { let changeset = ( outputs::status.eq(OutputStatus::LockedForSpend.as_key_str()), - outputs::lock_id.eq(lock_id as i32), + outputs::lock_id.eq(lock_id), outputs::locked_at.eq(diesel::dsl::now), ); diesel::update(outputs::table) @@ -915,7 +919,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { outputs::encryption_secret_key_index.eq(output.encryption_secret_key_index as i64), outputs::encrypted_data.eq(output.encrypted_data.as_ref()), outputs::status.eq(output.status.as_key_str()), - outputs::lock_id.eq(output.lock_id.map(|v| v as i32)), + outputs::lock_id.eq(output.lock_id), )) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_insert", e))?; @@ -923,12 +927,12 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - fn outputs_finalize_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError> { + fn outputs_finalize_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { use crate::schema::outputs; // Unlock locked unconfirmed outputs diesel::update(outputs::table) - .filter(outputs::lock_id.eq(lock_id as i32)) + .filter(outputs::lock_id.eq(lock_id)) .filter(outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) .set(( outputs::status.eq(OutputStatus::Unspent.as_key_str()), @@ -940,7 +944,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { // Mark locked outputs as spent diesel::update(outputs::table) - .filter(outputs::lock_id.eq(lock_id as i32)) + .filter(outputs::lock_id.eq(lock_id)) .filter(outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) .set(( outputs::status.eq(OutputStatus::Spent.as_key_str()), @@ -953,12 +957,12 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - fn outputs_release_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError> { + fn outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { use crate::schema::outputs; // Unlock locked unspent outputs diesel::update(outputs::table) - .filter(outputs::lock_id.eq(lock_id as i32)) + .filter(outputs::lock_id.eq(lock_id)) .filter(outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) .set(( outputs::status.eq(OutputStatus::Unspent.as_key_str()), @@ -971,7 +975,7 @@ impl WalletStoreWriter for WriteTransaction<'_> { // Remove outputs that were created by this lock diesel::delete(outputs::table) .filter(outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) - .filter(outputs::lock_id.eq(lock_id as i32)) + .filter(outputs::lock_id.eq(lock_id)) .execute(self.connection()) .map_err(|e| WalletStorageError::general("outputs_unlock_by_proof_id", e))?; @@ -981,12 +985,13 @@ impl WalletStoreWriter for WriteTransaction<'_> { fn stealth_outputs_lock_smallest_amount( &mut self, account_address: &ComponentAddress, - lock_id: OutputLockId, + resource_address: &ResourceAddress, + lock_id: WalletLockId, ) -> Result { const OPERATION: &str = "stealth_outputs_lock_smallest_amount"; use crate::schema::stealth_outputs; - let lock = self.get_lock(lock_id)?; + self.ensure_lock_exists(lock_id)?; let account_id = accounts::table .select(accounts::id) @@ -995,9 +1000,16 @@ impl WalletStoreWriter for WriteTransaction<'_> { .map_err(|e| WalletStorageError::general(OPERATION, e))?; let locked_output = stealth_outputs::table - .filter(stealth_outputs::resource_address.eq(&lock.resource_address)) + .filter(stealth_outputs::resource_address.eq(resource_address.to_string())) .filter(stealth_outputs::owner_account_id.eq(account_id)) - .filter(stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str())) + .filter( + stealth_outputs::status + .eq(OutputStatus::Unspent.as_key_str()) + // Allow locking a UTXO created within the transaction + .or(stealth_outputs::status + .eq(OutputStatus::LockedUnconfirmed.as_key_str()) + .and(stealth_outputs::lock_id.eq(lock_id ))), + ) .filter(stealth_outputs::is_burnt.eq(false)) .filter(stealth_outputs::is_frozen.eq(false)) .order_by(stealth_outputs::value.asc()) @@ -1007,15 +1019,13 @@ impl WalletStoreWriter for WriteTransaction<'_> { .ok_or_else(|| WalletStorageError::NotFound { operation: OPERATION, entity: "stealth_output".to_string(), - key: format!( - "resource={}, lock_id={}, account_id={} ({})", - lock.resource_address, lock_id, account_id, account_address - ), + key: format!("lock_id={}, account_id={} ({})", lock_id, account_id, account_address), })?; let changeset = ( stealth_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str()), - stealth_outputs::lock_id.eq(lock_id as i32), + stealth_outputs::lock_id.eq(lock_id), + stealth_outputs::is_on_chain.eq(locked_output.status != OutputStatus::LockedUnconfirmed.as_key_str()), stealth_outputs::locked_at.eq(diesel::dsl::now), ); diesel::update(stealth_outputs::table) @@ -1048,10 +1058,11 @@ impl WalletStoreWriter for WriteTransaction<'_> { stealth_outputs::encryption_secret_key_index.eq(output.encryption_secret_key_index as i64), stealth_outputs::encrypted_data.eq(output.encrypted_data.as_ref()), stealth_outputs::tag_byte.eq(i32::from(output.tag_byte.as_byte())), + stealth_outputs::is_on_chain.eq(output.is_on_chain), stealth_outputs::status.eq(output.status.as_key_str()), stealth_outputs::is_burnt.eq(output.is_burnt), stealth_outputs::is_frozen.eq(output.is_frozen), - stealth_outputs::lock_id.eq(output.lock_id.map(|v| v as i32)), + stealth_outputs::lock_id.eq(output.lock_id), )) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; @@ -1085,25 +1096,26 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - fn stealth_outputs_finalize_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError> { + fn stealth_outputs_finalize_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { const OPERATION: &str = "stealth_outputs_finalize_by_lock_id"; use crate::schema::stealth_outputs; // Unlock locked unconfirmed stealth_outputs diesel::update(stealth_outputs::table) - .filter(stealth_outputs::lock_id.eq(lock_id as i32)) + .filter(stealth_outputs::lock_id.eq(lock_id)) .filter(stealth_outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) .set(( stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str()), stealth_outputs::lock_id.eq::>(None), stealth_outputs::locked_at.eq::>(None), + stealth_outputs::is_on_chain.eq(true), )) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; // Mark locked outputs as spent diesel::update(stealth_outputs::table) - .filter(stealth_outputs::lock_id.eq(lock_id as i32)) + .filter(stealth_outputs::lock_id.eq(lock_id)) .filter(stealth_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) .set(( stealth_outputs::status.eq(OutputStatus::Spent.as_key_str()), @@ -1116,14 +1128,15 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - fn stealth_outputs_release_by_lock_id(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError> { + fn stealth_outputs_release_by_lock_id(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { const OPERATION: &str = "stealth_outputs_release_by_lock_id"; use crate::schema::stealth_outputs; // Unlock locked unspent stealth_outputs diesel::update(stealth_outputs::table) - .filter(stealth_outputs::lock_id.eq(lock_id as i32)) + .filter(stealth_outputs::lock_id.eq(lock_id)) .filter(stealth_outputs::status.eq(OutputStatus::LockedForSpend.as_key_str())) + .filter(stealth_outputs::is_on_chain.eq(true)) .set(( stealth_outputs::status.eq(OutputStatus::Unspent.as_key_str()), stealth_outputs::lock_id.eq::>(None), @@ -1134,8 +1147,14 @@ impl WalletStoreWriter for WriteTransaction<'_> { // Remove stealth_outputs that were created by this lock diesel::delete(stealth_outputs::table) - .filter(stealth_outputs::status.eq(OutputStatus::LockedUnconfirmed.as_key_str())) - .filter(stealth_outputs::lock_id.eq(lock_id as i32)) + .filter(stealth_outputs::lock_id.eq(lock_id)) + .filter( + stealth_outputs::status + .eq(OutputStatus::LockedUnconfirmed.as_key_str()) + .or(stealth_outputs::status + .eq(OutputStatus::LockedForSpend.as_key_str()) + .and(stealth_outputs::is_on_chain.eq(false))), + ) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; @@ -1176,108 +1195,46 @@ impl WalletStoreWriter for WriteTransaction<'_> { Ok(()) } - // Output locks - fn output_locks_insert(&mut self, resource_address: &ResourceAddress) -> Result { - const OPERATION: &str = "stealth_locks_insert"; - use crate::schema::output_locks; + // locks + fn locks_create(&mut self) -> Result { + const OPERATION: &str = "locks_create"; + use crate::schema::locks; - diesel::insert_into(output_locks::table) - .values(output_locks::resource_address.eq(resource_address.to_string())) + diesel::insert_into(locks::table) + .default_values() .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; // TODO: See if we can upgrade libSQLite 0.35 - let lock_id = output_locks::table - .select(output_locks::id) - .order_by(output_locks::id.desc()) + let lock_id = locks::table + .select(locks::id) + .order_by(locks::id.desc()) .first::(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; - Ok(lock_id as OutputLockId) + Ok(lock_id as WalletLockId) } - fn output_locks_insert_for_vault(&mut self, vault_id: &VaultId) -> Result { - const OPERATION: &str = "output_locks_insert"; - use crate::schema::{output_locks, vaults}; + fn locks_delete(&mut self, lock_id: WalletLockId) -> Result<(), WalletStorageError> { + const OPERATION: &str = "locks_delete"; + use crate::schema::locks; - let (vault_id, resource_address) = vaults::table - .select((vaults::id, vaults::resource_address)) - .filter(vaults::address.eq(vault_id.to_string())) - .first::<(i32, String)>(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - diesel::insert_into(output_locks::table) - .values(( - output_locks::resource_address.eq(resource_address), - output_locks::vault_id.eq(vault_id), - )) + diesel::delete(locks::table.filter(locks::id.eq(lock_id))) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; - // RETURNING only available from SQLite 3.35 https://www.sqlite.org/lang_returning.html - // TODO: See if we can upgrade SQLite - let lock_id = output_locks::table - .select(output_locks::id) - .order_by(output_locks::id.desc()) - .first::(self.connection()) - .map_err(|e| WalletStorageError::general(OPERATION, e))?; - - Ok(lock_id as OutputLockId) - } - - fn output_locks_delete(&mut self, lock_id: OutputLockId) -> Result<(), WalletStorageError> { - use crate::schema::output_locks; - - diesel::delete(output_locks::table.filter(output_locks::id.eq(lock_id as i32))) - .execute(self.connection()) - .map_err(|e| WalletStorageError::general("output_locks_delete", e))?; - Ok(()) } - fn output_locks_set_params( + fn locks_link_transaction( &mut self, - lock_id: OutputLockId, - transaction_id: Option, - vault_id: Option, + lock_id: WalletLockId, + transaction_id: TransactionId, ) -> Result<(), WalletStorageError> { - const OPERATION: &str = "output_locks_set_params"; - use crate::schema::output_locks; - - if transaction_id.is_none() && vault_id.is_none() { - return Err(WalletStorageError::BadQuery { - operation: "output_locks_set_params", - details: "At least one of transaction_id or vault_id must be provided".to_string(), - }); - } - - #[derive(AsChangeset)] - #[diesel(table_name = output_locks)] - struct UpdateOutputLock { - vault_id: Option>, - transaction_hash: Option, - } - - let vault_db_id = if let Some(vault_id) = vault_id { - use crate::schema::vaults; - - vaults::table - .select(vaults::id) - .filter(vaults::address.eq(vault_id.to_string())) - .first::(self.connection()) - .map(Some) - .map(Some) - .map_err(|e| WalletStorageError::general(OPERATION, e))? - } else { - None - }; - - let update_set = UpdateOutputLock { - vault_id: vault_db_id, - transaction_hash: transaction_id.map(|t| t.to_string()), - }; + const OPERATION: &str = "locks_link_transaction"; + use crate::schema::locks; - diesel::update(output_locks::table.filter(output_locks::id.eq(lock_id as i32))) - .set(update_set) + diesel::update(locks::table.filter(locks::id.eq(lock_id))) + .set(locks::transaction_id.eq(serialize_hex(transaction_id))) .execute(self.connection()) .map_err(|e| WalletStorageError::general(OPERATION, e))?; diff --git a/crates/wallet/storage_sqlite/tests/transaction.rs b/crates/wallet/storage_sqlite/tests/transaction.rs index 207c347ec8..bce807a8dd 100644 --- a/crates/wallet/storage_sqlite/tests/transaction.rs +++ b/crates/wallet/storage_sqlite/tests/transaction.rs @@ -1,17 +1,23 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_common_types::types::PrivateKey; +use tari_crypto::ristretto::RistrettoSecretKey; use tari_ootle_common_types::optional::Optional; use tari_ootle_wallet_sdk::{ models::TransactionStatus, storage::{WalletStore, WalletStoreReader, WalletStoreWriter}, }; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; -use tari_transaction::{Transaction, TransactionId}; +use tari_transaction::{args, Transaction, TransactionId}; fn build_transaction() -> Transaction { - Transaction::builder().build_and_seal(&PrivateKey::default()) + let key = RistrettoSecretKey::from(123); + Transaction::builder() + .allocate_component_address("component") + .put_last_instruction_output_on_workspace("bucket") + .call_method("component", "new", args!["bucket"]) + .with_dry_run(true) + .build_and_seal(&key) } #[test] @@ -21,13 +27,17 @@ fn get_and_insert_transaction() { let mut tx = db.create_write_tx().unwrap(); let transaction = tx.transactions_get(TransactionId::default()).optional().unwrap(); assert!(transaction.is_none()); + let transaction = build_transaction(); - let hash = transaction.calculate_id(); + assert!(transaction.verify_all_signatures()); + let tx_id = transaction.calculate_id(); tx.transactions_insert(&transaction, None, false).unwrap(); tx.commit().unwrap(); let mut tx = db.create_read_tx().unwrap(); - let returned = tx.transactions_get(hash).unwrap(); - assert_eq!(transaction.calculate_id(), returned.id); + let returned = tx.transactions_get(tx_id).unwrap(); + // Transaction was not malleated in the database + assert!(returned.transaction.verify_all_signatures()); + assert_eq!(returned.transaction.calculate_id(), tx_id); assert_eq!(returned.status, TransactionStatus::default()); } diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index bf2daed162..b2c756e62c 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -49,7 +49,7 @@ tari_comms_dht = { git = "https://github.com/tari-project/tari.git", tag = "v4.9 anyhow = { workspace = true } config = { workspace = true } cucumber = { version = "0.21.1", features = ["default", "libtest", "output-junit"] } -httpmock = { workspace = true } +httpmock = { version = "0.8.0-alpha.1", features = ["remote"] } indexmap = { workspace = true } libp2p = { workspace = true } log = { workspace = true, features = ["std"] } diff --git a/integration_tests/src/http_server.rs b/integration_tests/src/http_server.rs index 9d311b59a9..2f55b8d352 100644 --- a/integration_tests/src/http_server.rs +++ b/integration_tests/src/http_server.rs @@ -1,32 +1,21 @@ // Copyright 2022 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{fmt, net::SocketAddr}; +use std::fmt; use httpmock::MockServer; -use tari_shutdown::ShutdownSignal; -use tokio::task; - -use crate::helpers::get_os_assigned_port; pub struct MockHttpServer { server: MockServer, - base_url: SocketAddr, } impl MockHttpServer { - pub async fn connect(port: u16) -> Self { - let base_url = format!("127.0.0.1:{}", port); + pub async fn connect() -> Self { Self { - base_url: base_url.parse().unwrap(), - server: MockServer::connect_async(&base_url).await, + server: MockServer::start_async().await, } } - pub fn base_url(&self) -> &SocketAddr { - &self.base_url - } - pub async fn publish_file(&self, url_path: String, file_path: String) -> Mock<'_> { let mock = self .server @@ -36,14 +25,14 @@ impl MockHttpServer { }) .await; - let url = format!("{}/{}", self.server.base_url(), url_path); + let url = self.server.url(url_path); Mock { mock, url } } } impl fmt::Debug for MockHttpServer { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "MockHttpServer: {}", self.base_url) + write!(f, "MockHttpServer") } } @@ -57,15 +46,3 @@ pub struct Mock<'a> { // self.mock.delete(); // } // } - -pub async fn spawn_template_http_server(signal: ShutdownSignal) -> u16 { - let mock_port = get_os_assigned_port(); - task::spawn(async move { - httpmock::standalone::start_standalone_server(mock_port, false, None, false, 5, signal) - .await - .unwrap(); - }); - - println!("Mock server started at http://localhost:{}/", mock_port); - mock_port -} diff --git a/integration_tests/src/template.rs b/integration_tests/src/template.rs index d4783c87fd..e2043e0a4a 100644 --- a/integration_tests/src/template.rs +++ b/integration_tests/src/template.rs @@ -76,7 +76,7 @@ pub async fn publish_template( // look for the new UP template substate let template_id = finalize_result .result - .accept() + .any_accept() .and_then(|result| result.up_iter().find_map(|(substate_id, _)| substate_id.as_template())) .map(|id| id.as_hash()) .ok_or_else(|| anyhow!("Transaction result did not contain a published template!"))?; diff --git a/integration_tests/src/validator_node_cli.rs b/integration_tests/src/validator_node_cli.rs index 834cac7fc5..6fe5c6381d 100644 --- a/integration_tests/src/validator_node_cli.rs +++ b/integration_tests/src/validator_node_cli.rs @@ -67,7 +67,6 @@ pub async fn create_account(world: &mut TariWorld, account_name: String, validat version: None, dump_outputs_into: None, account_template_address: None, - dry_run: false, }; let mut client = world.get_validator_node(&validator_node_name).get_client(); let resp = submit_transaction(vec![instruction], common, data_dir, &mut client) @@ -82,7 +81,7 @@ pub async fn create_account(world: &mut TariWorld, account_name: String, validat add_substate_ids( world, account_name, - resp.dry_run_result.unwrap().finalize.result.accept().unwrap(), + resp.dry_run_result.unwrap().finalize.result.any_accept().unwrap(), ); } @@ -117,7 +116,6 @@ pub async fn create_component( version: None, dump_outputs_into: None, account_template_address: None, - dry_run: false, }, }; let mut client = world.get_validator_node(&vn_name).get_client(); @@ -130,7 +128,7 @@ pub async fn create_component( add_substate_ids( world, outputs_name, - resp.dry_run_result.unwrap().finalize.result.accept().unwrap(), + resp.dry_run_result.unwrap().finalize.result.any_accept().unwrap(), ); } @@ -267,7 +265,13 @@ pub async fn call_method( add_substate_ids( world, outputs_name, - resp.dry_run_result.as_ref().unwrap().finalize.result.accept().unwrap(), + resp.dry_run_result + .as_ref() + .unwrap() + .finalize + .result + .any_accept() + .unwrap(), ); Ok(resp) } @@ -295,7 +299,6 @@ async fn call_method_inner( version: None, dump_outputs_into: None, account_template_address: None, - dry_run: false, }, }; let resp = handle_submit(args, vn_data_dir, &mut vn_client.clone()).await.unwrap(); @@ -366,7 +369,6 @@ pub async fn submit_manifest( version: None, dump_outputs_into: None, account_template_address: None, - dry_run: false, }; let resp = submit_transaction(instructions.instructions, args, data_dir, &mut client) .await @@ -379,7 +381,7 @@ pub async fn submit_manifest( add_substate_ids( world, outputs_name, - resp.dry_run_result.unwrap().finalize.result.accept().unwrap(), + resp.dry_run_result.unwrap().finalize.result.any_accept().unwrap(), ); } diff --git a/integration_tests/src/wallet_daemon_cli.rs b/integration_tests/src/wallet_daemon_cli.rs index e5c13cd851..69de76fa4d 100644 --- a/integration_tests/src/wallet_daemon_cli.rs +++ b/integration_tests/src/wallet_daemon_cli.rs @@ -31,11 +31,11 @@ use tari_crypto::{ use tari_engine_types::{substate::SubstateId, ToByteType}; use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_wallet_sdk::{ - apis::confidential_transfer::ConfidentialTransferInputSelection, + apis::{confidential_transfer::ConfidentialTransferInputSelection, key_manager::KeyBranch}, models::{Account, AccountWithPublicKey, NonFungibleToken}, }; use tari_template_lib::{ - constants::CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + constants::STEALTH_TARI_RESOURCE_ADDRESS, prelude::{PedersenCommitmentBytes, ResourceAddress, RistrettoPublicKeyBytes}, resource::TOKEN_SYMBOL, types::{crypto::RangeProofBytes, Amount}, @@ -57,6 +57,7 @@ use tari_wallet_daemon_client::{ ClaimValidatorFeesRequest, ClaimValidatorFeesResponse, ConfidentialTransferRequest, + ExtClaimBurnProof, ListNftsRequest, MintFaucetNftRequest, ProofsGenerateRequest, @@ -88,14 +89,18 @@ pub async fn claim_burn( max_fee: u64, ) -> Result { let mut client = get_auth_wallet_daemon_client(world, &wallet_daemon_name).await; + let resp = client.create_key(KeyBranch::Nonce).await?; let claim_burn_request = ClaimBurnRequest { - account: ComponentAddressOrName::Name(account_name.clone()), - claim_proof: ClaimBurnProof { - commitment, - ownership_proof: ownership_proof.to_byte_type(), - reciprocal_claim_public_key: reciprocal_claim_public_key.to_byte_type(), - range_proof, + account: ComponentAddressOrName::Name(account_name), + claim_proof: ExtClaimBurnProof { + claim_proof: ClaimBurnProof { + commitment, + ownership_proof: ownership_proof.to_byte_type(), + reciprocal_claim_public_key: reciprocal_claim_public_key.to_byte_type(), + range_proof, + }, + owner_nonce_key_index: resp.id, }, max_fee: Some(max_fee), }; @@ -182,7 +187,7 @@ pub async fn transfer_confidential( let destination_account = destination_account_resp.account.address; let destination_public_key = destination_account_resp.public_key; - let resource_address = CONFIDENTIAL_TARI_RESOURCE_ADDRESS; + let resource_address = STEALTH_TARI_RESOURCE_ADDRESS; let create_transfer_proof_req = ProofsGenerateRequest { account: Some(source_account_name), @@ -864,7 +869,7 @@ pub async fn transfer( }; let resp = client.accounts_transfer(request).await.unwrap(); - add_substate_ids(world, outputs_name, resp.result.result.accept().unwrap()); + add_substate_ids(world, outputs_name, resp.result.result.any_accept().unwrap()); } pub async fn confidential_transfer( @@ -885,7 +890,7 @@ pub async fn confidential_transfer( amount, destination_public_key, max_fee, - resource_address: CONFIDENTIAL_TARI_RESOURCE_ADDRESS, + resource_address: STEALTH_TARI_RESOURCE_ADDRESS, proof_from_badge_resource: None, dry_run: false, input_selection: ConfidentialTransferInputSelection::PreferRevealed, @@ -893,7 +898,7 @@ pub async fn confidential_transfer( }; let resp = client.accounts_confidential_transfer(request).await.unwrap(); - add_substate_ids(world, outputs_name, resp.result.result.accept().unwrap()); + add_substate_ids(world, outputs_name, resp.result.result.any_accept().unwrap()); } pub async fn get_auth_wallet_daemon_client(world: &TariWorld, wallet_daemon_name: &str) -> WalletDaemonClient { diff --git a/integration_tests/tests/cucumber.rs b/integration_tests/tests/cucumber.rs index 1b569f4e94..8f2c3b1de8 100644 --- a/integration_tests/tests/cucumber.rs +++ b/integration_tests/tests/cucumber.rs @@ -26,7 +26,7 @@ use std::{fs, future, io, panic, str::FromStr, time::Duration}; use anyhow::bail; use cucumber::{gherkin::Step, given, then, when, writer, writer::Verbosity, ScenarioType, World, WriterExt}; use integration_tests::{ - http_server::{spawn_template_http_server, MockHttpServer}, + http_server::MockHttpServer, logging::{create_log_config_file, get_base_dir}, miner::{mine_blocks, register_miner_process}, validator_node_cli, @@ -59,7 +59,6 @@ async fn main() { // Start the mock server that continues to run for the duration of the tests let mut shutdown = Shutdown::new(); - let mock_port = spawn_template_http_server(shutdown.to_signal()).await; let file = fs::File::create("cucumber-output-junit.xml").unwrap(); let cucumber_fut = TariWorld::cucumber() @@ -81,7 +80,7 @@ async fn main() { Box::pin(async move { // Each scenario gets a mock connection. As each connection is dropped after the scenario, all the mock // urls are deregistered - world.http_server = Some(MockHttpServer::connect(mock_port).await); + world.http_server = Some(MockHttpServer::connect().await); }) }) .after(move |_feature, _rule, scenario, _finished, maybe_world| { diff --git a/integration_tests/tests/steps/wallet_daemon.rs b/integration_tests/tests/steps/wallet_daemon.rs index b5c349cfa8..b83e338c52 100644 --- a/integration_tests/tests/steps/wallet_daemon.rs +++ b/integration_tests/tests/steps/wallet_daemon.rs @@ -118,7 +118,7 @@ async fn when_i_claim_fees_for_validator_and_epoch( let resp = wallet_daemon_cli::claim_fees(world, wallet_daemon_name, account_name, validator_node, false) .await .unwrap(); - resp.result.result.accept().unwrap_or_else(|| { + resp.result.result.any_accept().unwrap_or_else(|| { panic!( "Expected fee claim to succeeded but failed with {}", resp.result.result.fee_reject().unwrap() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53bfe9fa1d..e276b9885f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ importers: file-saver: specifier: ^2.0.5 version: 2.0.5 + framer-motion: + specifier: ^12.23.12 + version: 12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) jwt-decode: specifier: ^4.0.0 version: 4.0.0 @@ -2951,6 +2954,20 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + framer-motion@12.23.12: + resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -3600,6 +3617,12 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.23.12: + resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + motion@10.16.2: resolution: {integrity: sha512-p+PurYqfUdcJZvtnmAqu5fJgV2kR0uLFQuBKtLeFVTrYEVllI99tiOTSefVNYuip9ELTEkepIIDftNdze76NAQ==} @@ -8139,6 +8162,16 @@ snapshots: flatted@3.3.3: {} + framer-motion@12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + motion-dom: 12.23.12 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.3.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + fresh@0.5.2: {} fs.realpath@1.0.0: {} @@ -9110,6 +9143,12 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.23.12: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + motion@10.16.2: dependencies: '@motionone/animation': 10.18.0 diff --git a/utilities/tariswap_test_bench/src/accounts.rs b/utilities/tariswap_test_bench/src/accounts.rs index c4261e1206..005b6d6745 100644 --- a/utilities/tariswap_test_bench/src/accounts.rs +++ b/utilities/tariswap_test_bench/src/accounts.rs @@ -44,7 +44,7 @@ impl Runner { .build_and_seal(&key.key); let finalize = self.submit_transaction_and_wait(transaction).await?; - let diff = finalize.result.accept().unwrap(); + let diff = finalize.result.any_accept().unwrap(); let account = diff .up_iter() .find_map(|(addr, _)| addr.as_component_address()) @@ -56,14 +56,9 @@ impl Runner { .unwrap(); self.sdk.accounts_api().add_account(None, &account, 0, true, true)?; - self.sdk.accounts_api().add_vault( - account, - vault, - XTR, - ResourceType::Confidential, - Some("XTR".to_string()), - 6, - )?; + self.sdk + .accounts_api() + .add_vault(account, vault, XTR, ResourceType::Stealth, Some("XTR".to_string()), 6)?; let account = self.sdk.accounts_api().get_account_by_address(&account)?; Ok(account.account) @@ -105,7 +100,7 @@ impl Runner { .build_and_seal(&key.key); let finalize = self.submit_transaction_and_wait(transaction).await?; - let diff = finalize.result.accept().unwrap(); + let diff = finalize.result.any_accept().unwrap(); let mut accounts = Vec::with_capacity(num_accounts); for owner in owners { @@ -180,7 +175,7 @@ impl Runner { let result = self.submit_transaction_and_wait(transaction).await?; let accounts_and_state = result .result - .accept() + .any_accept() .unwrap() .up_iter() .filter(|(addr, _)| { @@ -198,7 +193,7 @@ impl Runner { for vault_id in indexed.vault_ids() { let vault = result .result - .accept() + .any_accept() .unwrap() .up_iter() .find(|(addr, _)| addr == vault_id) diff --git a/utilities/tariswap_test_bench/src/faucet.rs b/utilities/tariswap_test_bench/src/faucet.rs index c11b02dfde..abef449845 100644 --- a/utilities/tariswap_test_bench/src/faucet.rs +++ b/utilities/tariswap_test_bench/src/faucet.rs @@ -40,7 +40,7 @@ impl Runner { .build_and_seal(&key.key); let finalize = self.submit_transaction_and_wait(transaction).await?; - let diff = finalize.result.accept().unwrap(); + let diff = finalize.result.any_accept().unwrap(); let component_address = diff .up_iter() diff --git a/utilities/tariswap_test_bench/src/runner.rs b/utilities/tariswap_test_bench/src/runner.rs index f463196f3b..8afba3721d 100644 --- a/utilities/tariswap_test_bench/src/runner.rs +++ b/utilities/tariswap_test_bench/src/runner.rs @@ -87,7 +87,7 @@ impl Runner { } self.stats - .add_substate_created(finalize.result.accept().unwrap().up_len()); + .add_substate_created(finalize.result.any_accept().unwrap().up_len()); return Ok(finalize.clone()); } diff --git a/utilities/tariswap_test_bench/src/tariswap.rs b/utilities/tariswap_test_bench/src/tariswap.rs index 98a8b8f97e..1ae9f13485 100644 --- a/utilities/tariswap_test_bench/src/tariswap.rs +++ b/utilities/tariswap_test_bench/src/tariswap.rs @@ -59,7 +59,7 @@ impl Runner { .build_and_seal(&key.key); let finalize = self.submit_transaction_and_wait(transaction).await?; - let diff = finalize.result.accept().unwrap(); + let diff = finalize.result.any_accept().unwrap(); Ok(diff .up_iter() .filter_map(|(addr, value)| { @@ -166,7 +166,7 @@ impl Runner { if let Some(reject) = result.result.any_reject() { return Err(anyhow::anyhow!("Transaction failed: {}", reject)); } - let diff = result.result.accept().unwrap(); + let diff = result.result.any_accept().unwrap(); let lp_vault = diff .up_iter() .find_map(|(addr, s)| { diff --git a/utilities/tariswap_test_bench/templates/tariswap/src/lib.rs b/utilities/tariswap_test_bench/templates/tariswap/src/lib.rs index e603a8947e..c448ac6634 100644 --- a/utilities/tariswap_test_bench/templates/tariswap/src/lib.rs +++ b/utilities/tariswap_test_bench/templates/tariswap/src/lib.rs @@ -226,8 +226,11 @@ mod tariswap { fn check_resource_is_fungible(resource: ResourceAddress) { let resource_type = ResourceManager::get(resource).resource_type(); assert!( - matches!(resource_type, ResourceType::Fungible | ResourceType::Confidential), - "Resource {} is not fungible nor confidential", + matches!( + resource_type, + ResourceType::Fungible | ResourceType::Confidential | ResourceType::Stealth + ), + "Resource {} is not fungible (fungible, stealth, confidential)", resource ); } diff --git a/utilities/transaction_submitter/src/main.rs b/utilities/transaction_submitter/src/main.rs index 58cdf8049a..6ff5c7c357 100644 --- a/utilities/transaction_submitter/src/main.rs +++ b/utilities/transaction_submitter/src/main.rs @@ -102,10 +102,7 @@ async fn stress_test(args: StressTestArgs) -> anyhow::Result { @@ -192,7 +189,7 @@ async fn fetch_result_summary( { Ok(Some(result)) => { let exec_result = result.transaction_execution.result(); - let result = if let Some(diff) = exec_result.finalize.result.accept() { + let result = if let Some(diff) = exec_result.finalize.result.any_accept() { TxFinalized { is_committed: true, is_error: false,