From 3062597fca5ceb7ee8d341b8b456277f2b69611d Mon Sep 17 00:00:00 2001 From: skoupidi Date: Thu, 25 Jan 2024 18:52:03 +0200 Subject: [PATCH] drk is back in the menu boys --- .gitignore | 3 + Cargo.lock | 397 ++++++- Cargo.toml | 3 +- Makefile | 7 + bin/drk/Cargo.toml | 36 +- bin/{drk2 => drk}/Makefile | 0 bin/drk/TODO | 4 - bin/{drk2 => drk}/drk_config.toml | 0 bin/drk/src/cli_util.rs | 402 ++++++- bin/{drk2 => drk}/src/dao.rs | 0 bin/drk/src/deploy_contract.rs | 181 --- bin/{drk2 => drk}/src/error.rs | 0 bin/drk/src/main.rs | 1151 +++++++++++-------- bin/{drk2 => drk}/src/money.rs | 0 bin/{drk2 => drk}/src/rpc.rs | 0 bin/drk/src/rpc_airdrop.rs | 72 -- bin/drk/src/rpc_blockchain.rs | 355 ------ bin/drk/src/rpc_dao.rs | 553 --------- bin/drk/src/rpc_swap.rs | 462 -------- bin/drk/src/rpc_token.rs | 153 --- bin/drk/src/rpc_transfer.rs | 171 --- bin/{drk2 => drk}/src/swap.rs | 0 bin/{drk2 => drk}/src/token.rs | 0 bin/{drk2 => drk}/src/transfer.rs | 0 bin/{drk2 => drk}/src/txs_history.rs | 0 bin/drk/src/wallet.rs | 43 - bin/drk/src/wallet_dao.rs | 1322 --------------------- bin/drk/src/wallet_money.rs | 788 ------------- bin/drk/src/wallet_token.rs | 97 -- bin/drk/src/wallet_txs_history.rs | 192 ---- bin/{drk2 => drk}/src/walletdb.rs | 0 bin/drk2/Cargo.toml | 39 - bin/drk2/src/cli_util.rs | 469 -------- bin/drk2/src/main.rs | 1579 -------------------------- bin/drk2/wallet.mp3 | Bin 29037 -> 0 bytes bin/drk2/wallet.sql | 9 - 36 files changed, 1511 insertions(+), 6977 deletions(-) rename bin/{drk2 => drk}/Makefile (100%) delete mode 100644 bin/drk/TODO rename bin/{drk2 => drk}/drk_config.toml (100%) rename bin/{drk2 => drk}/src/dao.rs (100%) delete mode 100644 bin/drk/src/deploy_contract.rs rename bin/{drk2 => drk}/src/error.rs (100%) rename bin/{drk2 => drk}/src/money.rs (100%) rename bin/{drk2 => drk}/src/rpc.rs (100%) delete mode 100644 bin/drk/src/rpc_airdrop.rs delete mode 100644 bin/drk/src/rpc_blockchain.rs delete mode 100644 bin/drk/src/rpc_dao.rs delete mode 100644 bin/drk/src/rpc_swap.rs delete mode 100644 bin/drk/src/rpc_token.rs delete mode 100644 bin/drk/src/rpc_transfer.rs rename bin/{drk2 => drk}/src/swap.rs (100%) rename bin/{drk2 => drk}/src/token.rs (100%) rename bin/{drk2 => drk}/src/transfer.rs (100%) rename bin/{drk2 => drk}/src/txs_history.rs (100%) delete mode 100644 bin/drk/src/wallet.rs delete mode 100644 bin/drk/src/wallet_dao.rs delete mode 100644 bin/drk/src/wallet_money.rs delete mode 100644 bin/drk/src/wallet_token.rs delete mode 100644 bin/drk/src/wallet_txs_history.rs rename bin/{drk2 => drk}/src/walletdb.rs (100%) delete mode 100644 bin/drk2/Cargo.toml delete mode 100644 bin/drk2/src/cli_util.rs delete mode 100644 bin/drk2/src/main.rs delete mode 100644 bin/drk2/wallet.mp3 delete mode 100644 bin/drk2/wallet.sql diff --git a/.gitignore b/.gitignore index 241250e00..e187f6f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ /bin/darkfi-mmproxy/darkfi-mmproxy /darkfi-mmproxy +/bin/drk/drk +/drk + /bin/darkirc/darkirc /darkirc diff --git a/Cargo.lock b/Cargo.lock index f78a6f832..60b35ef29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,28 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "alsa" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" +dependencies = [ + "alsa-sys", + "bitflags 1.3.2", + "libc", + "nix 0.24.3", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "amplify" version = "4.5.0" @@ -823,6 +845,26 @@ dependencies = [ "which", ] +[[package]] +name = "bindgen" +version = "0.69.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c69fae65a523209d34240b60abe0c42d33d1045d445c0839d8a4894a736e2d" +dependencies = [ + "bitflags 2.4.1", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1082,6 +1124,12 @@ dependencies = [ "libc", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1261,6 +1309,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes 1.5.0", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -1389,6 +1447,26 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +dependencies = [ + "bindgen 0.69.2", +] + [[package]] name = "corosensei" version = "0.1.4" @@ -1402,6 +1480,31 @@ dependencies = [ "windows-sys 0.33.0", ] +[[package]] +name = "cpal" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni 0.19.0", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "once_cell", + "parking_lot 0.12.1", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1699,7 +1802,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" dependencies = [ - "nix", + "nix 0.27.1", "windows-sys 0.52.0", ] @@ -2260,6 +2363,12 @@ dependencies = [ "num-order", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-encoding" version = "2.5.0" @@ -2558,6 +2667,33 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "drk" +version = "0.4.1" +dependencies = [ + "blake3 1.5.0", + "bs58", + "darkfi", + "darkfi-sdk", + "darkfi-serial", + "darkfi_dao_contract", + "darkfi_money_contract", + "easy-parallel", + "log", + "prettytable-rs", + "rand 0.8.5", + "rodio", + "rusqlite", + "serde", + "signal-hook", + "signal-hook-async-std", + "simplelog", + "smol", + "structopt", + "structopt-toml", + "url", +] + [[package]] name = "dwrote" version = "0.11.0" @@ -3811,6 +3947,40 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.27" @@ -4023,6 +4193,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -4118,6 +4297,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + +[[package]] +name = "minimp3_fixed" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b0f14e7e75da97ae396c2656b10262a3d4afa2ec98f35795630eff0c8b951b" +dependencies = [ + "minimp3-sys", + "slice-ring-buffer", + "thiserror", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -4149,6 +4348,46 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum 0.5.11", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "nix" version = "0.27.1" @@ -4208,6 +4447,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -4265,13 +4515,34 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + [[package]] name = "num_enum" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.7.2", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -4304,6 +4575,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "oboe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" +dependencies = [ + "jni 0.20.0", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" +dependencies = [ + "cc", +] + [[package]] name = "oid-registry" version = "0.6.1" @@ -5161,10 +5455,16 @@ name = "randomx" version = "1.1.11" source = "git+https://github.com/darkrenaissance/RandomX#65c0b322edf5ab66fad53bd0d163b7a2e9c63125" dependencies = [ - "bindgen", + "bindgen 0.66.1", "bitflags 1.3.2", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "rayon" version = "1.8.0" @@ -5378,6 +5678,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rodio" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" +dependencies = [ + "cpal", + "minimp3_fixed", +] + [[package]] name = "routefinder" version = "0.4.0" @@ -6098,6 +6408,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" +[[package]] +name = "slice-ring-buffer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7081c7e452cb62f5f0c32edd4e265391bdbb23e90905df8bb88a23d3b5166b77" +dependencies = [ + "libc", + "mach2", + "winapi", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -7223,7 +7544,7 @@ dependencies = [ "humantime", "humantime-serde", "itertools 0.12.0", - "num_enum", + "num_enum 0.7.2", "pin-project", "postage", "rand 0.8.5", @@ -7473,7 +7794,7 @@ dependencies = [ "hex", "humantime", "itertools 0.12.0", - "num_enum", + "num_enum 0.7.2", "rand 0.8.5", "serde", "signature 1.6.4", @@ -8393,6 +8714,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -8433,6 +8763,21 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -8463,6 +8808,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8481,6 +8832,12 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd761fd3eb9ab8cc1ed81e56e567f02dd82c4c837e48ac3b2181b9ffc5060807" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8499,6 +8856,12 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab0cf703a96bab2dc0c02c0fa748491294bf9b7feb27e1f4f96340f208ada0e" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8517,6 +8880,12 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cfdbe89cc9ad7ce618ba34abc34bbb6c36d99e96cae2245b7943cd75ee773d0" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8535,6 +8904,12 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4dd9b0c0e9ece7bb22e84d70d01b71c6d6248b81a3c60d11869451b4cb24784" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8547,6 +8922,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8565,6 +8946,12 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff1e4aa646495048ec7f3ffddc411e1d829c026a2ec62b39da15c1055e406eaa" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 5fcff8b98..79b34f4c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,7 @@ members = [ #"bin/darkfid", "bin/darkfid2", "bin/darkfi-mmproxy", - #"bin/drk", - #"bin/drk2", + "bin/drk", #"bin/faucetd", #"bin/fud/fu", #"bin/fud/fud", diff --git a/Makefile b/Makefile index a1b037bfc..1843864e0 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,13 @@ darkfi-mmproxy: RUST_TARGET="$(RUST_TARGET)" \ RUSTFLAGS="$(RUSTFLAGS)" +drk: contracts + $(MAKE) -C bin/$@ \ + PREFIX="$(PREFIX)" \ + CARGO="$(CARGO)" \ + RUST_TARGET="$(RUST_TARGET)" \ + RUSTFLAGS="$(RUSTFLAGS)" + darkirc: zkas $(MAKE) -C bin/$@ \ PREFIX="$(PREFIX)" \ diff --git a/bin/drk/Cargo.toml b/bin/drk/Cargo.toml index f151db5b5..9fd98bc00 100644 --- a/bin/drk/Cargo.toml +++ b/bin/drk/Cargo.toml @@ -9,23 +9,31 @@ license = "AGPL-3.0-only" edition = "2021" [dependencies] -anyhow = "1.0.79" -async-std = {version = "1.12.0", features = ["attributes"]} +# Darkfi +darkfi = {path = "../../", features = ["async-daemonize", "bs58", "rpc"]} +darkfi_money_contract = {path = "../../src/contract/money", features = ["no-entrypoint", "client"]} +darkfi_dao_contract = {path = "../../src/contract/dao", features = ["no-entrypoint", "client"]} +darkfi-sdk = {path = "../../src/sdk", features = ["async"]} +darkfi-serial = {path = "../../src/serial"} + +# Misc blake3 = "1.5.0" bs58 = "0.5.0" -clap = {version = "4.4.14", features = ["derive"]} -clap_complete = "4.4.6" -darkfi = {path = "../../", features = ["blockchain", "rpc", "util", "wallet"]} -darkfi-sdk = {path = "../../src/sdk"} -darkfi-serial = {path = "../../src/serial", features = ["derive", "crypto"]} -darkfi-money-contract = {path = "../../src/contract/money", features = ["no-entrypoint", "client"]} -darkfi-dao-contract = {path = "../../src/contract/dao", features = ["no-entrypoint", "client"]} +log = "0.4.20" prettytable-rs = "0.10.0" rand = "0.8.5" -serde_json = "1.0.111" -smol = "1.3.0" -simplelog = "0.12.1" +rodio = {version = "0.17.3", default-features = false, features = ["minimp3"]} +rusqlite = {version = "0.30.0", features = ["sqlcipher"]} +url = "2.5.0" + +# Daemon +easy-parallel = "3.3.1" signal-hook-async-std = "0.2.2" signal-hook = "0.3.17" -url = "2.5.0" -rodio = {version = "0.17.3", default-features = false, features = ["minimp3"]} +simplelog = "0.12.1" +smol = "1.3.0" + +# Argument parsing +serde = {version = "1.0.195", features = ["derive"]} +structopt = "0.3.26" +structopt-toml = "0.5.1" diff --git a/bin/drk2/Makefile b/bin/drk/Makefile similarity index 100% rename from bin/drk2/Makefile rename to bin/drk/Makefile diff --git a/bin/drk/TODO b/bin/drk/TODO deleted file mode 100644 index e8939f208..000000000 --- a/bin/drk/TODO +++ /dev/null @@ -1,4 +0,0 @@ -* Spent coins can be tracked through nullifiers - * So in the wallet, once we broadcast a tx, we can spend a coin - * Also extend the wallet.sql schema to support both confirmed and - unconfirmed coins (use tx hash for that like DAO does) diff --git a/bin/drk2/drk_config.toml b/bin/drk/drk_config.toml similarity index 100% rename from bin/drk2/drk_config.toml rename to bin/drk/drk_config.toml diff --git a/bin/drk/src/cli_util.rs b/bin/drk/src/cli_util.rs index d37711a20..aaf2fcc2e 100644 --- a/bin/drk/src/cli_util.rs +++ b/bin/drk/src/cli_util.rs @@ -15,17 +15,17 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -use std::{io::Cursor, process::exit}; +use std::{io::Cursor, process::exit, str::FromStr}; -use darkfi::{ - util::{async_util::sleep, parse::decode_base10}, - Result, -}; -use darkfi_sdk::crypto::TokenId; use rodio::{source::Source, Decoder, OutputStream}; +use structopt_toml::clap::{App, Arg, Shell, SubCommand}; -use super::Drk; +use darkfi::{cli_desc, system::sleep, util::parse::decode_base10, Error, Result}; +use darkfi_sdk::crypto::TokenId; +use crate::{money::BALANCE_BASE10_DECIMALS, Drk}; + +/// Auxiliary function to parse provided string into a values pair. pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> { let v: Vec<&str> = s.split(':').collect(); if v.len() != 2 { @@ -33,9 +33,8 @@ pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> { exit(1); } - // TODO: We shouldn't be hardcoding everything to 8 decimals. - let val0 = decode_base10(v[0], 8, true); - let val1 = decode_base10(v[1], 8, true); + let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true); + let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true); if val0.is_err() || val1.is_err() { eprintln!("Invalid value pair. Use a pair such as 13.37:11.0"); @@ -45,6 +44,7 @@ pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> { Ok((val0.unwrap(), val1.unwrap())) } +/// Auxiliary function to parse provided string into a tokens pair. pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> { let v: Vec<&str> = s.split(':').collect(); if v.len() != 2 { @@ -85,3 +85,385 @@ pub async fn kaching() { sleep(2).await; } + +/// Auxiliary function to generate provided shell completions. +pub fn generate_completions(shell: &str) -> Result<()> { + // Sub-commands + + // Kaching + let kaching = SubCommand::with_name("kaching").about("Fun"); + + // Ping + let ping = + SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint"); + + // Completions + let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for"); + + let completions = SubCommand::with_name("completions") + .about("Generate a SHELL completion script and print to stdout") + .arg(shell_arg); + + // Wallet + let initialize = + Arg::with_name("initialize").long("initialize").help("Initialize wallet database"); + + let keygen = + Arg::with_name("keygen").long("keygen").help("Generate a new keypair in the wallet"); + + let balance = + Arg::with_name("balance").long("balance").help("Query the wallet for known balances"); + + let address = + Arg::with_name("address").long("address").help("Get the default address in the wallet"); + + let addresses = + Arg::with_name("addresses").long("addresses").help("Print all the addresses in the wallet"); + + let default_address = Arg::with_name("default-address") + .long("default-address") + .takes_value(true) + .help("Set the default address in the wallet"); + + let secrets = + Arg::with_name("secrets").long("secrets").help("Print all the secret keys from the wallet"); + + let import_secrets = Arg::with_name("import-secrets") + .long("import-secrets") + .help("Import secret keys from stdin into the wallet, separated by newlines"); + + let tree = Arg::with_name("tree").long("tree").help("Print the Merkle tree in the wallet"); + + let coins = Arg::with_name("coins").long("coins").help("Print all the coins in the wallet"); + + let wallet = SubCommand::with_name("wallet").about("Wallet operations").args(&vec![ + initialize, + keygen, + balance, + address, + addresses, + default_address, + secrets, + import_secrets, + tree, + coins, + ]); + + // Unspend + let coin = Arg::with_name("coin").help("base58-encoded coin to mark as unspent"); + + let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin); + + // Transfer + let amount = Arg::with_name("amount").help("Amount to send"); + + let token = Arg::with_name("token").help("Token ID to send"); + + let recipient = Arg::with_name("recipient").help("Recipient address"); + + let transfer = SubCommand::with_name("transfer") + .about("Create a payment transaction") + .args(&vec![amount, token, recipient]); + + // Otc + let value_pair = Arg::with_name("value-pair") + .short("v") + .long("value-pair") + .takes_value(true) + .help("Value pair to send:recv (11.55:99.42)"); + + let token_pair = Arg::with_name("token-pair") + .short("t") + .long("token-pair") + .takes_value(true) + .help("Token pair to send:recv (f00:b4r)"); + + let init = SubCommand::with_name("init") + .about("Initialize the first half of the atomic swap") + .args(&vec![value_pair, token_pair]); + + let join = + SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin"); + + let inspect = SubCommand::with_name("inspect") + .about("Inspect a swap half or the full swap tx from stdin"); + + let sign = SubCommand::with_name("sign") + .about("Sign a transaction given from stdin as the first-half"); + + let otc = SubCommand::with_name("otc") + .about("OTC atomic swap") + .subcommands(vec![init, join, inspect, sign]); + + // Inspect + let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin"); + + // Broadcast + let broadcast = + SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it"); + + // Subscribe + let subscribe = SubCommand::with_name("subscribe").about( + "This subscription will listen for incoming blocks from darkfid and look \ + through their transactions to see if there's any that interest us. \ + With `drk` we look at transactions calling the money contract so we can \ + find coins sent to us and fill our wallet with the necessary metadata.", + ); + + // DAO + let proposer_limit = Arg::with_name("proposer-limit") + .help("The minimum amount of governance tokens needed to open a proposal for this DAO"); + + let quorum = Arg::with_name("quorum") + .help("Minimal threshold of participating total tokens needed for a proposal to pass"); + + let approval_ratio = Arg::with_name("approval-ratio") + .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)"); + + let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID"); + + let create = SubCommand::with_name("create").about("Create DAO parameters").args(&vec![ + proposer_limit, + quorum, + approval_ratio, + gov_token_id, + ]); + + let view = SubCommand::with_name("view").about("View DAO data from stdin"); + + let dao_name = Arg::with_name("dao-name").help("Named identifier for the DAO"); + + let import = + SubCommand::with_name("import").about("Import DAO data from stdin").args(&vec![dao_name]); + + let dao_alias = Arg::with_name("dao-alias").help("Numeric identifier for the DAO (optional)"); + + let list = SubCommand::with_name("list") + .about("List imported DAOs (or info about a specific one)") + .args(&vec![dao_alias]); + + let dao_alias = Arg::with_name("dao-alias").help("Name or numeric identifier for the DAO"); + + let balance = SubCommand::with_name("balance") + .about("Show the balance of a DAO") + .args(&vec![dao_alias.clone()]); + + let mint = SubCommand::with_name("mint") + .about("Mint an imported DAO on-chain") + .args(&vec![dao_alias.clone()]); + + let recipient = + Arg::with_name("recipient").help("Pubkey to send tokens to with proposal success"); + + let amount = Arg::with_name("amount").help("Amount to send from DAO with proposal success"); + + let token = Arg::with_name("token").help("Token ID to send from DAO with proposal success"); + + let propose = SubCommand::with_name("propose") + .about("Create a proposal for a DAO") + .args(&vec![dao_alias.clone(), recipient, amount, token]); + + let proposals = SubCommand::with_name("proposals") + .about("List DAO proposals") + .args(&vec![dao_alias.clone()]); + + let proposal_id = Arg::with_name("proposal-id").help("Numeric identifier for the proposal"); + + let proposal = SubCommand::with_name("proposal") + .about("View a DAO proposal data") + .args(&vec![dao_alias.clone(), proposal_id.clone()]); + + let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)"); + + let vote_weight = + Arg::with_name("vote-weight").help("Vote weight (amount of governance tokens)"); + + let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![ + dao_alias.clone(), + proposal_id.clone(), + vote, + vote_weight, + ]); + + let exec = SubCommand::with_name("exec") + .about("Execute a DAO proposal") + .args(&vec![dao_alias, proposal_id]); + + let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![ + create, view, import, list, balance, mint, propose, proposals, proposal, vote, exec, + ]); + + // Scan + let reset = Arg::with_name("reset") + .long("reset") + .help("Reset Merkle tree and start scanning from first block"); + + let list = Arg::with_name("list").long("list").help("List all available checkpoints"); + + let checkpoint = Arg::with_name("checkpoint") + .long("checkpoint") + .takes_value(true) + .help("Reset Merkle tree to checkpoint index and start scanning"); + + let scan = SubCommand::with_name("scan") + .about("Scan the blockchain and parse relevant transactions") + .args(&vec![reset, list, checkpoint]); + + // Explorer + let tx_hash = Arg::with_name("tx-hash").help("Transaction hash"); + + let full = Arg::with_name("full").long("full").help("Print the full transaction information"); + + let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base58"); + + let fetch_tx = SubCommand::with_name("fetch-tx") + .about("Fetch a blockchain transaction by hash") + .args(&vec![tx_hash, full, encode]); + + let simulate_tx = + SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it"); + + let tx_hash = Arg::with_name("tx-hash").help("Transaction hash"); + + let encode = Arg::with_name("encode") + .long("encode") + .help("Encode specific history record transaction to base58"); + + let txs_history = SubCommand::with_name("txs-history") + .about("Fetch broadcasted transactions history") + .args(&vec![tx_hash, encode]); + + let explorer = SubCommand::with_name("explorer") + .about("Explorer related subcommands") + .subcommands(vec![fetch_tx, simulate_tx, txs_history]); + + // Alias + let alias = Arg::with_name("alias").help("Token alias"); + + let token = Arg::with_name("token").help("Token to create alias for"); + + let add = SubCommand::with_name("add").about("Create a Token alias").args(&vec![alias, token]); + + let alias = Arg::with_name("alias") + .short("a") + .long("alias") + .takes_value(true) + .help("Token alias to search for"); + + let token = Arg::with_name("token") + .short("t") + .long("token") + .takes_value(true) + .help("Token to search alias for"); + + let show = SubCommand::with_name("show") + .about( + "Print alias info of optional arguments. \ + If no argument is provided, list all the aliases in the wallet.", + ) + .args(&vec![alias, token]); + + let alias = Arg::with_name("alias").help("Token alias to remove"); + + let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias); + + let alias = SubCommand::with_name("alias") + .about("Manage Token aliases") + .subcommands(vec![add, show, remove]); + + // Token + let import = SubCommand::with_name("import").about("Import a mint authority secret from stdin"); + + let generate_mint = + SubCommand::with_name("generate-mint").about("Generate a new mint authority"); + + let list = + SubCommand::with_name("list").about("List token IDs with available mint authorities"); + + let token = Arg::with_name("token").help("Token ID to mint"); + + let amount = Arg::with_name("amount").help("Amount to mint"); + + let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens"); + + let mint = + SubCommand::with_name("mint").about("Mint tokens").args(&vec![token, amount, recipient]); + + let token = Arg::with_name("token").help("Token ID to freeze"); + + let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token); + + let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![ + import, + generate_mint, + list, + mint, + freeze, + ]); + + // Main arguments + let config = Arg::with_name("config") + .short("c") + .long("config") + .takes_value(true) + .help("Configuration file to use"); + + let wallet_path = Arg::with_name("wallet_path") + .long("wallet-path") + .takes_value(true) + .help("Path to wallet database"); + + let wallet_pass = Arg::with_name("wallet_pass") + .long("wallet-pass") + .takes_value(true) + .help("Password for the wallet database"); + + let endpoint = Arg::with_name("endpoint") + .short("e") + .long("endpoint") + .takes_value(true) + .help("darkfid JSON-RPC endpoint"); + + let command = vec![ + kaching, + ping, + completions, + wallet, + unspend, + transfer, + otc, + inspect, + broadcast, + subscribe, + dao, + scan, + explorer, + alias, + token, + ]; + + let log = Arg::with_name("log") + .short("l") + .long("log") + .takes_value(true) + .help("Set log file to ouput into"); + + let verbose = Arg::with_name("verbose") + .short("v") + .multiple(true) + .help("Increase verbosity (-vvv supported)"); + + let mut app = App::new("drk") + .about(cli_desc!()) + .args(&vec![config, wallet_path, wallet_pass, endpoint, log, verbose]) + .subcommands(command); + + let shell = match Shell::from_str(shell) { + Ok(s) => s, + Err(e) => return Err(Error::Custom(e)), + }; + + app.gen_completions_to("./drk", shell, &mut std::io::stdout()); + + Ok(()) +} diff --git a/bin/drk2/src/dao.rs b/bin/drk/src/dao.rs similarity index 100% rename from bin/drk2/src/dao.rs rename to bin/drk/src/dao.rs diff --git a/bin/drk/src/deploy_contract.rs b/bin/drk/src/deploy_contract.rs deleted file mode 100644 index faa1f1e45..000000000 --- a/bin/drk/src/deploy_contract.rs +++ /dev/null @@ -1,181 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use std::{ - env::set_current_dir, - fs::{read, read_dir, read_to_string, File}, - io::{ErrorKind, Write}, - path::{Path, PathBuf}, - str::FromStr, -}; - -use rand::{rngs::OsRng, RngCore}; - -use darkfi::{ - crypto::keypair::SecretKey, - node::{MemoryState, State}, - runtime::vm_runtime::Runtime, - util::cli::{fg_green, fg_red}, - zkas::ZkBinary, - Error, Result, -}; - -const CIRCUIT_DIR_NAME: &str = "proof"; -const CONTRACT_FILE_NAME: &str = "contract.wasm"; -const DEPLOY_KEY_NAME: &str = "deploy.key"; - -/// Creates a new deploy key used for deploying private smart contracts. -/// This key allows to update the wasm code and the zk circuits on chain -/// by creating a signature. When deployed, the contract can be accessed -/// by requesting the public counterpart of this secret key. -pub fn create_deploy_key(mut rng: impl RngCore, path: &Path) -> Result { - let secret = SecretKey::random(&mut rng); - let mut file = File::create(path)?; - file.write_all(bs58::encode(&secret.to_bytes()).into_string().as_bytes())?; - Ok(secret) -} - -/// Reads a deploy key from a file on the filesystem and returns it. -fn read_deploy_key(s: &Path) -> core::result::Result { - eprintln!("Trying to read deploy key from file: {:?}", s); - let contents = read_to_string(s)?; - let secret = SecretKey::from_str(&contents).unwrap(); - Ok(secret) -} - -/// Creates necessary data to deploy a given smart contract on the network. -/// For consistency, we point this function to a directory where our smart -/// contract and the compiled circuits are contained. This is going to give -/// us a uniform approach to scm and gives a generic layout of the source: -/// ```text -/// smart-contract -/// ├── Cargo.toml -/// ├── deploy.key -/// ├── Makefile -/// ├── proof -/// │   ├── circuit0.zk -/// │   ├── circuit0.zk.bin -/// │   ├── circuit1.zk -/// │   └── circuit1.zk.bin -/// ├── contract.wasm -/// ├── src -/// │   └── lib.rs -/// └── tests -/// ``` -//pub fn create_deploy_data(path: &Path) -> Result { -pub fn create_deploy_data(path: &Path) -> Result<()> { - // Try to chdir into the contract directory - if let Err(e) = set_current_dir(path) { - eprintln!("Failed to chdir into {:?}", path); - return Err(e.into()) - } - - let deploy_key: SecretKey; - - let deploy_key = match read_deploy_key(&PathBuf::from(DEPLOY_KEY_NAME)) { - Ok(v) => deploy_key = v, - Err(e) => { - if e.kind() == ErrorKind::NotFound { - // We didn't find a deploy key, generate a new one. - eprintln!("Did not find an existing key, creating a new one."); - match create_deploy_key(&mut OsRng, &PathBuf::from(DEPLOY_KEY_NAME)) { - Ok(v) => { - eprintln!("Created new deploy key in \"{}\".", DEPLOY_KEY_NAME); - deploy_key = v; - } - Err(e) => { - eprintln!("Failed to create new deploy key"); - return Err(e) - } - } - } - eprintln!("Failed to read deploy key"); - return Err(e.into()) - } - }; - - // Search for ZK circuits in the directory. If none are found, we'll bail. - // The logic searches for `.zk.bin` files created by zkas. - eprintln!("Searching for compiled ZK circuits in \"{}\" ...", CIRCUIT_DIR_NAME); - let mut circuits = vec![]; - for i in read_dir(CIRCUIT_DIR_NAME)? { - if let Err(e) = i { - eprintln!("Error iterating over \"{}\" directory", CIRCUIT_DIR_NAME); - return Err(e.into()) - } - - let f = i.unwrap(); - let fname = f.file_name(); - let fname = fname.to_str().unwrap(); - - if fname.ends_with(".zk.bin") { - // Validate that the files can be properly decoded - eprintln!("{} {}", fg_green("Found:"), f.path().display()); - let buf = read(f.path())?; - if let Err(e) = ZkBinary::decode(&buf) { - eprintln!("{} Failed to decode zkas bincode in {:?}", fg_red("Error:"), f.path()); - return Err(e) - } - - circuits.push(buf.clone()); - } - } - - if circuits.is_empty() { - return Err(Error::Custom("Found no valid ZK circuits".to_string())) - } - - /* FIXME - // Validate wasm binary. We inspect the bincode and try to load it into - // the wasm runtime. If loaded, we then look for the `ENTRYPOINT` function - // which we hardcode into our sdk and runtime and is the canonical way to - // run wasm binaries on chain. - eprintln!("Inspecting wasm binary in \"{}\"", CONTRACT_FILE_NAME); - let wasm_bytes = read(CONTRACT_FILE_NAME)?; - eprintln!("Initializing moch wasm runtime to check validity"); - let runtime = match Runtime::new(&wasm_bytes, MemoryState::new(State::dummy()?)) { - Ok(v) => { - eprintln!("Found {} wasm binary", fg_green("valid")); - v - } - Err(e) => { - eprintln!("Failed to initialize wasm runtime"); - return Err(e) - } - }; - - eprintln!("Looking for entrypoint function inside the wasm"); - let cs = ContractSection::Exec; - if let Err(e) = runtime.instance.exports.get_function(cs.name()) { - eprintln!("{} Could not find entrypoint function", fg_red("Error:")); - return Err(e.into()) - } - - // TODO: Create a ZK proof enforcing the deploy key relations with their public - // counterparts (public key and contract address) - let mut total_bytes = 0; - total_bytes += wasm_bytes.len(); - for circuit in circuits { - total_bytes += circuit.len(); - } - */ - - // TODO: Return the data back to the main function, and work further in creating - // a transaction and broadcasting it. - Ok(()) -} diff --git a/bin/drk2/src/error.rs b/bin/drk/src/error.rs similarity index 100% rename from bin/drk2/src/error.rs rename to bin/drk/src/error.rs diff --git a/bin/drk/src/main.rs b/bin/drk/src/main.rs index 7f8f59155..cae719b1b 100644 --- a/bin/drk/src/main.rs +++ b/bin/drk/src/main.rs @@ -17,92 +17,114 @@ */ use std::{ + fs, io::{stdin, Read}, process::exit, str::FromStr, + sync::Arc, time::Instant, }; -use anyhow::{anyhow, Context, Result}; -use clap::{CommandFactory, Parser, Subcommand}; -use clap_complete::{generate, Shell}; -use darkfi::{tx::Transaction, util::parse::decode_base10, zk::halo2::Field}; +use prettytable::{format, row, Table}; +use rand::rngs::OsRng; +use smol::stream::StreamExt; +use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml}; +use url::Url; + +use darkfi::{ + async_daemonize, cli_desc, + rpc::{client::RpcClient, jsonrpc::JsonRequest, util::JsonValue}, + tx::Transaction, + util::{ + parse::{decode_base10, encode_base10}, + path::expand_path, + }, + zk::halo2::Field, + Result, +}; use darkfi_money_contract::model::Coin; use darkfi_sdk::{ crypto::{PublicKey, SecretKey, TokenId}, pasta::{group::ff::PrimeField, pallas}, }; use darkfi_serial::{deserialize, serialize}; -use prettytable::{format, row, Table}; -use rand::rngs::OsRng; -use serde_json::json; -use simplelog::{ColorChoice, TermLogger, TerminalMode}; -use url::Url; -use darkfi::{ - cli_desc, - rpc::{client::RpcClient, jsonrpc::JsonRequest}, - util::{ - cli::{get_log_config, get_log_level}, - parse::encode_base10, - }, -}; +/// Error codes +mod error; -/// Airdrop methods -mod rpc_airdrop; +/// darkfid JSON-RPC related methods +mod rpc; /// Payment methods -mod rpc_transfer; +mod transfer; /// Swap methods -mod rpc_swap; -use rpc_swap::PartialSwapData; - -/// DAO methods -mod rpc_dao; +mod swap; +use swap::PartialSwapData; /// Token methods -mod rpc_token; - -/// Blockchain methods -mod rpc_blockchain; +mod token; /// CLI utility functions mod cli_util; -use cli_util::{kaching, parse_token_pair, parse_value_pair}; - -/// Wallet functionality related to drk operations -mod wallet; - -/// Wallet functionality related to DAO -mod wallet_dao; -use wallet_dao::DaoParams; +use cli_util::{generate_completions, kaching, parse_token_pair, parse_value_pair}; /// Wallet functionality related to Money -mod wallet_money; +mod money; +use money::BALANCE_BASE10_DECIMALS; -/// Wallet functionality related to arbitrary tokens -mod wallet_token; +/// Wallet functionality related to Dao +mod dao; +use dao::DaoParams; /// Wallet functionality related to transactions history -mod wallet_txs_history; +mod txs_history; -#[derive(Parser)] -#[command(about = cli_desc!())] +/// Wallet database operations handler +mod walletdb; +use walletdb::{WalletDb, WalletPtr}; + +const CONFIG_FILE: &str = "drk_config.toml"; +const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml"); + +// Dev Note: when adding/modifying args here, +// don't forget to update cli_util::generate_completions() +#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)] +#[serde(default)] +#[structopt(name = "drk", about = cli_desc!())] struct Args { - #[arg(short, action = clap::ArgAction::Count)] - /// Increase verbosity (-vvv supported) - verbose: u8, + #[structopt(short, long)] + /// Configuration file to use + config: Option, - #[arg(short, long, default_value = "tcp://127.0.0.1:8340")] + #[structopt(long, default_value = "~/.local/darkfi/drk/wallet.db")] + /// Path to wallet database + wallet_path: String, + + #[structopt(long, default_value = "changeme")] + /// Password for the wallet database + wallet_pass: String, + + #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")] /// darkfid JSON-RPC endpoint endpoint: Url, - #[command(subcommand)] + #[structopt(subcommand)] + /// Sub command to execute command: Subcmd, + + #[structopt(short, long)] + /// Set log file to ouput into + log: Option, + + #[structopt(short, parse(from_occurrences))] + /// Increase verbosity (-vvv supported) + verbose: u8, } -#[derive(Subcommand)] +// Dev Note: when adding/modifying commands here, +// don't forget to update cli_util::generate_completions() +#[derive(Clone, Debug, Deserialize, StructOpt)] enum Subcmd { /// Fun Kaching, @@ -113,40 +135,48 @@ enum Subcmd { /// Generate a SHELL completion script and print to stdout Completions { /// The Shell you want to generate script for - shell: Shell, + shell: String, }, /// Wallet operations Wallet { - #[arg(long)] - /// Initialize wallet with data for Money Contract (run this first) + #[structopt(long)] + /// Initialize wallet database initialize: bool, - #[arg(long)] + #[structopt(long)] /// Generate a new keypair in the wallet keygen: bool, - #[arg(long)] + #[structopt(long)] /// Query the wallet for known balances balance: bool, - #[arg(long)] + #[structopt(long)] /// Get the default address in the wallet address: bool, - #[arg(long)] + #[structopt(long)] + /// Print all the addresses in the wallet + addresses: bool, + + #[structopt(long)] + /// Set the default address in the wallet + default_address: Option, + + #[structopt(long)] /// Print all the secret keys from the wallet secrets: bool, - #[arg(long)] + #[structopt(long)] /// Import secret keys from stdin into the wallet, separated by newlines import_secrets: bool, - #[arg(long)] + #[structopt(long)] /// Print the Merkle tree in the wallet tree: bool, - #[arg(long)] + #[structopt(long)] /// Print all the coins in the wallet coins: bool, }, @@ -157,19 +187,6 @@ enum Subcmd { coin: String, }, - /// Airdrop some tokens - Airdrop { - /// Faucet JSON-RPC endpoint - #[arg(short, long, default_value = "tls://faucetd.testnet.dark.fi:18340")] - faucet_endpoint: Url, - - /// Amount to request from the faucet - amount: String, - - /// Optional address to send tokens to (defaults to main address in wallet) - address: Option, - }, - /// Create a payment transaction Transfer { /// Amount to send @@ -180,18 +197,14 @@ enum Subcmd { /// Recipient address recipient: String, - - /// Mark if this is being sent to a DAO - #[clap(long)] - dao: bool, - - /// DAO bulla, if the tokens are being sent to a DAO - dao_bulla: Option, }, /// OTC atomic swap - #[command(subcommand)] - Otc(OtcSubcmd), + Otc { + #[structopt(subcommand)] + /// Sub command to execute + command: OtcSubcmd, + }, /// Inspect a transaction from stdin Inspect, @@ -199,52 +212,66 @@ enum Subcmd { /// Read a transaction from stdin and broadcast it Broadcast, - /// Subscribe to incoming notifications from darkfid - #[command(subcommand)] - Subscribe(SubscribeSubcmd), + /// This subscription will listen for incoming blocks from darkfid and look + /// through their transactions to see if there's any that interest us. + /// With `drk` we look at transactions calling the money contract so we can + /// find coins sent to us and fill our wallet with the necessary metadata. + Subscribe, /// DAO functionalities - #[command(subcommand)] - Dao(DaoSubcmd), + Dao { + #[structopt(subcommand)] + /// Sub command to execute + command: DaoSubcmd, + }, /// Scan the blockchain and parse relevant transactions Scan { - #[arg(long)] - /// Reset Merkle tree and start scanning from first slot + #[structopt(long)] + /// Reset Merkle tree and start scanning from first block reset: bool, - #[arg(long)] + #[structopt(long)] /// List all available checkpoints list: bool, - #[arg(short, long)] + #[structopt(long)] /// Reset Merkle tree to checkpoint index and start scanning checkpoint: Option, }, /// Explorer related subcommands - #[command(subcommand)] - Explorer(ExplorerSubcmd), + Explorer { + #[structopt(subcommand)] + /// Sub command to execute + command: ExplorerSubcmd, + }, /// Manage Token aliases - #[command(subcommand)] - Alias(AliasSubcmd), + Alias { + #[structopt(subcommand)] + /// Sub command to execute + command: AliasSubcmd, + }, /// Token functionalities - #[command(subcommand)] - Token(TokenSubcmd), + Token { + #[structopt(subcommand)] + /// Sub command to execute + command: TokenSubcmd, + }, } -#[derive(Subcommand)] +#[derive(Clone, Debug, Deserialize, StructOpt)] enum OtcSubcmd { /// Initialize the first half of the atomic swap Init { /// Value pair to send:recv (11.55:99.42) - #[clap(short, long)] + #[structopt(short, long)] value_pair: String, /// Token pair to send:recv (f00:b4r) - #[clap(short, long)] + #[structopt(short, long)] token_pair: String, }, @@ -258,7 +285,7 @@ enum OtcSubcmd { Sign, } -#[derive(Subcommand)] +#[derive(Clone, Debug, Deserialize, StructOpt)] enum DaoSubcmd { /// Create DAO parameters Create { @@ -266,7 +293,7 @@ enum DaoSubcmd { proposer_limit: String, /// Minimal threshold of participating total tokens needed for a proposal to pass quorum: String, - /// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals), + /// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals) approval_ratio: f64, /// DAO's governance token ID gov_token_id: String, @@ -354,18 +381,18 @@ enum DaoSubcmd { }, } -#[derive(Subcommand)] +#[derive(Clone, Debug, Deserialize, StructOpt)] enum ExplorerSubcmd { /// Fetch a blockchain transaction by hash FetchTx { /// Transaction hash tx_hash: String, - #[arg(long)] + #[structopt(long)] /// Print the full transaction information full: bool, - #[arg(long)] + #[structopt(long)] /// Encode transaction to base58 encode: bool, }, @@ -378,14 +405,13 @@ enum ExplorerSubcmd { /// Fetch specific history record (optional) tx_hash: Option, - #[arg(long)] - /// Encode specific history record transaction - /// to base58. + #[structopt(long)] + /// Encode specific history record transaction to base58 encode: bool, }, } -#[derive(Subcommand)] +#[derive(Clone, Debug, Deserialize, StructOpt)] enum AliasSubcmd { /// Create a Token alias Add { @@ -400,11 +426,11 @@ enum AliasSubcmd { /// If no argument is provided, list all the aliases in the wallet. Show { /// Token alias to search for - #[clap(short, long)] + #[structopt(short, long)] alias: Option, /// Token to search alias for - #[clap(short, long)] + #[structopt(short, long)] token: Option, }, @@ -415,7 +441,7 @@ enum AliasSubcmd { }, } -#[derive(Subcommand)] +#[derive(Clone, Debug, Deserialize, StructOpt)] enum TokenSubcmd { /// Import a mint authority secret from stdin Import, @@ -440,55 +466,73 @@ enum TokenSubcmd { /// Freeze a token mint Freeze { - /// Token ID mint to freeze + /// Token ID to freeze token: String, }, } -#[derive(Subcommand)] -enum SubscribeSubcmd { - /// This subscription will listen for incoming blocks from darkfid and look - /// through their transactions to see if there's any that interest us. - /// With `drk` we look at transactions calling the money contract so we can - /// find coins sent to us and fill our wallet with the necessary metadata. - Blocks, - - /// This subscription will listen for erroneous transactions that got - /// removed from darkfid mempool. - Transactions, -} - +/// CLI-util structure pub struct Drk { + /// Wallet database operations handler + pub wallet: WalletPtr, + /// JSON-RPC client to execute requests to darkfid daemon pub rpc_client: RpcClient, } impl Drk { - async fn new(endpoint: Url) -> Result { - let rpc_client = RpcClient::new(endpoint, None).await?; - Ok(Self { rpc_client }) + async fn new( + wallet_path: String, + wallet_pass: String, + endpoint: Url, + ex: Arc>, + ) -> Result { + // Initialize wallet + let wallet_path = expand_path(&wallet_path)?; + if !wallet_path.exists() { + if let Some(parent) = wallet_path.parent() { + fs::create_dir_all(parent)?; + } + } + let wallet = match WalletDb::new(Some(wallet_path), Some(&wallet_pass)) { + Ok(w) => w, + Err(e) => { + eprintln!("Error initializing wallet: {e:?}"); + exit(2); + } + }; + + // Initialize rpc client + let rpc_client = RpcClient::new(endpoint, ex).await?; + + Ok(Self { wallet, rpc_client }) } + /// Initialize wallet with tables for drk + async fn initialize_wallet(&self) -> Result<()> { + let wallet_schema = include_str!("../wallet.sql"); + if let Err(e) = self.wallet.exec_batch_sql(wallet_schema).await { + eprintln!("Error initializing wallet: {e:?}"); + exit(2); + } + + Ok(()) + } + + /// Auxilliary function to ping configured darkfid daemon for liveness. async fn ping(&self) -> Result<()> { + eprintln!("Executing ping request to darkfid..."); let latency = Instant::now(); - let req = JsonRequest::new("ping", json!([])); + let req = JsonRequest::new("ping", JsonValue::Array(vec![])); let rep = self.rpc_client.oneshot_request(req).await?; let latency = latency.elapsed(); - println!("Got reply: {}", rep); - println!("Latency: {:?}", latency); + eprintln!("Got reply: {rep:?}"); + eprintln!("Latency: {latency:?}"); Ok(()) } } -#[async_std::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - if args.verbose > 0 { - let log_level = get_log_level(args.verbose); - let log_config = get_log_config(args.verbose); - TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?; - } - +async_daemonize!(realmain); +async fn realmain(args: Args, ex: Arc>) -> Result<()> { match args.command { Subcmd::Kaching => { kaching().await; @@ -496,23 +540,19 @@ async fn main() -> Result<()> { } Subcmd::Ping => { - let drk = Drk::new(args.endpoint).await?; - drk.ping().await.with_context(|| "Failed to ping darkfid RPC endpoint")?; - - Ok(()) + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + drk.ping().await } - Subcmd::Completions { shell } => { - let mut cmd = Args::command(); - generate(shell, &mut cmd, "./drk", &mut std::io::stdout()); - Ok(()) - } + Subcmd::Completions { shell } => generate_completions(&shell), Subcmd::Wallet { initialize, keygen, balance, address, + addresses, + default_address, secrets, import_secrets, tree, @@ -522,6 +562,8 @@ async fn main() -> Result<()> { !keygen && !balance && !address && + !addresses && + default_address.is_none() && !secrets && !tree && !coins && @@ -532,28 +574,33 @@ async fn main() -> Result<()> { exit(2); } - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; if initialize { drk.initialize_wallet().await?; - drk.initialize_money().await?; - drk.initialize_dao().await?; + if let Err(e) = drk.initialize_money().await { + eprintln!("Failed to initialize Money: {e:?}"); + exit(2); + } + if let Err(e) = drk.initialize_dao().await { + eprintln!("Failed to initialize DAO: {e:?}"); + exit(2); + } return Ok(()) } if keygen { - drk.money_keygen().await.with_context(|| "Failed to generate keypair")?; + if let Err(e) = drk.money_keygen().await { + eprintln!("Failed to generate keypair: {e:?}"); + exit(2); + } return Ok(()) } if balance { - let balmap = - drk.money_balance().await.with_context(|| "Failed to fetch wallet balance")?; + let balmap = drk.money_balance().await?; - let aliases_map = drk - .get_aliases_mapped_by_token() - .await - .with_context(|| "Failed to fetch wallet aliases")?; + let aliases_map = drk.get_aliases_mapped_by_token().await?; // Create a prettytable with the new data: let mut table = Table::new(); @@ -565,40 +612,73 @@ async fn main() -> Result<()> { None => "-", }; - // FIXME: Don't hardcode to 8 decimals - table.add_row(row![token_id, aliases, encode_base10(*balance, 8)]); + table.add_row(row![ + token_id, + aliases, + encode_base10(*balance, BALANCE_BASE10_DECIMALS) + ]); } if table.is_empty() { - println!("No unspent balances found"); + eprintln!("No unspent balances found"); } else { - println!("{}", table); + eprintln!("{table}"); } return Ok(()) } if address { - let address = drk - .wallet_address(1) // <-- TODO: Use is_default from the sql table - .await - .with_context(|| "Failed to fetch default address")?; + let address = match drk.default_address().await { + Ok(a) => a, + Err(e) => { + eprintln!("Failed to fetch default address: {e:?}"); + exit(2); + } + }; - println!("{}", address); + eprintln!("{address}"); return Ok(()) } - if secrets { - let v = drk - .get_money_secrets() - .await - .with_context(|| "Failed to fetch wallet secrets")?; + if addresses { + let addresses = drk.addresses().await?; - drk.rpc_client.close().await?; + // Create a prettytable with the new data: + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.set_titles(row!["Key ID", "Public Key", "Secret Key", "Is Default"]); + for (key_id, public_key, secret_key, is_default) in addresses { + let is_default = match is_default { + 1 => "*", + _ => "", + }; + table.add_row(row![key_id, public_key, secret_key, is_default]); + } + + if table.is_empty() { + eprintln!("No addresses found"); + } else { + eprintln!("{table}"); + } + + return Ok(()) + } + + if let Some(idx) = default_address { + if let Err(e) = drk.set_default_address(idx).await { + eprintln!("Failed to set default address: {e:?}"); + exit(2); + } + return Ok(()) + } + + if secrets { + let v = drk.get_money_secrets().await?; for i in v { - println!("{}", i); + eprintln!("{i}"); } return Ok(()) @@ -611,49 +691,40 @@ async fn main() -> Result<()> { if let Ok(line) = line { let bytes = bs58::decode(&line.trim()).into_vec()?; let Ok(secret) = deserialize(&bytes) else { - eprintln!("Warning: Failed to deserialize secret on line {}", i); + eprintln!("Warning: Failed to deserialize secret on line {i}"); continue }; secrets.push(secret); } } - let pubkeys = drk - .import_money_secrets(secrets) - .await - .with_context(|| "Failed to import secret keys into wallet")?; - - drk.rpc_client.close().await?; + let pubkeys = match drk.import_money_secrets(secrets).await { + Ok(p) => p, + Err(e) => { + eprintln!("Failed to import secret keys into wallet: {e:?}"); + exit(2); + } + }; for key in pubkeys { - println!("{}", key); + eprintln!("{key}"); } return Ok(()) } if tree { - let v = - drk.get_money_tree().await.with_context(|| "Failed to fetch Merkle tree")?; - drk.rpc_client.close().await?; + let tree = drk.get_money_tree().await?; - println!("{:#?}", v); + eprintln!("{tree:#?}"); return Ok(()) } if coins { - let coins = drk - .get_coins(true) - .await - .with_context(|| "Failed to fetch coins from wallet")?; + let coins = drk.get_coins(true).await?; - let aliases_map = drk - .get_aliases_mapped_by_token() - .await - .with_context(|| "Failed to fetch wallet aliases")?; - - drk.rpc_client.close().await?; + let aliases_map = drk.get_aliases_mapped_by_token().await?; if coins.is_empty() { return Ok(()) @@ -700,7 +771,7 @@ async fn main() -> Result<()> { ]); } - println!("{}", table); + eprintln!("{table}"); return Ok(()) } @@ -709,69 +780,84 @@ async fn main() -> Result<()> { } Subcmd::Unspend { coin } => { - let bytes: [u8; 32] = bs58::decode(&coin).into_vec()?.try_into().unwrap(); + let bytes: [u8; 32] = match bs58::decode(&coin).into_vec()?.try_into() { + Ok(b) => b, + Err(e) => { + eprintln!("Invalid coin: {e:?}"); + exit(2); + } + }; let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { Some(v) => v, - None => return Err(anyhow!("Invalid coin")), + None => { + eprintln!("Invalid coin"); + exit(2); + } }; let coin = Coin::from(elem); - let drk = Drk::new(args.endpoint).await?; - drk.unspend_coin(&coin).await.with_context(|| "Failed to mark coin as unspent")?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + if let Err(e) = drk.unspend_coin(&coin).await { + eprintln!("Failed to mark coin as unspent: {e:?}"); + exit(2); + } Ok(()) } - Subcmd::Airdrop { faucet_endpoint, amount, address } => { - let amount = f64::from_str(&amount).with_context(|| "Invalid amount")?; - let drk = Drk::new(args.endpoint).await?; + Subcmd::Transfer { amount, token, recipient } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let address = match address { - Some(v) => PublicKey::from_str(v.as_str()).with_context(|| "Invalid address")?, - None => drk.wallet_address(1).await.with_context(|| { - "Failed to fetch default address, perhaps the wallet was not initialized?" - })?, + if let Err(e) = f64::from_str(&amount) { + eprintln!("Invalid amount: {e:?}"); + exit(2); + } + + let rcpt = match PublicKey::from_str(&recipient) { + Ok(r) => r, + Err(e) => { + eprintln!("Invalid recipient: {e:?}"); + exit(2); + } }; - let txid = drk - .request_airdrop(faucet_endpoint, amount, address) - .await - .with_context(|| "Failed to request airdrop")?; + let token_id = match drk.get_token(token).await { + Ok(t) => t, + Err(e) => { + eprintln!("Invalid token alias: {e:?}"); + exit(2); + } + }; - println!("Transaction ID: {}", txid); - - Ok(()) - } - - Subcmd::Transfer { amount, token, recipient, dao, dao_bulla } => { - let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?; - let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?; - let drk = Drk::new(args.endpoint).await?; - let token_id = drk.get_token(token).await.with_context(|| "Invalid token alias")?; - - let tx = drk - .transfer(&amount, token_id, rcpt, dao, dao_bulla) - .await - .with_context(|| "Failed to create payment transaction")?; + let tx = match drk.transfer(&amount, token_id, rcpt).await { + Ok(t) => t, + Err(e) => { + eprintln!("Failed to create payment transaction: {e:?}"); + exit(2); + } + }; println!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) } - Subcmd::Otc(cmd) => { - let drk = Drk::new(args.endpoint).await?; + Subcmd::Otc { command } => { + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - match cmd { + match command { OtcSubcmd::Init { value_pair, token_pair } => { let (vp_send, vp_recv) = parse_value_pair(&value_pair)?; let (tp_send, tp_recv) = parse_token_pair(&drk, &token_pair).await?; - let half = drk - .init_swap(vp_send, tp_send, vp_recv, tp_recv) - .await - .with_context(|| "Failed to create swap transaction half")?; + let half = match drk.init_swap(vp_send, tp_send, vp_recv, tp_recv).await { + Ok(h) => h, + Err(e) => { + eprintln!("Failed to create swap transaction half: {e:?}"); + exit(2); + } + }; println!("{}", bs58::encode(&serialize(&half)).into_string()); Ok(()) @@ -783,10 +869,13 @@ async fn main() -> Result<()> { let bytes = bs58::decode(&buf.trim()).into_vec()?; let partial: PartialSwapData = deserialize(&bytes)?; - let tx = drk - .join_swap(partial) - .await - .with_context(|| "Failed to create a join swap transaction")?; + let tx = match drk.join_swap(partial).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to create a join swap transaction: {e:?}"); + exit(2); + } + }; println!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) @@ -797,7 +886,11 @@ async fn main() -> Result<()> { stdin().read_to_string(&mut buf)?; let bytes = bs58::decode(&buf.trim()).into_vec()?; - drk.inspect_swap(bytes).await.with_context(|| "Failed to inspect swap")?; + if let Err(e) = drk.inspect_swap(bytes).await { + eprintln!("Failed to inspect swap: {e:?}"); + exit(2); + }; + Ok(()) } @@ -807,9 +900,10 @@ async fn main() -> Result<()> { let bytes = bs58::decode(&buf.trim()).into_vec()?; let mut tx: Transaction = deserialize(&bytes)?; - drk.sign_swap(&mut tx) - .await - .with_context(|| "Failed to sign joined swap transaction")?; + if let Err(e) = drk.sign_swap(&mut tx).await { + eprintln!("Failed to sign joined swap transaction: {e:?}"); + exit(2); + }; println!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) @@ -817,103 +911,36 @@ async fn main() -> Result<()> { } } - Subcmd::Inspect => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let tx: Transaction = deserialize(&bytes)?; - println!("{:#?}", tx); - Ok(()) - } - - Subcmd::Broadcast => { - eprintln!("Reading transaction from stdin..."); - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let tx = deserialize(&bytes)?; - - let drk = Drk::new(args.endpoint).await?; - - let txid = - drk.broadcast_tx(&tx).await.with_context(|| "Failed to broadcast transaction")?; - - println!("Transaction ID: {}", txid); - - Ok(()) - } - - Subcmd::Subscribe(cmd) => match cmd { - SubscribeSubcmd::Blocks => { - let drk = Drk::new(args.endpoint.clone()).await?; - - drk.subscribe_blocks(args.endpoint.clone()) - .await - .with_context(|| "Block subscription failed")?; - - Ok(()) - } - - SubscribeSubcmd::Transactions => { - let drk = Drk::new(args.endpoint.clone()).await?; - - drk.subscribe_err_txs(args.endpoint) - .await - .with_context(|| "Erroneous transactions subscription failed")?; - - Ok(()) - } - }, - - Subcmd::Scan { reset, list, checkpoint } => { - let drk = Drk::new(args.endpoint).await?; - - if reset { - eprintln!("Reset requested."); - drk.scan_blocks(true).await.with_context(|| "Failed during scanning")?; - - return Ok(()) - } - - if list { - eprintln!("List requested."); - // TODO: implement - - return Ok(()) - } - - if let Some(c) = checkpoint { - eprintln!("Checkpoint requested: {}", c); - // TODO: implement - - return Ok(()) - } - - drk.scan_blocks(false).await.with_context(|| "Failed during scanning")?; - eprintln!("Finished scanning blockchain"); - - Ok(()) - } - - Subcmd::Dao(cmd) => match cmd { + Subcmd::Dao { command } => match command { DaoSubcmd::Create { proposer_limit, quorum, approval_ratio, gov_token_id } => { - let _ = f64::from_str(&proposer_limit).with_context(|| "Invalid proposer limit")?; - let _ = f64::from_str(&quorum).with_context(|| "Invalid quorum")?; + if let Err(e) = f64::from_str(&proposer_limit) { + eprintln!("Invalid proposer limit: {e:?}"); + exit(2); + } + if let Err(e) = f64::from_str(&quorum) { + eprintln!("Invalid quorum: {e:?}"); + exit(2); + } - let proposer_limit = decode_base10(&proposer_limit, 8, true)?; - let quorum = decode_base10(&quorum, 8, true)?; + let proposer_limit = decode_base10(&proposer_limit, BALANCE_BASE10_DECIMALS, true)?; + let quorum = decode_base10(&quorum, BALANCE_BASE10_DECIMALS, true)?; if approval_ratio > 1.0 { eprintln!("Error: Approval ratio cannot be >1.0"); - exit(1); + exit(2); } let approval_ratio_base = 100_u64; let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64; - let drk = Drk::new(args.endpoint).await?; - let gov_token_id = - drk.get_token(gov_token_id).await.with_context(|| "Invalid Token ID")?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + let gov_token_id = match drk.get_token(gov_token_id).await { + Ok(g) => g, + Err(e) => { + eprintln!("Invalid Token ID: {e:?}"); + exit(2); + } + }; let secret_key = SecretKey::random(&mut OsRng); let bulla_blind = pallas::Base::random(&mut OsRng); @@ -929,7 +956,7 @@ async fn main() -> Result<()> { }; let encoded = bs58::encode(&serialize(&dao_params)).into_string(); - println!("{}", encoded); + eprintln!("{encoded}"); Ok(()) } @@ -939,7 +966,7 @@ async fn main() -> Result<()> { stdin().read_to_string(&mut buf)?; let bytes = bs58::decode(&buf.trim()).into_vec()?; let dao_params: DaoParams = deserialize(&bytes)?; - println!("{}", dao_params); + eprintln!("{dao_params}"); Ok(()) } @@ -950,39 +977,51 @@ async fn main() -> Result<()> { let bytes = bs58::decode(&buf.trim()).into_vec()?; let dao_params: DaoParams = deserialize(&bytes)?; - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - drk.import_dao(dao_name, dao_params) - .await - .with_context(|| "Failed to import DAO")?; + if let Err(e) = drk.import_dao(dao_name, dao_params).await { + eprintln!("Failed to import DAO: {e:?}"); + exit(2); + } Ok(()) } DaoSubcmd::List { dao_alias } => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; // We cannot use .map() since get_dao_id() uses ? let dao_id = match dao_alias { Some(alias) => Some(drk.get_dao_id(&alias).await?), None => None, }; - drk.dao_list(dao_id).await.with_context(|| "Failed to list DAO")?; + if let Err(e) = drk.dao_list(dao_id).await { + eprintln!("Failed to list DAO: {e:?}"); + exit(2); + } Ok(()) } DaoSubcmd::Balance { dao_alias } => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let dao_id = drk.get_dao_id(&dao_alias).await?; - let balmap = - drk.dao_balance(dao_id).await.with_context(|| "Failed to fetch DAO balance")?; + let balmap = match drk.dao_balance(dao_id).await { + Ok(b) => b, + Err(e) => { + eprintln!("Failed to fetch DAO balance: {e:?}"); + exit(2); + } + }; - let aliases_map = drk - .get_aliases_mapped_by_token() - .await - .with_context(|| "Failed to fetch wallet aliases")?; + let aliases_map = match drk.get_aliases_mapped_by_token().await { + Ok(a) => a, + Err(e) => { + eprintln!("Failed to fetch wallet aliases: {e:?}"); + exit(2); + } + }; // Create a prettytable with the new data: let mut table = Table::new(); @@ -994,149 +1033,271 @@ async fn main() -> Result<()> { None => "-", }; - // FIXME: Don't hardcode to 8 decimals - table.add_row(row![token_id, aliases, encode_base10(*balance, 8)]); + table.add_row(row![ + token_id, + aliases, + encode_base10(*balance, BALANCE_BASE10_DECIMALS) + ]); } if table.is_empty() { - println!("No unspent balances found"); + eprintln!("No unspent balances found"); } else { - println!("{}", table); + eprintln!("{table}"); } Ok(()) } DaoSubcmd::Mint { dao_alias } => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let dao_id = drk.get_dao_id(&dao_alias).await?; - let tx = drk.dao_mint(dao_id).await.with_context(|| "Failed to mint DAO")?; - println!("{}", bs58::encode(&serialize(&tx)).into_string()); + let tx = match drk.dao_mint(dao_id).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to mint DAO: {e:?}"); + exit(2); + } + }; + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) } DaoSubcmd::Propose { dao_alias, recipient, amount, token } => { - let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?; - let amount = decode_base10(&amount, 8, true)?; - let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?; - let drk = Drk::new(args.endpoint).await?; + if let Err(e) = f64::from_str(&amount) { + eprintln!("Invalid amount: {e:?}"); + exit(2); + } + let amount = decode_base10(&amount, BALANCE_BASE10_DECIMALS, true)?; + let rcpt = match PublicKey::from_str(&recipient) { + Ok(r) => r, + Err(e) => { + eprintln!("Invalid recipient: {e:?}"); + exit(2); + } + }; + + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let dao_id = drk.get_dao_id(&dao_alias).await?; - let token_id = drk.get_token(token).await.with_context(|| "Invalid token alias")?; + let token_id = match drk.get_token(token).await { + Ok(t) => t, + Err(e) => { + eprintln!("Invalid token alias: {e:?}"); + exit(2); + } + }; - let tx = drk - .dao_propose(dao_id, rcpt, amount, token_id) - .await - .with_context(|| "Failed to create DAO proposal")?; - - println!("{}", bs58::encode(&serialize(&tx)).into_string()); + let tx = match drk.dao_propose(dao_id, rcpt, amount, token_id).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to create DAO proposal: {e:?}"); + exit(2); + } + }; + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) } DaoSubcmd::Proposals { dao_alias } => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let dao_id = drk.get_dao_id(&dao_alias).await?; let proposals = drk.get_dao_proposals(dao_id).await?; for proposal in proposals { - println!("[{}] {:?}", proposal.id, proposal.bulla()); + eprintln!("[{}] {:?}", proposal.id, proposal.bulla()); } Ok(()) } DaoSubcmd::Proposal { dao_alias, proposal_id } => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let dao_id = drk.get_dao_id(&dao_alias).await?; let proposals = drk.get_dao_proposals(dao_id).await?; let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else { eprintln!("No such DAO proposal found"); - exit(1); + exit(2); }; - println!("{}", proposal); + eprintln!("{proposal}"); let votes = drk.get_dao_proposal_votes(proposal_id).await?; - println!("votes:"); + eprintln!("votes:"); for vote in votes { let option = if vote.vote_option { "yes" } else { "no " }; - println!(" {} {}", option, vote.all_vote_value); + eprintln!(" {option} {}", vote.all_vote_value); } Ok(()) } DaoSubcmd::Vote { dao_alias, proposal_id, vote, vote_weight } => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let dao_id = drk.get_dao_id(&dao_alias).await?; - let _ = f64::from_str(&vote_weight).with_context(|| "Invalid vote weight")?; - let weight = decode_base10(&vote_weight, 8, true)?; + if let Err(e) = f64::from_str(&vote_weight) { + eprintln!("Invalid vote weight: {e:?}"); + exit(2); + } + let weight = decode_base10(&vote_weight, BALANCE_BASE10_DECIMALS, true)?; if vote > 1 { eprintln!("Vote can be either 0 (NO) or 1 (YES)"); - exit(1); + exit(2); } let vote = vote != 0; - let tx = drk - .dao_vote(dao_id, proposal_id, vote, weight) - .await - .with_context(|| "Failed to create DAO Vote transaction")?; + let tx = match drk.dao_vote(dao_id, proposal_id, vote, weight).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to create DAO Vote transaction: {e:?}"); + exit(2); + } + }; // TODO: Write our_vote in the proposal sql. - println!("{}", bs58::encode(&serialize(&tx)).into_string()); + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) } DaoSubcmd::Exec { dao_alias, proposal_id } => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let dao_id = drk.get_dao_id(&dao_alias).await?; let dao = drk.get_dao_by_id(dao_id).await?; let proposal = drk.get_dao_proposal_by_id(proposal_id).await?; assert!(proposal.dao_bulla == dao.bulla()); - let tx = drk - .dao_exec(dao, proposal) - .await - .with_context(|| "Failed to execute DAO proposal")?; - - println!("{}", bs58::encode(&serialize(&tx)).into_string()); + let tx = match drk.dao_exec(dao, proposal).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to execute DAO proposal: {e:?}"); + exit(2); + } + }; + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) } }, - Subcmd::Explorer(cmd) => match cmd { + Subcmd::Inspect => { + let mut buf = String::new(); + stdin().read_to_string(&mut buf)?; + let bytes = bs58::decode(&buf.trim()).into_vec()?; + let tx: Transaction = deserialize(&bytes)?; + eprintln!("{tx:#?}"); + Ok(()) + } + + Subcmd::Broadcast => { + eprintln!("Reading transaction from stdin..."); + let mut buf = String::new(); + stdin().read_to_string(&mut buf)?; + let bytes = bs58::decode(&buf.trim()).into_vec()?; + let tx = deserialize(&bytes)?; + + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + + let txid = match drk.broadcast_tx(&tx).await { + Ok(t) => t, + Err(e) => { + eprintln!("Failed to broadcast transaction: {e:?}"); + exit(2); + } + }; + + eprintln!("Transaction ID: {txid}"); + + Ok(()) + } + + Subcmd::Subscribe => { + let drk = + Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) + .await?; + + if let Err(e) = drk.subscribe_blocks(args.endpoint, ex).await { + eprintln!("Block subscription failed: {e:?}"); + exit(2); + } + + Ok(()) + } + + Subcmd::Scan { reset, list, checkpoint } => { + let drk = + Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) + .await?; + + if reset { + eprintln!("Reset requested."); + if let Err(e) = drk.scan_blocks(true).await { + eprintln!("Failed during scanning: {e:?}"); + exit(2); + } + eprintln!("Finished scanning blockchain"); + + return Ok(()) + } + + if list { + eprintln!("List requested."); + // TODO: implement + unimplemented!() + } + + if let Some(c) = checkpoint { + eprintln!("Checkpoint requested: {c}"); + // TODO: implement + unimplemented!() + } + + if let Err(e) = drk.scan_blocks(false).await { + eprintln!("Failed during scanning: {e:?}"); + exit(2); + } + eprintln!("Finished scanning blockchain"); + + Ok(()) + } + + Subcmd::Explorer { command } => match command { ExplorerSubcmd::FetchTx { tx_hash, full, encode } => { let tx_hash = blake3::Hash::from_hex(&tx_hash)?; - let drk = Drk::new(args.endpoint).await?; + let drk = + Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) + .await?; - let tx = if let Some(tx) = - drk.get_tx(&tx_hash).await.with_context(|| "Failed to fetch transaction")? - { - tx - } else { + let tx = match drk.get_tx(&tx_hash).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to fetch transaction: {e:?}"); + exit(2); + } + }; + + let Some(tx) = tx else { eprintln!("Transaction was not found"); exit(1); }; // Make sure the tx is correct - assert_eq!(tx.hash(), tx_hash); + assert_eq!(tx.hash()?, tx_hash); if encode { - println!("{}", bs58::encode(&serialize(&tx)).into_string()); + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); exit(1) } - println!("Transaction ID: {}", tx_hash); + eprintln!("Transaction ID: {tx_hash}"); if full { - println!("{:?}", tx); + eprintln!("{tx:?}"); } Ok(()) @@ -1149,19 +1310,28 @@ async fn main() -> Result<()> { let bytes = bs58::decode(&buf.trim()).into_vec()?; let tx = deserialize(&bytes)?; - let drk = Drk::new(args.endpoint).await?; + let drk = + Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) + .await?; - let is_valid = - drk.simulate_tx(&tx).await.with_context(|| "Failed to simulate tx")?; + let is_valid = match drk.simulate_tx(&tx).await { + Ok(b) => b, + Err(e) => { + eprintln!("Failed to simulate tx: {e:?}"); + exit(2); + } + }; - println!("Transaction ID: {}", tx.hash()); - println!("State: {}", if is_valid { "valid" } else { "invalid" }); + eprintln!("Transaction ID: {}", tx.hash()?); + eprintln!("State: {}", if is_valid { "valid" } else { "invalid" }); Ok(()) } ExplorerSubcmd::TxsHistory { tx_hash, encode } => { - let drk = Drk::new(args.endpoint).await?; + let drk = + Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) + .await?; if let Some(c) = tx_hash { let (tx_hash, status, tx) = drk.get_tx_history_record(&c).await?; @@ -1171,14 +1341,20 @@ async fn main() -> Result<()> { exit(1) } - println!("Transaction ID: {}", tx_hash); - println!("Status: {}", status); - println!("{:?}", tx); + eprintln!("Transaction ID: {tx_hash}"); + eprintln!("Status: {status}"); + eprintln!("{tx:?}"); return Ok(()) } - let map = drk.get_txs_history().await?; + let map = match drk.get_txs_history().await { + Ok(m) => m, + Err(e) => { + eprintln!("Failed to retrieve transactions history records: {e:?}"); + exit(2); + } + }; // Create a prettytable with the new data: let mut table = Table::new(); @@ -1189,39 +1365,52 @@ async fn main() -> Result<()> { } if table.is_empty() { - println!("No transactions found"); + eprintln!("No transactions found"); } else { - println!("{}", table); + eprintln!("{table}"); } Ok(()) } }, - Subcmd::Alias(cmd) => match cmd { + Subcmd::Alias { command } => match command { AliasSubcmd::Add { alias, token } => { if alias.chars().count() > 5 { eprintln!("Error: Alias exceeds 5 characters"); - exit(1); + exit(2); } - let token_id = - TokenId::from_str(token.as_str()).with_context(|| "Invalid Token ID")?; - let drk = Drk::new(args.endpoint).await?; - drk.add_alias(alias, token_id).await?; + let token_id = match TokenId::from_str(token.as_str()) { + Ok(t) => t, + Err(e) => { + eprintln!("Invalid Token ID: {e:?}"); + exit(2); + } + }; + + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + if let Err(e) = drk.add_alias(alias, token_id).await { + eprintln!("Failed to add alias: {e:?}"); + exit(2); + } Ok(()) } AliasSubcmd::Show { alias, token } => { let token_id = match token { - Some(t) => { - Some(TokenId::from_str(t.as_str()).with_context(|| "Invalid Token ID")?) - } + Some(t) => match TokenId::from_str(t.as_str()) { + Ok(t) => Some(t), + Err(e) => { + eprintln!("Invalid Token ID: {e:?}"); + exit(2); + } + }, None => None, }; - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let map = drk.get_aliases(alias, token_id).await?; // Create a prettytable with the new data: @@ -1233,34 +1422,45 @@ async fn main() -> Result<()> { } if table.is_empty() { - println!("No aliases found"); + eprintln!("No aliases found"); } else { - println!("{}", table); + eprintln!("{table}"); } Ok(()) } AliasSubcmd::Remove { alias } => { - let drk = Drk::new(args.endpoint).await?; - drk.remove_alias(alias).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + if let Err(e) = drk.remove_alias(alias).await { + eprintln!("Failed to remove alias: {e:?}"); + exit(2); + } Ok(()) } }, - Subcmd::Token(cmd) => match cmd { + Subcmd::Token { command } => match command { TokenSubcmd::Import => { let mut buf = String::new(); stdin().read_to_string(&mut buf)?; - let mint_authority = - SecretKey::from_str(buf.trim()).with_context(|| "Invalid secret key")?; + let mint_authority = match SecretKey::from_str(buf.trim()) { + Ok(ma) => ma, + Err(e) => { + eprintln!("Invalid secret key: {e:?}"); + exit(2); + } + }; - let drk = Drk::new(args.endpoint).await?; - drk.import_mint_authority(mint_authority).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + if let Err(e) = drk.import_mint_authority(mint_authority).await { + eprintln!("Importing mint authority failed: {e:?}"); + exit(2); + }; let token_id = TokenId::derive(mint_authority); - eprintln!("Successfully imported mint authority for token ID: {}", token_id); + eprintln!("Successfully imported mint authority for token ID: {token_id}"); Ok(()) } @@ -1268,22 +1468,29 @@ async fn main() -> Result<()> { TokenSubcmd::GenerateMint => { let mint_authority = SecretKey::random(&mut OsRng); - let drk = Drk::new(args.endpoint).await?; - drk.import_mint_authority(mint_authority).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + + if let Err(e) = drk.import_mint_authority(mint_authority).await { + eprintln!("Importing mint authority failed: {e:?}"); + exit(2); + }; let token_id = TokenId::derive(mint_authority); - eprintln!("Successfully imported mint authority for token ID: {}", token_id); + eprintln!("Successfully imported mint authority for token ID: {token_id}"); Ok(()) } TokenSubcmd::List => { - let drk = Drk::new(args.endpoint).await?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; let tokens = drk.list_tokens().await?; - let aliases_map = drk - .get_aliases_mapped_by_token() - .await - .with_context(|| "Failed to fetch wallet aliases")?; + let aliases_map = match drk.get_aliases_mapped_by_token().await { + Ok(map) => map, + Err(e) => { + eprintln!("Failed to fetch wallet aliases: {e:?}"); + exit(2); + } + }; let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); @@ -1299,9 +1506,9 @@ async fn main() -> Result<()> { } if table.is_empty() { - println!("No tokens found"); + eprintln!("No tokens found"); } else { - println!("{}", table); + eprintln!("{table}"); } Ok(()) @@ -1309,31 +1516,61 @@ async fn main() -> Result<()> { // TODO: Mint directly into DAO treasury TokenSubcmd::Mint { token, amount, recipient } => { - let drk = Drk::new(args.endpoint).await?; - let _ = f64::from_str(&amount).with_context(|| "Invalid amount")?; - let rcpt = PublicKey::from_str(&recipient).with_context(|| "Invalid recipient")?; - let token_id = drk.get_token(token).await.with_context(|| "Invalid Token ID")?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let tx = drk - .mint_token(&amount, rcpt, token_id) - .await - .with_context(|| "Failed to create token mint transaction")?; + if let Err(e) = f64::from_str(&amount) { + eprintln!("Invalid amount: {e:?}"); + exit(2); + } - println!("{}", bs58::encode(&serialize(&tx)).into_string()); + let rcpt = match PublicKey::from_str(&recipient) { + Ok(r) => r, + Err(e) => { + eprintln!("Invalid recipient: {e:?}"); + exit(2); + } + }; + + let token_id = match drk.get_token(token).await { + Ok(t) => t, + Err(e) => { + eprintln!("Invalid Token ID: {e:?}"); + exit(2); + } + }; + + let tx = match drk.mint_token(&amount, rcpt, token_id).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to create token mint transaction: {e:?}"); + exit(2); + } + }; + + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) } TokenSubcmd::Freeze { token } => { - let drk = Drk::new(args.endpoint).await?; - let token_id = drk.get_token(token).await.with_context(|| "Invalid Token ID")?; + let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; + let token_id = match drk.get_token(token).await { + Ok(t) => t, + Err(e) => { + eprintln!("Invalid Token ID: {e:?}"); + exit(2); + } + }; - let tx = drk - .freeze_token(token_id) - .await - .with_context(|| "Failed to create token freeze transaction")?; + let tx = match drk.freeze_token(token_id).await { + Ok(tx) => tx, + Err(e) => { + eprintln!("Failed to create token freeze transaction: {e:?}"); + exit(2); + } + }; - println!("{}", bs58::encode(&serialize(&tx)).into_string()); + eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); Ok(()) } diff --git a/bin/drk2/src/money.rs b/bin/drk/src/money.rs similarity index 100% rename from bin/drk2/src/money.rs rename to bin/drk/src/money.rs diff --git a/bin/drk2/src/rpc.rs b/bin/drk/src/rpc.rs similarity index 100% rename from bin/drk2/src/rpc.rs rename to bin/drk/src/rpc.rs diff --git a/bin/drk/src/rpc_airdrop.rs b/bin/drk/src/rpc_airdrop.rs deleted file mode 100644 index a9d4d6ddd..000000000 --- a/bin/drk/src/rpc_airdrop.rs +++ /dev/null @@ -1,72 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use anyhow::{anyhow, Result}; -use darkfi::rpc::{client::RpcClient, jsonrpc::JsonRequest}; -use darkfi_sdk::{ - crypto::{mimc_vdf, PublicKey}, - num_bigint::BigUint, - num_traits::Num, -}; -use serde_json::json; -use url::Url; - -use super::Drk; - -impl Drk { - /// Request an airdrop of `amount` `token_id` tokens from a faucet. - /// Returns a transaction ID on success. - pub async fn request_airdrop( - &self, - faucet_endpoint: Url, - amount: f64, - address: PublicKey, - ) -> Result { - let rpc_client = RpcClient::new(faucet_endpoint, None).await?; - - // First we request a VDF challenge from the faucet - let params = json!([format!("{}", address)]); - let req = JsonRequest::new("challenge", params); - let rep = rpc_client.request(req).await?; - - let Some(rep) = rep.as_array() else { - return Err(anyhow!("Invalid challenge response from faucet: {:?}", rep)) - }; - if rep.len() != 2 || !rep[0].is_string() || !rep[1].is_u64() { - return Err(anyhow!("Invalid challenge response from faucet: {:?}", rep)) - } - - // Retrieve VDF challenge - let challenge = BigUint::from_str_radix(rep[0].as_str().unwrap(), 16)?; - let n_steps = rep[1].as_u64().unwrap(); - - // Then evaluate the VDF - eprintln!("Evaluating VDF with n_steps={} ... (this could take about a minute)", n_steps); - let witness = mimc_vdf::eval(&challenge, n_steps); - eprintln!("Done! Sending airdrop request..."); - - // And finally request airdrop with the VDF evaluation witness - let params = json!([format!("{}", address), amount, witness.to_str_radix(16)]); - let req = JsonRequest::new("airdrop", params); - let rep = rpc_client.oneshot_request(req).await?; - - let txid = serde_json::from_value(rep)?; - - Ok(txid) - } -} diff --git a/bin/drk/src/rpc_blockchain.rs b/bin/drk/src/rpc_blockchain.rs deleted file mode 100644 index e56333336..000000000 --- a/bin/drk/src/rpc_blockchain.rs +++ /dev/null @@ -1,355 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use anyhow::{anyhow, Result}; -use async_std::{stream::StreamExt, task}; -use darkfi::{ - consensus::BlockInfo, - rpc::{ - client::RpcClient, - jsonrpc::{JsonRequest, JsonResult}, - }, - system::Subscriber, - tx::Transaction, - wallet::walletdb::QueryType, -}; -use darkfi_money_contract::client::{MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE}; -use darkfi_sdk::crypto::ContractId; -use darkfi_serial::{deserialize, serialize}; -use serde_json::json; -use signal_hook::consts::{SIGINT, SIGQUIT, SIGTERM}; -use signal_hook_async_std::Signals; -use url::Url; - -use super::Drk; - -impl Drk { - /// Subscribes to darkfid's JSON-RPC notification endpoint that serves - /// new finalized blocks. Upon receiving them, all the transactions are - /// scanned and we check if any of them call the money contract, and if - /// the payments are intended for us. If so, we decrypt them and append - /// the metadata to our wallet. - pub async fn subscribe_blocks(&self, endpoint: Url) -> Result<()> { - let req = JsonRequest::new("blockchain.last_known_slot", json!([])); - let rep = self.rpc_client.request(req).await?; - let last_known: u64 = serde_json::from_value(rep)?; - let last_scanned = self.last_scanned_slot().await?; - - if last_known != last_scanned { - eprintln!("Warning: Last scanned slot is not the last known slot."); - eprintln!("You should first fully scan the blockchain, and then subscribe"); - return Err(anyhow!("Blockchain not fully scanned")) - } - - eprintln!("Subscribing to receive notifications of incoming blocks"); - let subscriber = Subscriber::new(); - let subscription = subscriber.clone().subscribe().await; - - let rpc_client = RpcClient::new(endpoint, None).await?; - - let req = JsonRequest::new("blockchain.subscribe_blocks", json!([])); - task::spawn(async move { rpc_client.subscribe(req, subscriber).await.unwrap() }); - eprintln!("Detached subscription to background"); - eprintln!("All is good. Waiting for block notifications..."); - - let e = loop { - match subscription.receive().await { - JsonResult::Notification(n) => { - eprintln!("Got Block notification from darkfid subscription"); - if n.method != "blockchain.subscribe_blocks" { - break anyhow!("Got foreign notification from darkfid: {}", n.method) - } - - let Some(params) = n.params.as_array() else { - break anyhow!("Received notification params are not an array") - }; - - if params.len() != 1 { - break anyhow!("Notification parameters are not len 1") - } - - let params = n.params.as_array().unwrap()[0].as_str().unwrap(); - let bytes = bs58::decode(params).into_vec()?; - - let block_data: BlockInfo = deserialize(&bytes)?; - eprintln!("======================================="); - eprintln!("Block header:\n{:#?}", block_data.header); - eprintln!("======================================="); - - eprintln!("Deserialized successfully. Scanning block..."); - self.scan_block_money(&block_data).await?; - self.scan_block_dao(&block_data).await?; - self.update_tx_history_records_status(&block_data.txs, "Finalized").await?; - } - - JsonResult::Error(e) => { - // Some error happened in the transmission - break anyhow!("Got error from JSON-RPC: {:?}", e) - } - - x => { - // And this is weird - break anyhow!("Got unexpected data from JSON-RPC: {:?}", x) - } - } - }; - - Err(e) - } - - /// `scan_block_dao` will go over transactions in a block and fetch the ones dealing - /// with the dao contract. Then over all of them, try to see if any are related - /// to us. If any are found, the metadata is extracted and placed into the wallet - /// for future use. - async fn scan_block_dao(&self, block: &BlockInfo) -> Result<()> { - eprintln!("[DAO] Iterating over {} transactions", block.txs.len()); - for tx in block.txs.iter() { - self.apply_tx_dao_data(tx, true).await?; - } - - Ok(()) - } - - /// `scan_block_money` will go over transactions in a block and fetch the ones dealing - /// with the money contract. Then over all of them, try to see if any are related - /// to us. If any are found, the metadata is extracted and placed into the wallet - /// for future use. - async fn scan_block_money(&self, block: &BlockInfo) -> Result<()> { - eprintln!("[Money] Iterating over {} transactions", block.txs.len()); - - for tx in block.txs.iter() { - self.apply_tx_money_data(tx, true).await?; - } - - // Write this slot into `last_scanned_slot` - let query = - format!("UPDATE {} SET {} = ?1;", MONEY_INFO_TABLE, MONEY_INFO_COL_LAST_SCANNED_SLOT); - let params = json!([query, QueryType::Integer as u8, block.header.slot]); - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Try to fetch zkas bincodes for the given `ContractId`. - pub async fn lookup_zkas(&self, contract_id: &ContractId) -> Result)>> { - eprintln!("Querying zkas bincode for {}", contract_id); - - let params = json!([format!("{}", contract_id)]); - let req = JsonRequest::new("blockchain.lookup_zkas", params); - - let rep = self.rpc_client.request(req).await?; - - let ret = serde_json::from_value(rep)?; - Ok(ret) - } - - /// Broadcast a given transaction to darkfid and forward onto the network. - /// Returns the transaction ID upon success - pub async fn broadcast_tx(&self, tx: &Transaction) -> Result { - eprintln!("Broadcasting transaction..."); - - let params = json!([bs58::encode(&serialize(tx)).into_string()]); - let req = JsonRequest::new("tx.broadcast", params); - let rep = self.rpc_client.request(req).await?; - - let txid = serde_json::from_value(rep)?; - - // Store transactions history record - self.insert_tx_history_record(tx).await?; - - Ok(txid) - } - - /// Simulate the transaction with the state machine - pub async fn simulate_tx(&self, tx: &Transaction) -> Result { - let params = json!([bs58::encode(&serialize(tx)).into_string()]); - let req = JsonRequest::new("tx.simulate", params); - let rep = self.rpc_client.request(req).await?; - - let is_valid = serde_json::from_value(rep)?; - Ok(is_valid) - } - - /// Queries darkfid for a block with given slot - async fn get_block_by_slot(&self, slot: u64) -> Result> { - let req = JsonRequest::new("blockchain.get_slot", json!([slot])); - - // This API is weird, we need some way of telling it's an empty slot and - // not an error - match self.rpc_client.request(req).await { - Ok(v) => { - let block_bytes: Vec = serde_json::from_value(v)?; - let block = deserialize(&block_bytes)?; - Ok(Some(block)) - } - - Err(_) => Ok(None), - } - } - - /// Queries darkfid for a tx with given hash - pub async fn get_tx(&self, tx_hash: &blake3::Hash) -> Result> { - let tx_hash_str: &str = &tx_hash.to_hex(); - let req = JsonRequest::new("blockchain.get_tx", json!([tx_hash_str])); - - match self.rpc_client.request(req).await { - Ok(v) => { - let tx_bytes: Vec = serde_json::from_value(v)?; - let tx = deserialize(&tx_bytes)?; - Ok(Some(tx)) - } - - Err(_) => Ok(None), - } - } - - /// Scans the blockchain starting from the last scanned slot, for relevant - /// money transfer transactions. If reset flag is provided, Merkle tree state - /// and coins are reset, and start scanning from beginning. Alternatively, - /// it looks for a checkpoint in the wallet to reset and start scanning from. - pub async fn scan_blocks(&self, reset: bool) -> Result<()> { - let mut sl = if reset { - self.reset_money_tree().await?; - self.reset_money_coins().await?; - self.reset_dao_trees().await?; - self.reset_daos().await?; - self.reset_dao_proposals().await?; - self.reset_dao_votes().await?; - self.update_all_tx_history_records_status("Rejected").await?; - 0 - } else { - self.last_scanned_slot().await? - }; - - let req = JsonRequest::new("blockchain.last_known_slot", json!([])); - let rep = self.rpc_client.request(req).await?; - let last: u64 = serde_json::from_value(rep)?; - - eprintln!("Requested to scan from slot number: {}", sl); - eprintln!("Last known slot number reported by darkfid: {}", last); - - // Already scanned last known slot - if sl == last { - return Ok(()) - } - - // We set this up to handle an interrupt - let mut signals = Signals::new([SIGTERM, SIGINT, SIGQUIT])?; - let handle = signals.handle(); - let (term_tx, _term_rx) = smol::channel::bounded::<()>(1); - - let term_tx_ = term_tx.clone(); - let signals_task = task::spawn(async move { - while let Some(signal) = signals.next().await { - match signal { - SIGTERM | SIGINT | SIGQUIT => term_tx_.close(), - _ => unreachable!(), - }; - } - }); - - while !term_tx.is_closed() { - sl += 1; - - if sl > last { - term_tx.close(); - break - } - - eprint!("Requesting slot {}... ", sl); - if let Some(block) = self.get_block_by_slot(sl).await? { - eprintln!("Found"); - self.scan_block_money(&block).await?; - self.scan_block_dao(&block).await?; - self.update_tx_history_records_status(&block.txs, "Finalized").await?; - } else { - eprintln!("Not found"); - // Write down the slot number into back to the wallet - // This might be a bit intense, but we accept it for now. - let query = format!( - "UPDATE {} SET {} = ?1;", - MONEY_INFO_TABLE, MONEY_INFO_COL_LAST_SCANNED_SLOT - ); - let params = json!([query, QueryType::Integer as u8, sl]); - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - } - - handle.close(); - signals_task.await; - - Ok(()) - } - - /// Subscribes to darkfid's JSON-RPC notification endpoint that serves - /// erroneous transactions rejections. - pub async fn subscribe_err_txs(&self, endpoint: Url) -> Result<()> { - eprintln!("Subscribing to receive notifications of erroneous transactions"); - let subscriber = Subscriber::new(); - let subscription = subscriber.clone().subscribe().await; - - let rpc_client = RpcClient::new(endpoint, None).await?; - - let req = JsonRequest::new("blockchain.subscribe_err_txs", json!([])); - task::spawn(async move { rpc_client.subscribe(req, subscriber).await.unwrap() }); - eprintln!("Detached subscription to background"); - eprintln!("All is good. Waiting for erroneous transactions notifications..."); - - let e = loop { - match subscription.receive().await { - JsonResult::Notification(n) => { - eprintln!("Got erroneous transaction notification from darkfid subscription"); - if n.method != "blockchain.subscribe_err_txs" { - break anyhow!("Got foreign notification from darkfid: {}", n.method) - } - - let Some(params) = n.params.as_array() else { - break anyhow!("Received notification params are not an array") - }; - - if params.len() != 1 { - break anyhow!("Notification parameters are not len 1") - } - - let params = n.params.as_array().unwrap()[0].as_str().unwrap(); - let bytes = bs58::decode(params).into_vec()?; - - let tx_hash: String = deserialize(&bytes)?; - eprintln!("==================================="); - eprintln!("Erroneous transaction: {}", tx_hash); - eprintln!("==================================="); - self.update_tx_history_record_status(&tx_hash, "Rejected").await?; - } - - JsonResult::Error(e) => { - // Some error happened in the transmission - break anyhow!("Got error from JSON-RPC: {:?}", e) - } - - x => { - // And this is weird - break anyhow!("Got unexpected data from JSON-RPC: {:?}", x) - } - } - }; - - Err(e) - } -} diff --git a/bin/drk/src/rpc_dao.rs b/bin/drk/src/rpc_dao.rs deleted file mode 100644 index 240422cd5..000000000 --- a/bin/drk/src/rpc_dao.rs +++ /dev/null @@ -1,553 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use anyhow::{anyhow, Result}; -use darkfi::{ - tx::Transaction, - zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit}, - zkas::ZkBinary, -}; -use darkfi_dao_contract::{ - client as dao_client, - client::{DaoInfo, DaoProposalInfo, DaoVoteCall, DaoVoteInput}, - model::DaoBlindAggregateVote, - DaoFunction, DAO_CONTRACT_ZKAS_DAO_EXEC_NS, DAO_CONTRACT_ZKAS_DAO_MINT_NS, - DAO_CONTRACT_ZKAS_DAO_PROPOSE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS, - DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS, -}; -use darkfi_money_contract::{ - client::{transfer_v1::TransferCallBuilder, OwnCoin}, - MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1, -}; -use darkfi_sdk::{ - crypto::{ - pedersen_commitment_u64, Keypair, PublicKey, SecretKey, TokenId, DAO_CONTRACT_ID, - MONEY_CONTRACT_ID, - }, - pasta::pallas, - ContractCall, -}; -use darkfi_serial::Encodable; -use rand::rngs::OsRng; - -use super::Drk; -use crate::wallet_dao::{Dao, DaoProposal}; - -impl Drk { - /// Mint a DAO on-chain - pub async fn dao_mint(&self, dao_id: u64) -> Result { - let dao = self.get_dao_by_id(dao_id).await?; - - if dao.tx_hash.is_some() { - return Err(anyhow!("This DAO seems to have already been minted on-chain")) - } - - let dao_info = DaoInfo { - proposer_limit: dao.proposer_limit, - quorum: dao.quorum, - approval_ratio_base: dao.approval_ratio_base, - approval_ratio_quot: dao.approval_ratio_quot, - gov_token_id: dao.gov_token_id, - public_key: PublicKey::from_secret(dao.secret_key), - bulla_blind: dao.bulla_blind, - }; - - let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; - let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_MINT_NS) - else { - return Err(anyhow!("DAO Mint circuit not found")) - }; - - let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?; - let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin); - eprintln!("Creating DAO Mint proving key"); - let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit); - - let (params, proofs) = - dao_client::make_mint_call(&dao_info, &dao.secret_key, &dao_mint_zkbin, &dao_mint_pk)?; - - let mut data = vec![DaoFunction::Mint as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *DAO_CONTRACT_ID, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &[dao.secret_key])?; - tx.signatures = vec![sigs]; - - Ok(tx) - } - - /// Create a DAO proposal - pub async fn dao_propose( - &self, - dao_id: u64, - recipient: PublicKey, - amount: u64, - token_id: TokenId, - ) -> Result { - let Ok(dao) = self.get_dao_by_id(dao_id).await else { - return Err(anyhow!("DAO not found in wallet")) - }; - - if dao.leaf_position.is_none() || dao.tx_hash.is_none() { - return Err(anyhow!("DAO seems to not have been deployed yet")) - } - - let bulla = dao.bulla(); - let owncoins = self.get_coins(false).await?; - - let mut dao_owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); - dao_owncoins.retain(|x| { - x.note.token_id == token_id && - x.note.spend_hook == DAO_CONTRACT_ID.inner() && - x.note.user_data == bulla.inner() - }); - - let mut gov_owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); - gov_owncoins.retain(|x| x.note.token_id == dao.gov_token_id); - - if dao_owncoins.is_empty() { - return Err(anyhow!("Did not find any {} coins owned by this DAO", token_id)) - } - - if gov_owncoins.is_empty() { - return Err(anyhow!("Did not find any governance {} coins in wallet", dao.gov_token_id)) - } - - if dao_owncoins.iter().map(|x| x.note.value).sum::() < amount { - return Err(anyhow!("Not enough DAO balance for token ID: {}", token_id)) - } - - if gov_owncoins.iter().map(|x| x.note.value).sum::() < dao.proposer_limit { - return Err(anyhow!("Not enough gov token {} balance to propose", dao.gov_token_id)) - } - - // FIXME: Here we're looking for a coin == proposer_limit but this shouldn't have to - // be the case { - let Some(gov_coin) = gov_owncoins.iter().find(|x| x.note.value == dao.proposer_limit) - else { - return Err(anyhow!("Did not find a single gov coin of value {}", dao.proposer_limit)) - }; - // } - - // Lookup the zkas bins - let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; - let Some(propose_burn_zkbin) = - zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_BURN_NS) - else { - return Err(anyhow!("Propose Burn circuit not found")) - }; - - let Some(propose_main_zkbin) = - zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS) - else { - return Err(anyhow!("Propose Main circuit not found")) - }; - - let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?; - let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?; - - let propose_burn_circuit = - ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin); - let propose_main_circuit = - ZkCircuit::new(empty_witnesses(&propose_main_zkbin)?, &propose_main_zkbin); - - eprintln!("Creating Propose Burn circuit proving key"); - let propose_burn_pk = ProvingKey::build(propose_burn_zkbin.k, &propose_burn_circuit); - eprintln!("Creating Propose Main circuit proving key"); - let propose_main_pk = ProvingKey::build(propose_main_zkbin.k, &propose_main_circuit); - - // Now create the parameters for the proposal tx - let signature_secret = SecretKey::random(&mut OsRng); - - // Get the Merkle path for the gov coin in the money tree - let money_merkle_tree = self.get_money_tree().await?; - let gov_coin_merkle_path = money_merkle_tree.witness(gov_coin.leaf_position, 0).unwrap(); - - // Fetch the daos Merkle tree - let (daos_tree, _) = self.get_dao_trees().await?; - - let input = dao_client::DaoProposeStakeInput { - secret: gov_coin.secret, // <-- TODO: Is this correct? - note: gov_coin.note.clone(), - leaf_position: gov_coin.leaf_position, - merkle_path: gov_coin_merkle_path, - signature_secret, - }; - - let (dao_merkle_path, dao_merkle_root) = { - let root = daos_tree.root(0).unwrap(); - let leaf_pos = dao.leaf_position.unwrap(); - let dao_merkle_path = daos_tree.witness(leaf_pos, 0).unwrap(); - (dao_merkle_path, root) - }; - - let proposal_blind = pallas::Base::random(&mut OsRng); - let proposal = dao_client::DaoProposalInfo { - dest: recipient, - amount, - token_id, - blind: proposal_blind, - }; - - let daoinfo = DaoInfo { - proposer_limit: dao.proposer_limit, - quorum: dao.quorum, - approval_ratio_quot: dao.approval_ratio_quot, - approval_ratio_base: dao.approval_ratio_base, - gov_token_id: dao.gov_token_id, - public_key: PublicKey::from_secret(dao.secret_key), - bulla_blind: dao.bulla_blind, - }; - - let call = dao_client::DaoProposeCall { - inputs: vec![input], - proposal, - dao: daoinfo, - dao_leaf_position: dao.leaf_position.unwrap(), - dao_merkle_path, - dao_merkle_root, - }; - - eprintln!("Creating ZK proofs..."); - let (params, proofs) = call.make( - &propose_burn_zkbin, - &propose_burn_pk, - &propose_main_zkbin, - &propose_main_pk, - )?; - - let mut data = vec![DaoFunction::Propose as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *DAO_CONTRACT_ID, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &[signature_secret])?; - tx.signatures = vec![sigs]; - - Ok(tx) - } - - /// Vote on a DAO proposal - pub async fn dao_vote( - &self, - dao_id: u64, - proposal_id: u64, - vote_option: bool, - weight: u64, - ) -> Result { - let dao = self.get_dao_by_id(dao_id).await?; - let proposals = self.get_dao_proposals(dao_id).await?; - let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else { - return Err(anyhow!("Proposal ID not found")) - }; - - let money_tree = proposal.money_snapshot_tree.clone().unwrap(); - - let mut coins: Vec = - self.get_coins(false).await?.iter().map(|x| x.0.clone()).collect(); - - coins.retain(|x| x.note.token_id == dao.gov_token_id); - coins.retain(|x| x.note.spend_hook == pallas::Base::zero()); - - if coins.iter().map(|x| x.note.value).sum::() < weight { - return Err(anyhow!("Not enough balance for vote weight")) - } - - // TODO: The spent coins need to either be marked as spent here, and/or on scan - let mut spent_value = 0; - let mut spent_coins = vec![]; - let mut inputs = vec![]; - let mut input_secrets = vec![]; - - // FIXME: We don't take back any change so it's possible to vote with > requested weight. - for coin in coins { - if spent_value >= weight { - break - } - - spent_value += coin.note.value; - spent_coins.push(coin.clone()); - - let signature_secret = SecretKey::random(&mut OsRng); - input_secrets.push(signature_secret); - - let leaf_position = coin.leaf_position; - let merkle_path = money_tree.witness(coin.leaf_position, 0).unwrap(); - - let input = DaoVoteInput { - secret: coin.secret, - note: coin.note.clone(), - leaf_position, - merkle_path, - signature_secret, - }; - - inputs.push(input); - } - - // We use the DAO secret to encrypt the vote. - let vote_keypair = Keypair::new(dao.secret_key); - - let proposal_info = DaoProposalInfo { - dest: proposal.recipient, - amount: proposal.amount, - token_id: proposal.token_id, - blind: proposal.bulla_blind, - }; - - let dao_info = DaoInfo { - proposer_limit: dao.proposer_limit, - quorum: dao.quorum, - approval_ratio_quot: dao.approval_ratio_quot, - approval_ratio_base: dao.approval_ratio_base, - gov_token_id: dao.gov_token_id, - public_key: PublicKey::from_secret(dao.secret_key), - bulla_blind: dao.bulla_blind, - }; - - let call = DaoVoteCall { - inputs, - vote_option, - yes_vote_blind: pallas::Scalar::random(&mut OsRng), - vote_keypair, - proposal: proposal_info, - dao: dao_info, - }; - - let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; - let Some(dao_vote_burn_zkbin) = - zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_BURN_NS) - else { - return Err(anyhow!("DAO Vote Burn circuit not found")) - }; - - let Some(dao_vote_main_zkbin) = - zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS) - else { - return Err(anyhow!("DAO Vote Main circuit not found")) - }; - - let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1)?; - let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1)?; - - let dao_vote_burn_circuit = - ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin); - let dao_vote_main_circuit = - ZkCircuit::new(empty_witnesses(&dao_vote_main_zkbin)?, &dao_vote_main_zkbin); - - eprintln!("Creating DAO Vote Burn proving key"); - let dao_vote_burn_pk = ProvingKey::build(dao_vote_burn_zkbin.k, &dao_vote_burn_circuit); - eprintln!("Creating DAO Vote Main proving key"); - let dao_vote_main_pk = ProvingKey::build(dao_vote_main_zkbin.k, &dao_vote_main_circuit); - - let (params, proofs) = call.make( - &dao_vote_burn_zkbin, - &dao_vote_burn_pk, - &dao_vote_main_zkbin, - &dao_vote_main_pk, - )?; - - let mut data = vec![DaoFunction::Vote as u8]; - params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *DAO_CONTRACT_ID, data }]; - let proofs = vec![proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &input_secrets)?; - tx.signatures = vec![sigs]; - - Ok(tx) - } - - /// Import given DAO votes into the wallet - /// This function is really bad but I'm also really tired and annoyed. - pub async fn dao_exec(&self, dao: Dao, proposal: DaoProposal) -> Result { - let dao_bulla = dao.bulla(); - eprintln!("Fetching proposal's votes"); - let votes = self.get_dao_proposal_votes(proposal.id).await?; - - // Find the treasury coins that can be used for this proposal - let mut coins: Vec = - self.get_coins(false).await?.iter().map(|x| x.0.clone()).collect(); - coins.retain(|x| x.note.spend_hook == DAO_CONTRACT_ID.inner()); - coins.retain(|x| x.note.user_data == dao_bulla.inner()); - coins.retain(|x| x.note.token_id == proposal.token_id); - - if coins.iter().map(|x| x.note.value).sum::() < proposal.amount { - return Err(anyhow!("Not enough balance in DAO treasury to execute proposal")) - } - - // FIXME: This assumes we aren't sending to a protocol. - let rcpt_spend_hook = pallas::Base::ZERO; - let rcpt_user_data = pallas::Base::ZERO; - let rcpt_user_data_blind = pallas::Base::random(&mut OsRng); - - let change_spend_hook = DAO_CONTRACT_ID.inner(); - let change_user_data = dao_bulla.inner(); - let change_user_data_blind = pallas::Base::random(&mut OsRng); - - let money_merkle_tree = self.get_money_tree().await?; - - let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; - let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) - else { - return Err(anyhow!("Money Mint circuit not found")) - }; - let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1) - else { - return Err(anyhow!("Money Burn circuit not found")) - }; - let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?; - let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?; - let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); - let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); - eprintln!("Creating Money Mint circuit proving key"); - let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); - eprintln!("Creating Money Burn circuit proving key"); - let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); - - let xfer_builder = TransferCallBuilder { - keypair: dao.keypair(), - recipient: proposal.recipient, - value: proposal.amount, - token_id: proposal.token_id, - rcpt_spend_hook, - rcpt_user_data, - rcpt_user_data_blind, - change_spend_hook, - change_user_data, - change_user_data_blind, - coins, - tree: money_merkle_tree, - mint_zkbin: mint_zkbin.clone(), - mint_pk: mint_pk.clone(), - burn_zkbin: burn_zkbin.clone(), - burn_pk: burn_pk.clone(), - clear_input: false, - }; - - let xfer_debris = xfer_builder.build()?; - - let mut data = vec![MoneyFunction::TransferV1 as u8]; - xfer_debris.params.encode(&mut data)?; - let xfer_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data }; - - let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; - let Some(exec_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_EXEC_NS) - else { - return Err(anyhow!("DAO Exec circuit not found")) - }; - let exec_zkbin = ZkBinary::decode(&exec_zkbin.1)?; - let exec_circuit = ZkCircuit::new(empty_witnesses(&exec_zkbin)?, &exec_zkbin); - eprintln!("Creating DAO Exec circuit proving key"); - let exec_pk = ProvingKey::build(exec_zkbin.k, &exec_circuit); - - // Count votes - let mut total_yes_vote_value = 0; - let mut total_all_vote_value = 0; - let mut blind_total_vote = DaoBlindAggregateVote::default(); - let mut total_yes_vote_blind = pallas::Scalar::zero(); - let mut total_all_vote_blind = pallas::Scalar::zero(); - - for (_, vote) in votes.iter().enumerate() { - total_yes_vote_blind += vote.yes_vote_blind; - total_all_vote_blind += vote.all_vote_blind; - - let yes_vote_value = vote.vote_option as u64 * vote.all_vote_value; - eprintln!("yes_vote = {}", yes_vote_value); - total_yes_vote_value += yes_vote_value; - total_all_vote_value += vote.all_vote_value; - - let yes_vote_commit = pedersen_commitment_u64(yes_vote_value, vote.yes_vote_blind); - let all_vote_commit = pedersen_commitment_u64(vote.all_vote_value, vote.all_vote_blind); - - let blind_vote = DaoBlindAggregateVote { yes_vote_commit, all_vote_commit }; - blind_total_vote.aggregate(blind_vote); - } - - eprintln!("yes = {}, all = {}", total_yes_vote_value, total_all_vote_value); - - let prop_t = DaoProposalInfo { - dest: proposal.recipient, - amount: proposal.amount, - token_id: proposal.token_id, - blind: proposal.bulla_blind, // <-- FIXME: wtf - }; - - // TODO: allvote/yesvote is 11 weirdly - - let dao_t = DaoInfo { - proposer_limit: dao.proposer_limit, - quorum: dao.quorum, - approval_ratio_quot: dao.approval_ratio_quot, - approval_ratio_base: dao.approval_ratio_base, - gov_token_id: dao.gov_token_id, - public_key: PublicKey::from_secret(dao.secret_key), - bulla_blind: dao.bulla_blind, - }; - - // We need to extract stuff from the inputs and outputs that we'll also - // use in the DAO::Exec call. This DAO API needs to be better. - let mut input_value = 0; - let mut input_value_blind = pallas::Scalar::ZERO; - for (input, blind) in xfer_debris.spent_coins.iter().zip(xfer_debris.input_value_blinds) { - input_value += input.note.value; - input_value_blind += blind; - } - - // First output is change, second output is recipient. - let dao_serial = xfer_debris.minted_coins[0].note.serial; - let user_serial = xfer_debris.minted_coins[1].note.serial; - - // TODO: FIXME: This is not checked anywhere! - let exec_signature_secret = SecretKey::random(&mut OsRng); - - let dao_exec_call = dao_client::DaoExecCall { - proposal: prop_t, - dao: dao_t, - yes_vote_value: total_yes_vote_value, - all_vote_value: total_all_vote_value, - yes_vote_blind: total_yes_vote_blind, - all_vote_blind: total_all_vote_blind, - user_serial, - dao_serial, - input_value, - input_value_blind, - hook_dao_exec: DAO_CONTRACT_ID.inner(), - signature_secret: exec_signature_secret, - }; - - let (exec_params, exec_proofs) = dao_exec_call.make(&exec_zkbin, &exec_pk)?; - - let mut data = vec![DaoFunction::Exec as u8]; - exec_params.encode(&mut data)?; - let exec_call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; - - let mut tx = Transaction { - calls: vec![xfer_call, exec_call], - proofs: vec![xfer_debris.proofs, exec_proofs], - signatures: vec![], - }; - - let xfer_sigs = tx.create_sigs(&mut OsRng, &xfer_debris.signature_secrets)?; - let exec_sigs = tx.create_sigs(&mut OsRng, &[exec_signature_secret])?; - tx.signatures = vec![xfer_sigs, exec_sigs]; - - Ok(tx) - } -} diff --git a/bin/drk/src/rpc_swap.rs b/bin/drk/src/rpc_swap.rs deleted file mode 100644 index b969c0c40..000000000 --- a/bin/drk/src/rpc_swap.rs +++ /dev/null @@ -1,462 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -use std::fmt; - -use anyhow::{anyhow, Result}; -use darkfi::{ - tx::Transaction, - util::parse::encode_base10, - zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses, Proof}, - zkas::ZkBinary, -}; -use darkfi_money_contract::{ - client::{swap_v1::SwapCallBuilder, MoneyNote}, - model::{Coin, MoneyTransferParamsV1}, - MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1, -}; -use darkfi_sdk::{ - crypto::{ - contract_id::MONEY_CONTRACT_ID, pedersen::pedersen_commitment_u64, poseidon_hash, - PublicKey, SecretKey, TokenId, - }, - pasta::pallas, - tx::ContractCall, -}; -use darkfi_serial::{deserialize, Encodable, SerialDecodable, SerialEncodable}; -use rand::rngs::OsRng; - -use super::Drk; - -#[derive(Debug, Clone, SerialEncodable, SerialDecodable)] -/// Half of the swap data, includes the coin that is supposed to be sent, -/// and the coin that is supposed to be received. -pub struct PartialSwapData { - params: MoneyTransferParamsV1, - proofs: Vec, - value_pair: (u64, u64), - token_pair: (TokenId, TokenId), - value_blinds: Vec, - token_blinds: Vec, -} - -impl fmt::Display for PartialSwapData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = - format!( - "{:#?}\nValue pair: {}:{}\nToken pair: {}:{}\nValue blinds: {:?}\nToken blinds: {:?}\n", - self.params, self.value_pair.0, self.value_pair.1, self.token_pair.0, self.token_pair.1, - self.value_blinds, self.token_blinds, - ); - - write!(f, "{}", s) - } -} - -impl Drk { - /// Initialize the first half of an atomic swap - pub async fn init_swap( - &self, - value_send: u64, - token_send: TokenId, - value_recv: u64, - token_recv: TokenId, - ) -> Result { - // First we'll fetch all of our unspent coins from the wallet. - let mut owncoins = self.get_coins(false).await?; - // Then we see if we have one that we can send. - owncoins.retain(|x| { - x.0.note.value == value_send && - x.0.note.token_id == token_send && - x.0.note.spend_hook == pallas::Base::zero() - }); - - if owncoins.is_empty() { - return Err(anyhow!( - "Did not find any unspent coins of value {} and token_id {}", - value_send, - token_send - )) - } - - // If there are any, we'll just spend the first one we see. - let burn_coin = owncoins[0].0.clone(); - - // Fetch our default address - let address = self.wallet_address(1).await?; - - // We'll also need our Merkle tree - let tree = self.get_money_tree().await?; - - let contract_id = *MONEY_CONTRACT_ID; - - // Now we need to do a lookup for the zkas proof bincodes, and create - // the circuit objects and proving keys so we can build the transaction. - // We also do this through the RPC. - let zkas_bins = self.lookup_zkas(&contract_id).await?; - - let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) - else { - return Err(anyhow!("Mint circuit not found")) - }; - - let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1) - else { - return Err(anyhow!("Burn circuit not found")) - }; - - let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?; - let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?; - - let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); - let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); - - // Since we're creating the first half, we generate the blinds. - let value_blinds = [pallas::Scalar::random(&mut OsRng), pallas::Scalar::random(&mut OsRng)]; - let token_blinds = [pallas::Base::random(&mut OsRng), pallas::Base::random(&mut OsRng)]; - - // Now we should have everything we need to build the swap half - eprintln!("Creating Mint and Burn circuit proving keys"); - let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); - let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); - let builder = SwapCallBuilder { - pubkey: address, - value_send, - token_id_send: token_send, - value_recv, - token_id_recv: token_recv, - user_data_blind_send: pallas::Base::random(&mut OsRng), // <-- FIXME: Perhaps should be passed in - spend_hook_recv: pallas::Base::zero(), // <-- FIXME: Should be passed in - user_data_recv: pallas::Base::zero(), // <-- FIXME: Should be passed in - value_blinds, - token_blinds, - coin: burn_coin, - tree, - mint_zkbin, - mint_pk, - burn_zkbin, - burn_pk, - }; - - eprintln!("Building first half of the swap transaction"); - let debris = builder.build()?; - - // Now we have the half, so we can build `PartialSwapData` and return it. - let ret = PartialSwapData { - params: debris.params, - proofs: debris.proofs, - value_pair: (value_send, value_recv), - token_pair: (token_send, token_recv), - value_blinds: value_blinds.to_vec(), - token_blinds: token_blinds.to_vec(), - }; - - Ok(ret) - } - - /// Create a full transaction by inspecting and verifying given partial swap data, - /// making the other half, and joining all this into a `Transaction` object. - pub async fn join_swap(&self, partial: PartialSwapData) -> Result { - // Our side of the tx in the pairs is the second half, so we try to find - // an unspent coin like that in our wallet. - let mut owncoins = self.get_coins(false).await?; - owncoins.retain(|x| { - x.0.note.value == partial.value_pair.1 && x.0.note.token_id == partial.token_pair.1 - }); - - if owncoins.is_empty() { - return Err(anyhow!( - "Did not find any unspent coins of value {} and token_id {}", - partial.value_pair.1, - partial.token_pair.1 - )) - } - - // If there are any, we'll just spend the first one we see. - let burn_coin = owncoins[0].0.clone(); - - // Fetch our default address // FIXME: Should actually be getting is_default - let address = self.wallet_address(1).await?; - - // We'll also need our Merkle tree - let tree = self.get_money_tree().await?; - - let contract_id = *MONEY_CONTRACT_ID; - - // Now we need to do a lookup for the zkas proof bincodes, and create - // the circuit objects and proving keys so we can build the transaction. - // We also do this through the RPC. - let zkas_bins = self.lookup_zkas(&contract_id).await?; - - let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) - else { - return Err(anyhow!("Mint circuit not found")) - }; - - let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1) - else { - return Err(anyhow!("Burn circuit not found")) - }; - - let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?; - let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?; - - let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); - let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); - - // TODO: Maybe some kind of verification at this point - - // Now we should have everything we need to build the swap half - eprintln!("Creating Mint and Burn circuit proving keys"); - let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); - let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); - let builder = SwapCallBuilder { - pubkey: address, - value_send: partial.value_pair.1, - token_id_send: partial.token_pair.1, - value_recv: partial.value_pair.0, - token_id_recv: partial.token_pair.0, - user_data_blind_send: pallas::Base::random(&mut OsRng), // <-- FIXME: Perhaps should be passed in - spend_hook_recv: pallas::Base::zero(), // <-- FIXME: Should be passed in - user_data_recv: pallas::Base::zero(), // <-- FIXME: Should be passed in - value_blinds: [partial.value_blinds[1], partial.value_blinds[0]], - token_blinds: [partial.token_blinds[1], partial.token_blinds[0]], - coin: burn_coin, - tree, - mint_zkbin, - mint_pk, - burn_zkbin, - burn_pk, - }; - - eprintln!("Building second half of the swap transaction"); - let debris = builder.build()?; - - let full_params = MoneyTransferParamsV1 { - clear_inputs: vec![], - inputs: vec![partial.params.inputs[0].clone(), debris.params.inputs[0].clone()], - outputs: vec![partial.params.outputs[0].clone(), debris.params.outputs[0].clone()], - }; - - let full_proofs = vec![ - partial.proofs[0].clone(), - debris.proofs[0].clone(), - partial.proofs[1].clone(), - debris.proofs[1].clone(), - ]; - - let mut data = vec![MoneyFunction::OtcSwapV1 as u8]; - full_params.encode(&mut data)?; - let mut tx = Transaction { - calls: vec![ContractCall { contract_id, data }], - proofs: vec![full_proofs], - signatures: vec![], - }; - eprintln!("Signing swap transaction"); - let sigs = tx.create_sigs(&mut OsRng, &[debris.signature_secret])?; - tx.signatures = vec![sigs]; - - Ok(tx) - } - - /// Inspect and verify a given swap (half or full) transaction - pub async fn inspect_swap(&self, bytes: Vec) -> Result<()> { - let mut full: Option = None; - let mut half: Option = None; - - if let Ok(v) = deserialize(&bytes) { - full = Some(v) - }; - - match deserialize(&bytes) { - Ok(v) => half = Some(v), - Err(_) => { - if full.is_none() { - return Err(anyhow!("Failed to deserialize to Transaction or PartialSwapData")) - } - } - } - - if let Some(tx) = full { - // We're inspecting a full transaction - if tx.calls.len() != 1 { - eprintln!( - "Found {} contract calls in the transaction, there should be 1", - tx.calls.len() - ); - return Err(anyhow!("Inspection failed")) - } - - let params: MoneyTransferParamsV1 = deserialize(&tx.calls[0].data[1..])?; - eprintln!("Parameters:\n{:#?}", params); - - if params.inputs.len() != 2 { - eprintln!("Found {} inputs, there should be 2", params.inputs.len()); - return Err(anyhow!("Inspection failed")) - } - - if params.outputs.len() != 2 { - eprintln!("Found {} outputs, there should be 2", params.outputs.len()); - return Err(anyhow!("Inspection failed")) - } - - // Try to decrypt one of the outputs. - let secret_keys = self.get_money_secrets().await?; - let mut skey: Option = None; - let mut note: Option = None; - let mut output_idx = 0; - - for output in ¶ms.outputs { - eprintln!("Trying to decrypt note in output {}", output_idx); - - for secret in &secret_keys { - if let Ok(d_note) = output.note.decrypt::(secret) { - let s: SecretKey = deserialize(&d_note.memo)?; - skey = Some(s); - note = Some(d_note); - eprintln!("Successfully decrypted and found an ephemeral secret"); - break - } - } - - if note.is_some() { - break - } - - output_idx += 1; - } - - let Some(note) = note else { - eprintln!("Error: Could not decrypt notes of either output"); - return Err(anyhow!("Inspection failed")) - }; - - eprintln!( - "Output[{}] value: {} ({})", - output_idx, - note.value, - encode_base10(note.value, 8) - ); - eprintln!("Output[{}] token ID: {}", output_idx, note.token_id); - - let skey = skey.unwrap(); - let (pub_x, pub_y) = PublicKey::from_secret(skey).xy(); - let coin = Coin::from(poseidon_hash([ - pub_x, - pub_y, - pallas::Base::from(note.value), - note.token_id.inner(), - note.serial, - ])); - - if coin == params.outputs[output_idx].coin { - eprintln!("Output[{}] coin matches decrypted note metadata", output_idx); - } else { - eprintln!("Error: Output[{}] coin does not match note metadata", output_idx); - return Err(anyhow!("Inspection failed")) - } - - let valcom = pedersen_commitment_u64(note.value, note.value_blind); - let tokcom = poseidon_hash([note.token_id.inner(), note.token_blind]); - - if valcom != params.outputs[output_idx].value_commit { - eprintln!( - "Error: Output[{}] value commitment does not match note metadata", - output_idx - ); - return Err(anyhow!("Inspection failed")) - } - - if tokcom != params.outputs[output_idx].token_commit { - eprintln!( - "Error: Output[{}] token commitment does not match note metadata", - output_idx - ); - return Err(anyhow!("Inspection failed")) - } - - eprintln!("Value and token commitments match decrypted note metadata"); - - // Verify that the output commitments match the other input commitments - match output_idx { - 0 => { - if valcom != params.inputs[1].value_commit || - tokcom != params.inputs[1].token_commit - { - eprintln!("Error: Value/Token commits of output[0] do not match input[1]"); - return Err(anyhow!("Inspection failed")) - } - } - 1 => { - if valcom != params.inputs[0].value_commit || - tokcom != params.inputs[0].token_commit - { - eprintln!("Error: Value/Token commits of output[1] do not match input[0]"); - return Err(anyhow!("Inspection failed")) - } - } - _ => unreachable!(), - } - - eprintln!("Found matching pedersen commitments for outputs and inputs"); - - // TODO: Verify signature - // TODO: Verify ZK proofs - return Ok(()) - } - - // Inspect PartialSwapData - let partial = half.unwrap(); - eprintln!("{}", partial); - - Ok(()) - } - - /// Sign a given transaction by retrieving the secret key from the encrypted - /// note and prepending it to the transaction's signatures. - pub async fn sign_swap(&self, tx: &mut Transaction) -> Result<()> { - // We need our secret keys to try and decrypt the note - let secret_keys = self.get_money_secrets().await?; - let params: MoneyTransferParamsV1 = deserialize(&tx.calls[0].data[1..])?; - - // Our output should be outputs[0] so we try to decrypt that. - let encrypted_note = ¶ms.outputs[0].note; - - eprintln!("Trying to decrypt note in outputs[0]"); - let mut skey = None; - - for secret in &secret_keys { - if let Ok(note) = encrypted_note.decrypt::(secret) { - let s: SecretKey = deserialize(¬e.memo)?; - eprintln!("Successfully decrypted and found an ephemeral secret"); - skey = Some(s); - break - } - } - - let Some(skey) = skey else { - eprintln!("Error: Failed to decrypt note with any of our secret keys"); - return Err(anyhow!("Failed to decrypt note with any of our secret keys")) - }; - - eprintln!("Signing swap transaction"); - let sigs = tx.create_sigs(&mut OsRng, &[skey])?; - tx.signatures[0].insert(0, sigs[0]); - - Ok(()) - } -} diff --git a/bin/drk/src/rpc_token.rs b/bin/drk/src/rpc_token.rs deleted file mode 100644 index 1b38bfbe9..000000000 --- a/bin/drk/src/rpc_token.rs +++ /dev/null @@ -1,153 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use anyhow::{anyhow, Result}; -use darkfi::{ - tx::Transaction, - util::parse::decode_base10, - zk::{proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses}, - zkas::ZkBinary, -}; -use darkfi_money_contract::{ - client::{token_freeze_v1::TokenFreezeCallBuilder, token_mint_v1::TokenMintCallBuilder}, - MoneyFunction, MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1, -}; -use darkfi_sdk::{ - crypto::{contract_id::MONEY_CONTRACT_ID, Keypair, PublicKey, TokenId}, - pasta::pallas, - tx::ContractCall, -}; -use darkfi_serial::Encodable; -use rand::rngs::OsRng; - -use super::Drk; - -impl Drk { - /// Create a token mint transaction. Returns the transaction object on success. - pub async fn mint_token( - &self, - amount: &str, - recipient: PublicKey, - token_id: TokenId, - ) -> Result { - // TODO: Mint directly into DAO treasury - let spend_hook = pallas::Base::zero(); - let user_data = pallas::Base::zero(); - - let amount = decode_base10(amount, 8, false)?; - - let mut tokens = self.list_tokens().await?; - tokens.retain(|x| x.0 == token_id); - if tokens.is_empty() { - return Err(anyhow!("Did not find mint authority for token ID {}", token_id)) - } - assert!(tokens.len() == 1); - - let mint_authority = Keypair::new(tokens[0].1); - - if tokens[0].2 { - return Err(anyhow!("This token mint is marked as frozen in the wallet")) - } - - // Now we need to do a lookup for the zkas proof bincodes, and create - // the circuit objects and proving keys so we can build the transaction. - // We also do this through the RPC. - let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; - let zkas_ns = MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1; - - let Some(token_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == zkas_ns) else { - return Err(anyhow!("Token mint circuit not found")) - }; - - let token_mint_zkbin = ZkBinary::decode(&token_mint_zkbin.1)?; - let token_mint_circuit = - ZkCircuit::new(empty_witnesses(&token_mint_zkbin)?, &token_mint_zkbin); - - eprintln!("Creating token mint circuit proving keys"); - let token_mint_pk = ProvingKey::build(token_mint_zkbin.k, &token_mint_circuit); - let mint_builder = TokenMintCallBuilder { - mint_authority, - recipient, - amount, - spend_hook, - user_data, - token_mint_zkbin, - token_mint_pk, - }; - - eprintln!("Building transaction parameters"); - let debris = mint_builder.build()?; - - // Encode and sign the transaction - let mut data = vec![MoneyFunction::TokenMintV1 as u8]; - debris.params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }]; - let proofs = vec![debris.proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?; - tx.signatures = vec![sigs]; - - Ok(tx) - } - - /// Create a token freeze transaction. Returns the transaction object on success. - pub async fn freeze_token(&self, token_id: TokenId) -> Result { - let mut tokens = self.list_tokens().await?; - tokens.retain(|x| x.0 == token_id); - if tokens.is_empty() { - return Err(anyhow!("Did not find mint authority for token ID {}", token_id)) - } - assert!(tokens.len() == 1); - - let mint_authority = Keypair::new(tokens[0].1); - - if tokens[0].2 { - return Err(anyhow!("This token is already marked as frozen in the wallet")) - } - - let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; - let zkas_ns = MONEY_CONTRACT_ZKAS_TOKEN_FRZ_NS_V1; - - let Some(token_freeze_zkbin) = zkas_bins.iter().find(|x| x.0 == zkas_ns) else { - return Err(anyhow!("Token freeze circuit not found")) - }; - - let token_freeze_zkbin = ZkBinary::decode(&token_freeze_zkbin.1)?; - let token_freeze_circuit = - ZkCircuit::new(empty_witnesses(&token_freeze_zkbin)?, &token_freeze_zkbin); - - eprintln!("Creating token freeze circuit proving keys"); - let token_freeze_pk = ProvingKey::build(token_freeze_zkbin.k, &token_freeze_circuit); - let freeze_builder = - TokenFreezeCallBuilder { mint_authority, token_freeze_zkbin, token_freeze_pk }; - - eprintln!("Building transaction parameters"); - let debris = freeze_builder.build()?; - - // Encode and sign the transaction - let mut data = vec![MoneyFunction::TokenFreezeV1 as u8]; - debris.params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }]; - let proofs = vec![debris.proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &[mint_authority.secret])?; - tx.signatures = vec![sigs]; - - Ok(tx) - } -} diff --git a/bin/drk/src/rpc_transfer.rs b/bin/drk/src/rpc_transfer.rs deleted file mode 100644 index e1955101b..000000000 --- a/bin/drk/src/rpc_transfer.rs +++ /dev/null @@ -1,171 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use std::str::FromStr; - -use anyhow::{anyhow, Result}; -use darkfi::{ - tx::Transaction, - util::parse::{decode_base10, encode_base10}, - zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses}, - zkas::ZkBinary, -}; -use darkfi_dao_contract::model::DaoBulla; -use darkfi_money_contract::{ - client::{transfer_v1::TransferCallBuilder, OwnCoin}, - MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1, -}; -use darkfi_sdk::{ - crypto::{ - contract_id::{DAO_CONTRACT_ID, MONEY_CONTRACT_ID}, - Keypair, PublicKey, TokenId, - }, - pasta::pallas, - tx::ContractCall, -}; -use darkfi_serial::Encodable; -use rand::rngs::OsRng; - -use super::Drk; - -impl Drk { - /// Create a payment transaction. Returns the transaction object on success. - pub async fn transfer( - &self, - amount: &str, - token_id: TokenId, - recipient: PublicKey, - dao: bool, - dao_bulla: Option, - ) -> Result { - let dao_bulla: Option = if dao { - let Some(dao_bulla) = dao_bulla else { - return Err(anyhow!("Missing DAO bulla in parameters")) - }; - - Some(DaoBulla::from_str(dao_bulla.as_str())?) - } else { - None - }; - - let (spend_hook, user_data, user_data_blind) = if dao { - (DAO_CONTRACT_ID.inner(), dao_bulla.unwrap().inner(), pallas::Base::random(&mut OsRng)) - } else { - (pallas::Base::zero(), pallas::Base::zero(), pallas::Base::random(&mut OsRng)) - }; - - // First get all unspent OwnCoins to see what our balance is. - eprintln!("Fetching OwnCoins"); - let owncoins = self.get_coins(false).await?; - let mut owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); - // We're only interested in the ones for the token_id we're sending - // And the ones not owned by some protocol (meaning spend-hook should be 0) - owncoins.retain(|x| x.note.token_id == token_id); - owncoins.retain(|x| x.note.spend_hook == pallas::Base::zero()); - if owncoins.is_empty() { - return Err(anyhow!("Did not find any coins with token ID: {}", token_id)) - } - - // FIXME: Do not hardcode 8 decimals - let amount = decode_base10(amount, 8, false)?; - let mut balance = 0; - for coin in owncoins.iter() { - balance += coin.note.value; - } - - if balance < amount { - return Err(anyhow!( - "Not enough balance for token ID: {}, found: {}", - token_id, - encode_base10(balance, 8) - )) - } - - // We'll also need our Merkle tree - let tree = self.get_money_tree().await?; - - // TODO: Which keypair to actually use? - let secrets = self.get_money_secrets().await?; - let keypair = Keypair::new(secrets[0]); - - let contract_id = *MONEY_CONTRACT_ID; - - // Now we need to do a lookup for the zkas proof bincodes, and create - // the circuit objects and proving keys so we can build the transaction. - // We also do this through the RPC. - let zkas_bins = self.lookup_zkas(&contract_id).await?; - - let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) - else { - return Err(anyhow!("Mint circuit not found")) - }; - - let Some(burn_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1) - else { - return Err(anyhow!("Burn circuit not found")) - }; - - let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?; - let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?; - - let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin); - let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin); - - eprintln!("Creating Mint and Burn circuit proving keys"); - let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit); - let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit); - let transfer_builder = TransferCallBuilder { - keypair, - recipient, - value: amount, - token_id, - rcpt_spend_hook: spend_hook, - rcpt_user_data: user_data, - rcpt_user_data_blind: user_data_blind, - change_spend_hook: pallas::Base::zero(), - change_user_data: pallas::Base::zero(), - change_user_data_blind: user_data_blind, // FIXME: I'm reusing this blind but dunno why - coins: owncoins, - tree, - mint_zkbin, - mint_pk, - burn_zkbin, - burn_pk, - clear_input: false, - }; - - eprintln!("Building transaction parameters"); - let debris = transfer_builder.build()?; - - // Encode and sign the transaction - let mut data = vec![MoneyFunction::TransferV1 as u8]; - debris.params.encode(&mut data)?; - let calls = vec![ContractCall { contract_id, data }]; - let proofs = vec![debris.proofs]; - let mut tx = Transaction { calls, proofs, signatures: vec![] }; - let sigs = tx.create_sigs(&mut OsRng, &debris.signature_secrets)?; - tx.signatures = vec![sigs]; - - // We need to mark the coins we've spent in our wallet - for spent_coin in debris.spent_coins { - self.mark_spent_coin(&spent_coin.coin).await?; - } - - Ok(tx) - } -} diff --git a/bin/drk2/src/swap.rs b/bin/drk/src/swap.rs similarity index 100% rename from bin/drk2/src/swap.rs rename to bin/drk/src/swap.rs diff --git a/bin/drk2/src/token.rs b/bin/drk/src/token.rs similarity index 100% rename from bin/drk2/src/token.rs rename to bin/drk/src/token.rs diff --git a/bin/drk2/src/transfer.rs b/bin/drk/src/transfer.rs similarity index 100% rename from bin/drk2/src/transfer.rs rename to bin/drk/src/transfer.rs diff --git a/bin/drk2/src/txs_history.rs b/bin/drk/src/txs_history.rs similarity index 100% rename from bin/drk2/src/txs_history.rs rename to bin/drk/src/txs_history.rs diff --git a/bin/drk/src/wallet.rs b/bin/drk/src/wallet.rs deleted file mode 100644 index fb6f6da36..000000000 --- a/bin/drk/src/wallet.rs +++ /dev/null @@ -1,43 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use anyhow::Result; -use darkfi::rpc::jsonrpc::JsonRequest; -use serde_json::json; - -use super::Drk; - -impl Drk { - /// Initialize wallet with tables for drk - pub async fn initialize_wallet(&self) -> Result<()> { - let wallet_schema = include_str!("../wallet.sql"); - - // We perform a request to darkfid with the schema to initialize - // the necessary tables in the wallet. - let req = JsonRequest::new("wallet.exec_sql", json!([wallet_schema])); - let rep = self.rpc_client.request(req).await?; - - if rep == true { - eprintln!("Successfully initialized wallet schema for drk"); - } else { - eprintln!("[initialize_wallet] Got unexpected reply from darkfid: {}", rep); - } - - Ok(()) - } -} diff --git a/bin/drk/src/wallet_dao.rs b/bin/drk/src/wallet_dao.rs deleted file mode 100644 index ad9b57358..000000000 --- a/bin/drk/src/wallet_dao.rs +++ /dev/null @@ -1,1322 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use std::{collections::HashMap, fmt}; - -use anyhow::{anyhow, Result}; -use darkfi::{ - rpc::jsonrpc::JsonRequest, tx::Transaction, util::parse::encode_base10, - wallet::walletdb::QueryType, -}; -use darkfi_dao_contract::{ - client::{ - DaoProposeNote, DaoVoteNote, DAO_DAOS_COL_APPROVAL_RATIO_BASE, - DAO_DAOS_COL_APPROVAL_RATIO_QUOT, DAO_DAOS_COL_BULLA_BLIND, DAO_DAOS_COL_CALL_INDEX, - DAO_DAOS_COL_DAO_ID, DAO_DAOS_COL_GOV_TOKEN_ID, DAO_DAOS_COL_LEAF_POSITION, - DAO_DAOS_COL_NAME, DAO_DAOS_COL_PROPOSER_LIMIT, DAO_DAOS_COL_QUORUM, DAO_DAOS_COL_SECRET, - DAO_DAOS_COL_TX_HASH, DAO_DAOS_TABLE, DAO_PROPOSALS_COL_AMOUNT, - DAO_PROPOSALS_COL_BULLA_BLIND, DAO_PROPOSALS_COL_CALL_INDEX, DAO_PROPOSALS_COL_DAO_ID, - DAO_PROPOSALS_COL_LEAF_POSITION, DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, - DAO_PROPOSALS_COL_OUR_VOTE_ID, DAO_PROPOSALS_COL_PROPOSAL_ID, - DAO_PROPOSALS_COL_RECV_PUBLIC, DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, - DAO_PROPOSALS_COL_TX_HASH, DAO_PROPOSALS_TABLE, DAO_TREES_COL_DAOS_TREE, - DAO_TREES_COL_PROPOSALS_TREE, DAO_TREES_TABLE, DAO_VOTES_COL_ALL_VOTE_BLIND, - DAO_VOTES_COL_ALL_VOTE_VALUE, DAO_VOTES_COL_CALL_INDEX, DAO_VOTES_COL_PROPOSAL_ID, - DAO_VOTES_COL_TX_HASH, DAO_VOTES_COL_VOTE_ID, DAO_VOTES_COL_VOTE_OPTION, - DAO_VOTES_COL_YES_VOTE_BLIND, DAO_VOTES_TABLE, - }, - model::{DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams}, - DaoFunction, -}; -use darkfi_sdk::{ - bridgetree, - crypto::{ - poseidon_hash, Keypair, MerkleNode, MerkleTree, PublicKey, SecretKey, TokenId, - DAO_CONTRACT_ID, - }, - pasta::pallas, -}; -use darkfi_serial::{deserialize, serialize, SerialDecodable, SerialEncodable}; -use serde_json::json; - -use super::Drk; - -#[derive(Debug, Clone, SerialEncodable, SerialDecodable)] -/// Parameters representing a DAO to be initialized -pub struct DaoParams { - /// The minimum amount of governance tokens needed to open a proposal - pub proposer_limit: u64, - /// Minimal threshold of participating total tokens needed for a proposal to pass - pub quorum: u64, - /// The ratio of winning/total votes needed for a proposal to pass - pub approval_ratio_base: u64, - pub approval_ratio_quot: u64, - /// DAO's governance token ID - pub gov_token_id: TokenId, - /// Secret key for the DAO - pub secret_key: SecretKey, - /// DAO bulla blind - pub bulla_blind: pallas::Base, -} - -impl fmt::Display for DaoParams { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = format!( - "{}\n{}\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {:?}", - "DAO Parameters", - "==============", - "Proposer limit", - encode_base10(self.proposer_limit, 8), - self.proposer_limit, - "Quorum", - encode_base10(self.quorum, 8), - self.quorum, - "Approval ratio", - self.approval_ratio_quot as f64 / self.approval_ratio_base as f64, - "Governance Token ID", - self.gov_token_id, - "Public key", - PublicKey::from_secret(self.secret_key), - "Secret key", - self.secret_key, - "Bulla blind", - self.bulla_blind, - ); - - write!(f, "{}", s) - } -} - -#[derive(Debug, Clone)] -/// Parameters representing an intialized DAO, optionally deployed on-chain -pub struct Dao { - /// Numeric identifier for the DAO - pub id: u64, - /// Named identifier for the DAO - pub name: String, - /// The minimum amount of governance tokens needed to open a proposal - pub proposer_limit: u64, - /// Minimal threshold of participating total tokens needed for a proposal to pass - pub quorum: u64, - /// The ratio of winning/total votes needed for a proposal to pass - pub approval_ratio_base: u64, - pub approval_ratio_quot: u64, - /// DAO's governance token ID - pub gov_token_id: TokenId, - /// Secret key for the DAO - pub secret_key: SecretKey, - /// DAO bulla blind - pub bulla_blind: pallas::Base, - /// Leaf position of the DAO in the Merkle tree of DAOs - pub leaf_position: Option, - /// The transaction hash where the DAO was deployed - pub tx_hash: Option, - /// The call index in the transaction where the DAO was deployed - pub call_index: Option, -} - -impl Dao { - pub fn bulla(&self) -> DaoBulla { - let (x, y) = PublicKey::from_secret(self.secret_key).xy(); - - DaoBulla::from(poseidon_hash([ - pallas::Base::from(self.proposer_limit), - pallas::Base::from(self.quorum), - pallas::Base::from(self.approval_ratio_quot), - pallas::Base::from(self.approval_ratio_base), - self.gov_token_id.inner(), - x, - y, - self.bulla_blind, - ])) - } - - pub fn keypair(&self) -> Keypair { - let public = PublicKey::from_secret(self.secret_key); - Keypair { public, secret: self.secret_key } - } -} - -impl fmt::Display for Dao { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = format!( - "{}\n{}\n{}: {}\n{}: {}\n{}: {} ({})\n{}: {} ({})\n{}: {}\n{}: {}\n{}: {}\n{}: {}\n{}: {:?}\n{}: {:?}\n{}: {:?}\n{}: {:?}", - "DAO Parameters", - "==============", - "Name", - self.name, - "Bulla", - self.bulla(), - "Proposer limit", - encode_base10(self.proposer_limit, 8), - self.proposer_limit, - "Quorum", - encode_base10(self.quorum, 8), - self.quorum, - "Approval ratio", - self.approval_ratio_quot as f64 / self.approval_ratio_base as f64, - "Governance Token ID", - self.gov_token_id, - "Public key", - PublicKey::from_secret(self.secret_key), - "Secret key", - self.secret_key, - "Bulla blind", - self.bulla_blind, - "Leaf position", - self.leaf_position, - "Tx hash", - self.tx_hash, - "Call idx", - self.call_index, - ); - - write!(f, "{}", s) - } -} - -#[derive(Debug, Clone)] -/// Parameters representing an initialized DAO proposal, optionally deployed on-chain -pub struct DaoProposal { - /// Numeric identifier for the proposal - pub id: u64, - /// The DAO bulla related to this proposal - pub dao_bulla: DaoBulla, - /// Recipient of this proposal's funds - pub recipient: PublicKey, - /// Amount of this proposal - pub amount: u64, - /// Token ID to be sent - pub token_id: TokenId, - /// Proposal's bulla blind - pub bulla_blind: pallas::Base, - /// Leaf position of this proposal in the Merkle tree of proposals - pub leaf_position: Option, - /// Snapshotted Money Merkle tree - pub money_snapshot_tree: Option, - /// Transaction hash where this proposal was proposed - pub tx_hash: Option, - /// call index in the transaction where this proposal was proposed - pub call_index: Option, - /// The vote ID we've voted on this proposal - pub vote_id: Option, -} - -impl DaoProposal { - pub fn bulla(&self) -> pallas::Base { - let (dest_x, dest_y) = self.recipient.xy(); - - poseidon_hash([ - dest_x, - dest_y, - pallas::Base::from(self.amount), - self.token_id.inner(), - self.dao_bulla.inner(), - self.bulla_blind, - ]) - } -} - -impl fmt::Display for DaoProposal { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = format!( - concat!( - "Proposal parameters\n", - "===================\n", - "DAO Bulla: {}\n", - "Recipient: {}\n", - "Proposal amount: {} ({})\n", - "Proposal Token ID: {:?}\n", - "Proposal bulla blind: {:?}\n", - "Proposal leaf position: {:?}\n", - "Proposal tx hash: {:?}\n", - "Proposal call index: {:?}\n", - "Proposal vote ID: {:?}", - ), - self.dao_bulla, - self.recipient, - encode_base10(self.amount, 8), - self.amount, - self.token_id, - self.bulla_blind, - self.leaf_position, - self.tx_hash, - self.call_index, - self.vote_id, - ); - - write!(f, "{}", s) - } -} - -#[derive(Debug, Clone)] -/// Parameters representing a vote we've made on a DAO proposal -pub struct DaoVote { - /// Numeric identifier for the vote - pub id: u64, - /// Numeric identifier for the proposal related to this vote - pub proposal_id: u64, - /// The vote - pub vote_option: bool, - /// Blinding factor for the yes vote - pub yes_vote_blind: pallas::Scalar, - /// Value of all votes - pub all_vote_value: u64, - /// Blinding facfor of all votes - pub all_vote_blind: pallas::Scalar, - /// Transaction hash where this vote was casted - pub tx_hash: Option, - /// call index in the transaction where this vote was casted - pub call_index: Option, -} - -impl Drk { - /// Initialize wallet with tables for the DAO contract - pub async fn initialize_dao(&self) -> Result<()> { - let wallet_schema = include_str!("../../../src/contract/dao/wallet.sql"); - - // We perform a request to darkfid with the schema to initialize - // the necessary tables in the wallet. - let req = JsonRequest::new("wallet.exec_sql", json!([wallet_schema])); - let rep = self.rpc_client.request(req).await?; - - if rep == true { - eprintln!("Successfully initialized wallet schema for the DAO contract"); - } else { - eprintln!("[initialize_dao] Got unexpected reply from darkfid: {}", rep); - } - - // Check if we have to initialize the Merkle trees. - // We check if one exists, but we actually create two. This should be written - // a bit better and safer. - let mut tree_needs_init = false; - let query = format!("SELECT {} FROM {}", DAO_TREES_COL_DAOS_TREE, DAO_TREES_TABLE); - let params = json!([query, QueryType::Blob as u8, DAO_TREES_COL_DAOS_TREE]); - let req = JsonRequest::new("wallet.query_row_single", params); - - // For now, on success, we don't care what's returned, but in the future - // we should actually check it. - // TODO: The RPC needs a better variant for errors so detailed inspection - // can be done with error codes and all that. - if (self.rpc_client.request(req).await).is_err() { - tree_needs_init = true; - } - - if tree_needs_init { - eprintln!("Initializing DAO Merkle trees"); - let tree = MerkleTree::new(100); - self.put_dao_trees(&tree, &tree).await?; - eprintln!("Successfully initialized Merkle trees for the DAO contract"); - } - - Ok(()) - } - - /// Fetch all DAO secret keys from the wallet - pub async fn get_dao_secrets(&self) -> Result> { - let daos = self.get_daos().await?; - let mut ret = Vec::with_capacity(daos.len()); - for dao in daos { - ret.push(dao.secret_key); - } - - Ok(ret) - } - - /// Replace the DAO Merkle trees in the wallet. - pub async fn put_dao_trees( - &self, - daos_tree: &MerkleTree, - proposals_tree: &MerkleTree, - ) -> Result<()> { - let query = format!( - "DELETE FROM {}; INSERT INTO {} ({}, {}) VALUES (?1, ?2);", - DAO_TREES_TABLE, DAO_TREES_TABLE, DAO_TREES_COL_DAOS_TREE, DAO_TREES_COL_PROPOSALS_TREE, - ); - - let params = json!([ - query, - QueryType::Blob as u8, - serialize(daos_tree), - QueryType::Blob as u8, - serialize(proposals_tree), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Fetch DAO Merkle trees from the wallet - pub async fn get_dao_trees(&self) -> Result<(MerkleTree, MerkleTree)> { - let query = format!("SELECT * FROM {}", DAO_TREES_TABLE); - - let params = json!([ - query, - QueryType::Blob as u8, - DAO_TREES_COL_DAOS_TREE, - QueryType::Blob as u8, - DAO_TREES_COL_PROPOSALS_TREE, - ]); - - let req = JsonRequest::new("wallet.query_row_single", params); - let rep = self.rpc_client.request(req).await?; - - let daos_tree_bytes: Vec = serde_json::from_value(rep[0].clone())?; - let daos_tree = deserialize(&daos_tree_bytes)?; - - let proposals_tree_bytes: Vec = serde_json::from_value(rep[1].clone())?; - let proposals_tree = deserialize(&proposals_tree_bytes)?; - - Ok((daos_tree, proposals_tree)) - } - - /// Reset the DAO Merkle trees in the wallet - pub async fn reset_dao_trees(&self) -> Result<()> { - eprintln!("Resetting DAO Merkle trees"); - let tree = MerkleTree::new(100); - self.put_dao_trees(&tree, &tree).await?; - eprintln!("Successfully reset DAO Merkle trees"); - - Ok(()) - } - - /// Reset confirmed DAOs in the wallet - pub async fn reset_daos(&self) -> Result<()> { - eprintln!("Resetting DAO confirmations"); - let daos = self.get_daos().await?; - self.unconfirm_daos(&daos).await?; - eprintln!("Successfully unconfirmed DAOs"); - - Ok(()) - } - - pub async fn reset_dao_proposals(&self) -> Result<()> { - eprintln!("Resetting DAO proposals"); - let query = format!("DELETE FROM {};", DAO_PROPOSALS_TABLE); - - let params = json!([query]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - pub async fn reset_dao_votes(&self) -> Result<()> { - eprintln!("Resetting DAO votes"); - let query = format!("DELETE FROM {};", DAO_VOTES_TABLE); - - let params = json!([query]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - pub async fn get_dao_id_by_alias(&self, alias_filter: &str) -> Result { - let query = format!( - "SELECT {}, {} FROM {}", - DAO_DAOS_COL_DAO_ID, DAO_DAOS_COL_NAME, DAO_DAOS_TABLE, - ); - let params = json!([ - query, - QueryType::Integer as u8, - DAO_DAOS_COL_DAO_ID, - QueryType::Text as u8, - DAO_DAOS_COL_NAME, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - // The returned thing should be an array of found rows. - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[get_dao_id_by_alias] Unexpected response from darkfid: {}", rep)) - }; - - for row in rows { - let Some(row) = row.as_array() else { - return Err(anyhow!( - "[get_dao_id_by_alias] Unexpected response from darkfid: {}", - rep - )) - }; - - let alias: String = serde_json::from_value(row[1].clone())?; - if alias != alias_filter { - continue - } - - let dao_id: u64 = serde_json::from_value(row[0].clone())?; - return Ok(dao_id) - } - - Err(anyhow!("[get_dao_id_by_alias] DAO not found")) - } - - /// Convenience function. Interprets the alias either as the DAO alias or its ID - pub async fn get_dao_id(&self, alias: &str) -> Result { - if let Ok(id) = self.get_dao_id_by_alias(alias).await { - return Ok(id) - } - Ok(alias.parse()?) - } - - /// Import given DAO params into the wallet with a given name. - pub async fn import_dao(&self, dao_name: String, dao_params: DaoParams) -> Result<()> { - // First let's check if we've imported this DAO with the given name before. - let daos = self.get_daos().await?; - if daos.iter().any(|x| x.name == dao_name) { - return Err(anyhow!("This DAO has already been imported")) - } - - eprintln!("Importing \"{}\" DAO into the wallet", dao_name); - - let query = format!( - "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);", - DAO_DAOS_TABLE, - DAO_DAOS_COL_NAME, - DAO_DAOS_COL_PROPOSER_LIMIT, - DAO_DAOS_COL_QUORUM, - DAO_DAOS_COL_APPROVAL_RATIO_BASE, - DAO_DAOS_COL_APPROVAL_RATIO_QUOT, - DAO_DAOS_COL_GOV_TOKEN_ID, - DAO_DAOS_COL_SECRET, - DAO_DAOS_COL_BULLA_BLIND, - ); - - let params = json!([ - query, - QueryType::Text as u8, - dao_name, - QueryType::Blob as u8, - serialize(&dao_params.proposer_limit), - QueryType::Blob as u8, - serialize(&dao_params.quorum), - QueryType::Integer as u8, - dao_params.approval_ratio_base, - QueryType::Integer as u8, - dao_params.approval_ratio_quot, - QueryType::Blob as u8, - serialize(&dao_params.gov_token_id), - QueryType::Blob as u8, - serialize(&dao_params.secret_key), - QueryType::Blob as u8, - serialize(&dao_params.bulla_blind), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - eprintln!("DAO imported successfully"); - - Ok(()) - } - - /// List DAO(s) imported in the wallet. If an ID is given, just print the - /// metadata for that specific one, if found. - pub async fn dao_list(&self, dao_id: Option) -> Result<()> { - if let Some(dao_id) = dao_id { - return self.dao_list_single(dao_id).await - } - - let daos = self.get_daos().await?; - for dao in daos { - println!("[{}] {}", dao.id, dao.name); - } - - Ok(()) - } - - async fn dao_list_single(&self, dao_id: u64) -> Result<()> { - let dao = self.get_dao_by_id(dao_id).await?; - - println!("{}", dao); - - Ok(()) - } - - /// Fetch a DAO given a numeric ID - pub async fn get_dao_by_id(&self, dao_id: u64) -> Result { - let daos = self.get_daos().await?; - - let Some(dao) = daos.iter().find(|x| x.id == dao_id) else { - return Err(anyhow!("DAO not found in wallet")) - }; - - Ok(dao.clone()) - } - - /// Fetch all known DAOs from the wallet. - pub async fn get_daos(&self) -> Result> { - let query = format!("SELECT * FROM {}", DAO_DAOS_TABLE); - - let params = json!([ - query, - QueryType::Integer as u8, - DAO_DAOS_COL_DAO_ID, - QueryType::Text as u8, - DAO_DAOS_COL_NAME, - QueryType::Blob as u8, - DAO_DAOS_COL_PROPOSER_LIMIT, - QueryType::Blob as u8, - DAO_DAOS_COL_QUORUM, - QueryType::Integer as u8, - DAO_DAOS_COL_APPROVAL_RATIO_BASE, - QueryType::Integer as u8, - DAO_DAOS_COL_APPROVAL_RATIO_QUOT, - QueryType::Blob as u8, - DAO_DAOS_COL_GOV_TOKEN_ID, - QueryType::Blob as u8, - DAO_DAOS_COL_SECRET, - QueryType::Blob as u8, - DAO_DAOS_COL_BULLA_BLIND, - QueryType::OptionBlob as u8, - DAO_DAOS_COL_LEAF_POSITION, - QueryType::OptionBlob as u8, - DAO_DAOS_COL_TX_HASH, - QueryType::OptionInteger as u8, - DAO_DAOS_COL_CALL_INDEX, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[get_daos] Unexpected response from darkfid: {}", rep)) - }; - - let mut daos = Vec::with_capacity(rows.len()); - - for row in rows { - let Some(row) = row.as_array() else { - return Err(anyhow!("[get_daos] Unexpected response from darkfid: {}", rep)) - }; - - let id: u64 = serde_json::from_value(row[0].clone())?; - - let name: String = serde_json::from_value(row[1].clone())?; - - let proposer_limit_bytes: Vec = serde_json::from_value(row[2].clone())?; - let proposer_limit = deserialize(&proposer_limit_bytes)?; - - let quorum_bytes: Vec = serde_json::from_value(row[3].clone())?; - let quorum = deserialize(&quorum_bytes)?; - - let approval_ratio_base = serde_json::from_value(row[4].clone())?; - let approval_ratio_quot = serde_json::from_value(row[5].clone())?; - - let gov_token_bytes: Vec = serde_json::from_value(row[6].clone())?; - let gov_token_id = deserialize(&gov_token_bytes)?; - - let secret_bytes: Vec = serde_json::from_value(row[7].clone())?; - let secret_key = deserialize(&secret_bytes)?; - - let bulla_blind_bytes: Vec = serde_json::from_value(row[8].clone())?; - let bulla_blind = deserialize(&bulla_blind_bytes)?; - - let leaf_position_bytes: Vec = serde_json::from_value(row[9].clone())?; - let tx_hash_bytes: Vec = serde_json::from_value(row[10].clone())?; - let call_index = serde_json::from_value(row[11].clone())?; - - let leaf_position = if leaf_position_bytes.is_empty() { - None - } else { - Some(deserialize(&leaf_position_bytes)?) - }; - - let tx_hash = - if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) }; - - let dao = Dao { - id, - name, - proposer_limit, - quorum, - approval_ratio_base, - approval_ratio_quot, - gov_token_id, - secret_key, - bulla_blind, - leaf_position, - tx_hash, - call_index, - }; - - daos.push(dao); - } - - // Here we sort the vec by ID. The SQL SELECT statement does not guarantee - // this, so just do it here. - daos.sort_by(|a, b| a.id.cmp(&b.id)); - Ok(daos) - } - - /// Fetch known unspent balances from the wallet for the given DAO ID - pub async fn dao_balance(&self, dao_id: u64) -> Result> { - let daos = self.get_daos().await?; - let Some(dao) = daos.get(dao_id as usize - 1) else { - return Err(anyhow!("DAO with ID {} not found in wallet", dao_id)) - }; - - let mut coins = self.get_coins(false).await?; - coins.retain(|x| x.0.note.spend_hook == DAO_CONTRACT_ID.inner()); - coins.retain(|x| x.0.note.user_data == dao.bulla().inner()); - - // Fill this map with balances - let mut balmap: HashMap = HashMap::new(); - - for coin in coins { - let mut value = coin.0.note.value; - - if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) { - value += prev; - } - - balmap.insert(coin.0.note.token_id.to_string(), value); - } - - Ok(balmap) - } - - /// Fetch all known DAO proposals from the wallet given a DAO ID - pub async fn get_dao_proposals(&self, dao_id: u64) -> Result> { - let daos = self.get_daos().await?; - let Some(dao) = daos.get(dao_id as usize - 1) else { - return Err(anyhow!("DAO with ID {} not found in wallet", dao_id)) - }; - - let query = format!( - "SELECT * FROM {} WHERE {} = {}", - DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_DAO_ID, dao_id - ); - - let params = json!([ - query, - QueryType::Integer as u8, - DAO_PROPOSALS_COL_PROPOSAL_ID, - QueryType::Integer as u8, - DAO_PROPOSALS_COL_DAO_ID, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_RECV_PUBLIC, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_AMOUNT, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_BULLA_BLIND, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_LEAF_POSITION, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_TX_HASH, - QueryType::OptionInteger as u8, - DAO_PROPOSALS_COL_CALL_INDEX, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_OUR_VOTE_ID, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[get_proposals] Unexpected response from darkfid: {}", rep)) - }; - - let mut proposals = Vec::with_capacity(rows.len()); - - for row in rows { - let Some(row) = row.as_array() else { - return Err(anyhow!("[get_proposals] Unexpected response from darkfid: {}", rep)) - }; - - let id: u64 = serde_json::from_value(row[0].clone())?; - - let dao_id: u64 = serde_json::from_value(row[1].clone())?; - assert!(dao_id == dao.id); - let dao_bulla = dao.bulla(); - - let recipient_bytes: Vec = serde_json::from_value(row[2].clone())?; - let recipient = deserialize(&recipient_bytes)?; - - let amount_bytes: Vec = serde_json::from_value(row[3].clone())?; - let amount = deserialize(&amount_bytes)?; - - let token_id_bytes: Vec = serde_json::from_value(row[4].clone())?; - let token_id = deserialize(&token_id_bytes)?; - - let bulla_blind_bytes: Vec = serde_json::from_value(row[5].clone())?; - let bulla_blind = deserialize(&bulla_blind_bytes)?; - - let leaf_position_bytes: Vec = serde_json::from_value(row[6].clone())?; - - let money_snapshot_tree_bytes: Vec = serde_json::from_value(row[7].clone())?; - - let tx_hash_bytes: Vec = serde_json::from_value(row[8].clone())?; - - let call_index = serde_json::from_value(row[9].clone())?; - - let vote_id_bytes: Vec = serde_json::from_value(row[10].clone())?; - - let leaf_position = if leaf_position_bytes.is_empty() { - None - } else { - Some(deserialize(&leaf_position_bytes)?) - }; - - let money_snapshot_tree = if money_snapshot_tree_bytes.is_empty() { - None - } else { - Some(deserialize(&money_snapshot_tree_bytes)?) - }; - - let tx_hash = - if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) }; - - let vote_id = - if vote_id_bytes.is_empty() { None } else { Some(deserialize(&vote_id_bytes)?) }; - - let proposal = DaoProposal { - id, - dao_bulla, - recipient, - amount, - token_id, - bulla_blind, - leaf_position, - money_snapshot_tree, - tx_hash, - call_index, - vote_id, - }; - - proposals.push(proposal); - } - - // Here we sort the vec by ID. The SQL SELECT statement does not guarantee - // this, so just do it here. - proposals.sort_by(|a, b| a.id.cmp(&b.id)); - Ok(proposals) - } - - /// Fetch a DAO proposal by its ID - pub async fn get_dao_proposal_by_id(&self, proposal_id: u64) -> Result { - let query = format!( - "SELECT * FROM {} WHERE {} = {}", - DAO_PROPOSALS_TABLE, DAO_PROPOSALS_COL_PROPOSAL_ID, proposal_id - ); - - let params = json!([ - query, - QueryType::Integer as u8, - DAO_PROPOSALS_COL_PROPOSAL_ID, - QueryType::Integer as u8, - DAO_PROPOSALS_COL_DAO_ID, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_RECV_PUBLIC, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_AMOUNT, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, - QueryType::Blob as u8, - DAO_PROPOSALS_COL_BULLA_BLIND, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_LEAF_POSITION, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_TX_HASH, - QueryType::OptionInteger as u8, - DAO_PROPOSALS_COL_CALL_INDEX, - QueryType::OptionBlob as u8, - DAO_PROPOSALS_COL_OUR_VOTE_ID, - ]); - - let req = JsonRequest::new("wallet.query_row_single", params); - let rep = self.rpc_client.request(req).await?; - - let Some(row) = rep.as_array() else { - return Err(anyhow!("[get_proposal_by_id] Unexpected response from darkfid: {}", rep)) - }; - - let id: u64 = serde_json::from_value(row[0].clone())?; - let dao_id: u64 = serde_json::from_value(row[1].clone())?; - - let recipient_bytes: Vec = serde_json::from_value(row[2].clone())?; - let recipient = deserialize(&recipient_bytes)?; - - let amount_bytes: Vec = serde_json::from_value(row[3].clone())?; - let amount = deserialize(&amount_bytes)?; - - let token_id_bytes: Vec = serde_json::from_value(row[4].clone())?; - let token_id = deserialize(&token_id_bytes)?; - - let bulla_blind_bytes: Vec = serde_json::from_value(row[5].clone())?; - let bulla_blind = deserialize(&bulla_blind_bytes)?; - - let leaf_position_bytes: Vec = serde_json::from_value(row[6].clone())?; - - let money_snapshot_tree_bytes: Vec = serde_json::from_value(row[7].clone())?; - - let tx_hash_bytes: Vec = serde_json::from_value(row[8].clone())?; - - let call_index = serde_json::from_value(row[9].clone())?; - - let vote_id_bytes: Vec = serde_json::from_value(row[10].clone())?; - - let leaf_position = if leaf_position_bytes.is_empty() { - None - } else { - Some(deserialize(&leaf_position_bytes)?) - }; - - let tx_hash = - if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) }; - - let money_snapshot_tree = if money_snapshot_tree_bytes.is_empty() { - None - } else { - Some(deserialize(&money_snapshot_tree_bytes)?) - }; - - let vote_id = - if vote_id_bytes.is_empty() { None } else { Some(deserialize(&vote_id_bytes)?) }; - - let dao = self.get_dao_by_id(dao_id).await?; - - let proposal = DaoProposal { - id, - dao_bulla: dao.bulla(), - recipient, - amount, - token_id, - bulla_blind, - leaf_position, - money_snapshot_tree, - tx_hash, - call_index, - vote_id, - }; - - Ok(proposal) - } - - // Fetch all known DAO proposal votes from the wallet given a proposal ID - pub async fn get_dao_proposal_votes(&self, proposal_id: u64) -> Result> { - let query = format!( - "SELECT * FROM {} WHERE {} = {}", - DAO_VOTES_TABLE, DAO_VOTES_COL_PROPOSAL_ID, proposal_id - ); - - let params = json!([ - query, - QueryType::Integer as u8, - DAO_VOTES_COL_VOTE_ID, - QueryType::Integer as u8, - DAO_VOTES_COL_PROPOSAL_ID, - QueryType::Integer as u8, - DAO_VOTES_COL_VOTE_OPTION, - QueryType::Blob as u8, - DAO_VOTES_COL_YES_VOTE_BLIND, - QueryType::Blob as u8, - DAO_VOTES_COL_ALL_VOTE_VALUE, - QueryType::Blob as u8, - DAO_VOTES_COL_ALL_VOTE_BLIND, - QueryType::OptionBlob as u8, - DAO_VOTES_COL_TX_HASH, - QueryType::OptionInteger as u8, - DAO_VOTES_COL_CALL_INDEX, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - let Some(rows) = rep.as_array() else { - return Err(anyhow!( - "[get_dao_proposal_votes] Unexpected response from darkfid: {}", - rep - )) - }; - - let mut votes = Vec::with_capacity(rows.len()); - - for row in rows { - let Some(row) = row.as_array() else { - return Err(anyhow!( - "[get_dao_proposal_votes] Unexpected response from darkfid: {}", - rep - )) - }; - - let id: u64 = serde_json::from_value(row[0].clone())?; - let proposal_id: u64 = serde_json::from_value(row[1].clone())?; - let vote_option: u32 = serde_json::from_value(row[2].clone())?; - let vote_option = vote_option != 0; - - let yes_vote_blind_bytes: Vec = serde_json::from_value(row[3].clone())?; - let yes_vote_blind = deserialize(&yes_vote_blind_bytes)?; - - let all_vote_value_bytes: Vec = serde_json::from_value(row[4].clone())?; - let all_vote_value = deserialize(&all_vote_value_bytes)?; - - let all_vote_blind_bytes: Vec = serde_json::from_value(row[5].clone())?; - let all_vote_blind = deserialize(&all_vote_blind_bytes)?; - - let tx_hash_bytes: Vec = serde_json::from_value(row[6].clone())?; - - let call_index = serde_json::from_value(row[7].clone())?; - - let tx_hash = - if tx_hash_bytes.is_empty() { None } else { Some(deserialize(&tx_hash_bytes)?) }; - - let vote = DaoVote { - id, - proposal_id, - vote_option, - yes_vote_blind, - all_vote_value, - all_vote_blind, - tx_hash, - call_index, - }; - - votes.push(vote); - } - - Ok(votes) - } - - /// Append data related to DAO contract transactions into the wallet database. - /// Optionally, if `confirm` is true, also append the data in the Merkle trees, etc. - pub async fn apply_tx_dao_data(&self, tx: &Transaction, confirm: bool) -> Result<()> { - let cid = *DAO_CONTRACT_ID; - let mut daos = self.get_daos().await?; - let mut daos_to_confirm = vec![]; - let (mut daos_tree, mut proposals_tree) = self.get_dao_trees().await?; - - // DAOs that have been minted - let mut new_dao_bullas: Vec<(DaoBulla, Option, u32)> = vec![]; - // DAO proposals that have been minted - let mut new_dao_proposals: Vec<( - DaoProposeParams, - Option, - Option, - u32, - )> = vec![]; - let mut our_proposals: Vec = vec![]; - // DAO votes that have been seen - let mut new_dao_votes: Vec<(DaoVoteParams, Option, u32)> = vec![]; - let mut dao_votes: Vec = vec![]; - - // Run through the transaction and see what we got: - for (i, call) in tx.calls.iter().enumerate() { - if call.contract_id == cid && call.data[0] == DaoFunction::Mint as u8 { - eprintln!("Found Dao::Mint in call {}", i); - let params: DaoMintParams = deserialize(&call.data[1..])?; - let tx_hash = if confirm { Some(blake3::hash(&serialize(tx))) } else { None }; - new_dao_bullas.push((params.dao_bulla, tx_hash, i as u32)); - continue - } - - if call.contract_id == cid && call.data[0] == DaoFunction::Propose as u8 { - eprintln!("Found Dao::Propose in call {}", i); - let params: DaoProposeParams = deserialize(&call.data[1..])?; - let tx_hash = if confirm { Some(blake3::hash(&serialize(tx))) } else { None }; - // We need to clone the tree here for reproducing the snapshot Merkle root - let money_tree = if confirm { Some(self.get_money_tree().await?) } else { None }; - new_dao_proposals.push((params, money_tree, tx_hash, i as u32)); - continue - } - - if call.contract_id == cid && call.data[0] == DaoFunction::Vote as u8 { - eprintln!("Found Dao::Vote in call {}", i); - let params: DaoVoteParams = deserialize(&call.data[1..])?; - let tx_hash = if confirm { Some(blake3::hash(&serialize(tx))) } else { None }; - new_dao_votes.push((params, tx_hash, i as u32)); - continue - } - - if call.contract_id == cid && call.data[0] == DaoFunction::Exec as u8 { - // This seems to not need any special action - eprintln!("Found Dao::Exec in call {}", i); - continue - } - } - - // This code should only be executed when finalized blocks are being scanned. - // Here we write the tx metadata, and actually do Merkle tree appends so we - // have to make sure it's the same for everyone. - if confirm { - for new_bulla in new_dao_bullas { - daos_tree.append(MerkleNode::from(new_bulla.0.inner())); - for dao in daos.iter_mut() { - if dao.bulla() == new_bulla.0 { - eprintln!( - "Found minted DAO {}, noting down for wallet update", - new_bulla.0 - ); - // We have this DAO imported in our wallet. Add the metadata: - dao.leaf_position = daos_tree.mark(); - dao.tx_hash = new_bulla.1; - dao.call_index = Some(new_bulla.2); - daos_to_confirm.push(dao.clone()); - } - } - } - - for proposal in new_dao_proposals { - proposals_tree.append(MerkleNode::from(proposal.0.proposal_bulla.inner())); - - // If we're able to decrypt this note, that's the way to link it - // to a specific DAO. - for dao in &daos { - if let Ok(note) = proposal.0.note.decrypt::(&dao.secret_key) { - // We managed to decrypt it. Let's place this in a proper - // DaoProposal object. We assume we can just increment the - // ID by looking at how many proposals we already have. - // We also assume we don't mantain duplicate DAOs in the - // wallet. - eprintln!("Managed to decrypt DAO proposal note"); - let daos_proposals = self.get_dao_proposals(dao.id).await?; - let our_prop = DaoProposal { - // This ID stuff is flaky. - id: daos_proposals.len() as u64 + our_proposals.len() as u64 + 1, - dao_bulla: dao.bulla(), - recipient: note.proposal.dest, - amount: note.proposal.amount, - token_id: note.proposal.token_id, - bulla_blind: note.proposal.blind, - leaf_position: proposals_tree.mark(), - money_snapshot_tree: proposal.1, - tx_hash: proposal.2, - call_index: Some(proposal.3), - vote_id: None, - }; - - our_proposals.push(our_prop); - break - } - } - } - - for vote in new_dao_votes { - for dao in &daos { - if let Ok(note) = vote.0.note.decrypt::(&dao.secret_key) { - eprintln!("Managed to decrypt DAO proposal vote note"); - let daos_proposals = self.get_dao_proposals(dao.id).await?; - let mut proposal_id = None; - - for i in daos_proposals { - if i.bulla() == vote.0.proposal_bulla.inner() { - proposal_id = Some(i.id); - break - } - } - - if proposal_id.is_none() { - eprintln!("Warning: Decrypted DaoVoteNote but did not find proposal"); - break - } - - let v = DaoVote { - id: 0, - proposal_id: proposal_id.unwrap(), - vote_option: note.vote_option, - yes_vote_blind: note.yes_vote_blind, - all_vote_value: note.all_vote_value, - all_vote_blind: note.all_vote_blind, - tx_hash: vote.1, - call_index: Some(vote.2), - }; - - dao_votes.push(v); - } - } - } - } - - if confirm { - self.put_dao_trees(&daos_tree, &proposals_tree).await?; - self.confirm_daos(&daos_to_confirm).await?; - self.put_dao_proposals(&our_proposals).await?; - self.put_dao_votes(&dao_votes).await?; - } - - Ok(()) - } - - /// Confirm already imported DAO metadata into the wallet. - /// Here we just write the leaf position, tx hash, and call index. - /// Panics if the fields are None. - pub async fn confirm_daos(&self, daos: &[Dao]) -> Result<()> { - for dao in daos { - let query = format!( - "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = {};", - DAO_DAOS_TABLE, - DAO_DAOS_COL_LEAF_POSITION, - DAO_DAOS_COL_TX_HASH, - DAO_DAOS_COL_CALL_INDEX, - DAO_DAOS_COL_DAO_ID, - dao.id, - ); - - let params = json!([ - query, - QueryType::Blob as u8, - serialize(&dao.leaf_position.unwrap()), - QueryType::Blob as u8, - serialize(&dao.tx_hash.unwrap()), - QueryType::Integer as u8, - dao.call_index.unwrap(), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - - Ok(()) - } - - /// Unconfirm imported DAOs by removing the leaf position, txid, and call index. - pub async fn unconfirm_daos(&self, daos: &[Dao]) -> Result<()> { - for dao in daos { - let query = format!( - "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = {};", - DAO_DAOS_TABLE, - DAO_DAOS_COL_LEAF_POSITION, - DAO_DAOS_COL_TX_HASH, - DAO_DAOS_COL_CALL_INDEX, - DAO_DAOS_COL_DAO_ID, - dao.id, - ); - - let params = json!([ - query, - QueryType::OptionBlob as u8, - None::>, - QueryType::OptionBlob as u8, - None::>, - QueryType::OptionInteger as u8, - None::, - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - - Ok(()) - } - - /// Import given DAO proposals into the wallet - pub async fn put_dao_proposals(&self, proposals: &[DaoProposal]) -> Result<()> { - let daos = self.get_daos().await?; - - for proposal in proposals { - let Some(dao) = daos.iter().find(|x| x.bulla() == proposal.dao_bulla) else { - return Err(anyhow!("[put_dao_proposals] Couldn't find respective DAO")) - }; - - let query = format!( - "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);", - DAO_PROPOSALS_TABLE, - DAO_PROPOSALS_COL_DAO_ID, - DAO_PROPOSALS_COL_RECV_PUBLIC, - DAO_PROPOSALS_COL_AMOUNT, - DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, - DAO_PROPOSALS_COL_BULLA_BLIND, - DAO_PROPOSALS_COL_LEAF_POSITION, - DAO_PROPOSALS_COL_MONEY_SNAPSHOT_TREE, - DAO_PROPOSALS_COL_TX_HASH, - DAO_PROPOSALS_COL_CALL_INDEX, - ); - - let params = json!([ - query, - QueryType::Integer as u8, - dao.id, - QueryType::Blob as u8, - serialize(&proposal.recipient), - QueryType::Blob as u8, - serialize(&proposal.amount), - QueryType::Blob as u8, - serialize(&proposal.token_id), - QueryType::Blob as u8, - serialize(&proposal.bulla_blind), - QueryType::Blob as u8, - serialize(&proposal.leaf_position.unwrap()), - QueryType::Blob as u8, - serialize(&proposal.money_snapshot_tree.clone().unwrap()), - QueryType::Blob as u8, - serialize(&proposal.tx_hash.unwrap()), - QueryType::Integer as u8, - proposal.call_index, - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - - Ok(()) - } - - /// Import given DAO votes into the wallet - pub async fn put_dao_votes(&self, votes: &[DaoVote]) -> Result<()> { - for vote in votes { - eprintln!("Importing DAO vote into wallet"); - - let query = format!( - "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);", - DAO_VOTES_TABLE, - DAO_VOTES_COL_PROPOSAL_ID, - DAO_VOTES_COL_VOTE_OPTION, - DAO_VOTES_COL_YES_VOTE_BLIND, - DAO_VOTES_COL_ALL_VOTE_VALUE, - DAO_VOTES_COL_ALL_VOTE_BLIND, - DAO_VOTES_COL_TX_HASH, - DAO_VOTES_COL_CALL_INDEX, - ); - - let params = json!([ - query, - QueryType::Integer as u8, - vote.proposal_id, - QueryType::Integer as u8, - vote.vote_option as u64, - QueryType::Blob as u8, - serialize(&vote.yes_vote_blind), - QueryType::Blob as u8, - serialize(&vote.all_vote_value), - QueryType::Blob as u8, - serialize(&vote.all_vote_blind), - QueryType::Blob as u8, - serialize(&vote.tx_hash.unwrap()), - QueryType::Integer as u8, - vote.call_index.unwrap(), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - eprintln!("DAO vote added to wallet"); - } - - Ok(()) - } -} diff --git a/bin/drk/src/wallet_money.rs b/bin/drk/src/wallet_money.rs deleted file mode 100644 index a1bd62ca1..000000000 --- a/bin/drk/src/wallet_money.rs +++ /dev/null @@ -1,788 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use std::{collections::HashMap, str::FromStr}; - -use anyhow::{anyhow, Result}; -use darkfi::{rpc::jsonrpc::JsonRequest, tx::Transaction, wallet::walletdb::QueryType}; -use darkfi_money_contract::{ - client::{ - MoneyNote, OwnCoin, MONEY_ALIASES_COL_ALIAS, MONEY_ALIASES_COL_TOKEN_ID, - MONEY_ALIASES_TABLE, MONEY_COINS_COL_COIN, MONEY_COINS_COL_IS_SPENT, - MONEY_COINS_COL_LEAF_POSITION, MONEY_COINS_COL_MEMO, MONEY_COINS_COL_NULLIFIER, - MONEY_COINS_COL_SECRET, MONEY_COINS_COL_SERIAL, MONEY_COINS_COL_SPEND_HOOK, - MONEY_COINS_COL_TOKEN_BLIND, MONEY_COINS_COL_TOKEN_ID, MONEY_COINS_COL_USER_DATA, - MONEY_COINS_COL_VALUE, MONEY_COINS_COL_VALUE_BLIND, MONEY_COINS_TABLE, - MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE, MONEY_KEYS_COL_IS_DEFAULT, - MONEY_KEYS_COL_KEY_ID, MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE, - MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_TOKEN_ID, MONEY_TOKENS_TABLE, - MONEY_TREE_COL_TREE, MONEY_TREE_TABLE, - }, - model::{ - Coin, MoneyTokenFreezeParamsV1, MoneyTokenMintParamsV1, MoneyTransferParamsV1, Output, - }, - MoneyFunction, -}; -use darkfi_sdk::{ - bridgetree, - crypto::{ - pasta_prelude::Field, poseidon_hash, Keypair, MerkleNode, MerkleTree, Nullifier, PublicKey, - SecretKey, TokenId, MONEY_CONTRACT_ID, - }, - pasta::pallas, -}; -use darkfi_serial::{deserialize, serialize}; -use rand::rngs::OsRng; -use serde_json::json; - -use super::Drk; -use crate::cli_util::kaching; - -impl Drk { - /// Initialize wallet with tables for the Money contract - pub async fn initialize_money(&self) -> Result<()> { - let wallet_schema = include_str!("../../../src/contract/money/wallet.sql"); - - // We perform a request to darkfid with the schema to initialize - // the necessary tables in the wallet. - let req = JsonRequest::new("wallet.exec_sql", json!([wallet_schema])); - let rep = self.rpc_client.request(req).await?; - - if rep == true { - eprintln!("Successfully initialized wallet schema for the Money contract"); - } else { - eprintln!("[initialize_money] Got unexpected reply from darkfid: {}", rep); - } - - // Check if we have to initialize the Merkle tree. - // We check if we find a row in the tree table, and if not, we create a - // new tree and push it into the table. - let mut tree_needs_init = false; - let query = format!("SELECT {} FROM {}", MONEY_TREE_COL_TREE, MONEY_TREE_TABLE); - let params = json!([query, QueryType::Blob as u8, MONEY_TREE_COL_TREE]); - let req = JsonRequest::new("wallet.query_row_single", params); - - // For now, on success, we don't care what's returned, but in the future - // we should actually check it. - // TODO: The RPC needs a better variant for errors so detailed inspection - // can be done with error codes and all that. - if (self.rpc_client.request(req).await).is_err() { - tree_needs_init = true; - } - - if tree_needs_init { - eprintln!("Initializing Money Merkle tree"); - let mut tree = MerkleTree::new(100); - tree.append(MerkleNode::from(pallas::Base::ZERO)); - let _ = tree.mark().unwrap(); - self.put_money_tree(&tree).await?; - eprintln!("Successfully initialized Merkle tree for the Money contract"); - } - - // We maintain the last scanned slot as part of the Money contract, - // but at this moment it is also somewhat applicable to DAO scans. - if (self.last_scanned_slot().await).is_err() { - let query = format!( - "INSERT INTO {} ({}) VALUES (?1);", - MONEY_INFO_TABLE, MONEY_INFO_COL_LAST_SCANNED_SLOT - ); - - let params = json!([query, QueryType::Integer as u8, 0]); - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - - Ok(()) - } - - /// Generate a new keypair and place it into the wallet. - pub async fn money_keygen(&self) -> Result<()> { - eprintln!("Generating a new keypair"); - // TODO: We might want to have hierarchical deterministic key derivation. - let keypair = Keypair::random(&mut OsRng); - let is_default = 0; - - let query = format!( - "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);", - MONEY_KEYS_TABLE, - MONEY_KEYS_COL_IS_DEFAULT, - MONEY_KEYS_COL_PUBLIC, - MONEY_KEYS_COL_SECRET, - ); - - let params = json!([ - query, - QueryType::Integer as u8, - is_default, - QueryType::Blob as u8, - serialize(&keypair.public), - QueryType::Blob as u8, - serialize(&keypair.secret), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let rep = self.rpc_client.request(req).await?; - - if rep == true { - eprintln!("Successfully added new keypair to wallet"); - } else { - eprintln!("[money_keygen] Got unexpected reply from darkfid: {}", rep); - } - - eprintln!("New address:"); - println!("{}", keypair.public); - - Ok(()) - } - - /// Fetch all secret keys from the wallet - pub async fn get_money_secrets(&self) -> Result> { - let query = format!("SELECT {} FROM {};", MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE); - let params = json!([query, QueryType::Blob as u8, MONEY_KEYS_COL_SECRET]); - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - // The returned thing should be an array of found rows. - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[get_money_secrets] Unexpected response from darkfid: {}", rep)) - }; - - let mut secrets = Vec::with_capacity(rows.len()); - - // Let's scan through the rows and see if we got anything. - for row in rows { - let secret_bytes: Vec = serde_json::from_value(row[0].clone())?; - let secret = deserialize(&secret_bytes)?; - secrets.push(secret); - } - - Ok(secrets) - } - - /// Import given secret keys into the wallet. - /// The query uses INSERT, so if the key already exists, it will be skipped. - /// Returns the respective PublicKey objects for the imported keys. - pub async fn import_money_secrets(&self, secrets: Vec) -> Result> { - let mut ret = Vec::with_capacity(secrets.len()); - - for secret in secrets { - ret.push(PublicKey::from_secret(secret)); - let is_default = 0; - let public = serialize(&PublicKey::from_secret(secret)); - let secret = serialize(&secret); - - let query = format!( - "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);", - MONEY_KEYS_TABLE, - MONEY_KEYS_COL_IS_DEFAULT, - MONEY_KEYS_COL_PUBLIC, - MONEY_KEYS_COL_SECRET, - ); - - let params = json!([ - query, - QueryType::Integer as u8, - is_default, - QueryType::Blob as u8, - public, - QueryType::Blob as u8, - secret, - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - - Ok(ret) - } - - /// Fetch pubkeys from the wallet and return the requested index. - pub async fn wallet_address(&self, idx: u64) -> Result { - let query = format!( - "SELECT {} FROM {} WHERE {} = {};", - MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_TABLE, MONEY_KEYS_COL_KEY_ID, idx - ); - - let params = json!([query, QueryType::Blob as u8, MONEY_KEYS_COL_PUBLIC]); - let req = JsonRequest::new("wallet.query_row_single", params); - let rep = self.rpc_client.request(req).await?; - - let Some(arr) = rep.as_array() else { - return Err(anyhow!("[wallet_address] Unexpected response from darkfid: {}", rep)) - }; - - if arr.len() != 1 { - return Err(anyhow!("Did not find pubkey with index {}", idx)) - } - - let key_bytes: Vec = serde_json::from_value(arr[0].clone())?; - let public_key: PublicKey = deserialize(&key_bytes)?; - - Ok(public_key) - } - - /// Fetch all coins and their metadata related to the Money contract from the wallet. - /// Optionally also fetch spent ones. - /// The boolean in the returned tuple notes if the coin was marked as spent. - pub async fn get_coins(&self, fetch_spent: bool) -> Result> { - let query = if fetch_spent { - format!("SELECT * FROM {}", MONEY_COINS_TABLE) - } else { - format!( - "SELECT * FROM {} WHERE {} = {}", - MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, false, - ) - }; - - let params = json!([ - query, - QueryType::Blob as u8, - MONEY_COINS_COL_COIN, - QueryType::Integer as u8, - MONEY_COINS_COL_IS_SPENT, - QueryType::Blob as u8, - MONEY_COINS_COL_SERIAL, - QueryType::Blob as u8, - MONEY_COINS_COL_VALUE, - QueryType::Blob as u8, - MONEY_COINS_COL_TOKEN_ID, - QueryType::Blob as u8, - MONEY_COINS_COL_SPEND_HOOK, - QueryType::Blob as u8, - MONEY_COINS_COL_USER_DATA, - QueryType::Blob as u8, - MONEY_COINS_COL_VALUE_BLIND, - QueryType::Blob as u8, - MONEY_COINS_COL_TOKEN_BLIND, - QueryType::Blob as u8, - MONEY_COINS_COL_SECRET, - QueryType::Blob as u8, - MONEY_COINS_COL_NULLIFIER, - QueryType::Blob as u8, - MONEY_COINS_COL_LEAF_POSITION, - QueryType::Blob as u8, - MONEY_COINS_COL_MEMO, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - // The returned thing should be an array of found rows. - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[get_coins] Unexpected response from darkfid: {}", rep)) - }; - - let mut owncoins = Vec::with_capacity(rows.len()); - - for row in rows { - let Some(row) = row.as_array() else { - return Err(anyhow!("[get_coins] Unexpected response from darkfid: {}", rep)) - }; - - let coin_bytes: Vec = serde_json::from_value(row[0].clone())?; - let coin: Coin = deserialize(&coin_bytes)?; - - let is_spent: u64 = serde_json::from_value(row[1].clone())?; - let is_spent = is_spent > 0; - - let serial_bytes: Vec = serde_json::from_value(row[2].clone())?; - let serial: pallas::Base = deserialize(&serial_bytes)?; - - let value_bytes: Vec = serde_json::from_value(row[3].clone())?; - let value: u64 = deserialize(&value_bytes)?; - - let token_id_bytes: Vec = serde_json::from_value(row[4].clone())?; - let token_id: TokenId = deserialize(&token_id_bytes)?; - - let spend_hook_bytes: Vec = serde_json::from_value(row[5].clone())?; - let spend_hook: pallas::Base = deserialize(&spend_hook_bytes)?; - - let user_data_bytes: Vec = serde_json::from_value(row[6].clone())?; - let user_data: pallas::Base = deserialize(&user_data_bytes)?; - - let value_blind_bytes: Vec = serde_json::from_value(row[7].clone())?; - let value_blind: pallas::Scalar = deserialize(&value_blind_bytes)?; - - let token_blind_bytes: Vec = serde_json::from_value(row[8].clone())?; - let token_blind: pallas::Base = deserialize(&token_blind_bytes)?; - - let secret_bytes: Vec = serde_json::from_value(row[9].clone())?; - let secret: SecretKey = deserialize(&secret_bytes)?; - - let nullifier_bytes: Vec = serde_json::from_value(row[10].clone())?; - let nullifier: Nullifier = deserialize(&nullifier_bytes)?; - - let leaf_position_bytes: Vec = serde_json::from_value(row[11].clone())?; - let leaf_position: bridgetree::Position = deserialize(&leaf_position_bytes)?; - - let memo: Vec = serde_json::from_value(row[12].clone())?; - - let note = MoneyNote { - serial, - value, - token_id, - spend_hook, - user_data, - value_blind, - token_blind, - memo, - }; - let owncoin = OwnCoin { coin, note, secret, nullifier, leaf_position }; - - owncoins.push((owncoin, is_spent)) - } - - Ok(owncoins) - } - - /// Mark a coin in the wallet as spent - pub async fn mark_spent_coin(&self, coin: &Coin) -> Result<()> { - let query = format!( - "UPDATE {} SET {} = ?1 WHERE {} = ?2;", - MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_COIN - ); - - let params = json!([ - query, - QueryType::Integer as u8, - 1, - QueryType::Blob as u8, - serialize(&coin.inner()) - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Marks all coins in the wallet as spent, if their nullifier is in the given set - pub async fn mark_spent_coins(&self, nullifiers: &[Nullifier]) -> Result<()> { - if nullifiers.is_empty() { - return Ok(()) - } - - for (coin, _) in self.get_coins(false).await? { - if nullifiers.contains(&coin.nullifier) { - self.mark_spent_coin(&coin.coin).await?; - } - } - - Ok(()) - } - - /// Mark a given coin in the wallet as unspent - pub async fn unspend_coin(&self, coin: &Coin) -> Result<()> { - let query = format!( - "UPDATE {} SET {} = ?1 WHERE {} = ?2;", - MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_COIN, - ); - - let params = json!([ - query, - QueryType::Integer as u8, - 0, - QueryType::Blob as u8, - serialize(&coin.inner()) - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Replace the Money Merkle tree in the wallet. - pub async fn put_money_tree(&self, tree: &MerkleTree) -> Result<()> { - let query = format!( - "DELETE FROM {}; INSERT INTO {} ({}) VALUES (?1);", - MONEY_TREE_TABLE, MONEY_TREE_TABLE, MONEY_TREE_COL_TREE, - ); - - let params = json!([query, QueryType::Blob as u8, serialize(tree)]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Fetch the Money Merkle tree from the wallet - pub async fn get_money_tree(&self) -> Result { - let query = format!("SELECT * FROM {}", MONEY_TREE_TABLE); - let params = json!([query, QueryType::Blob as u8, MONEY_TREE_COL_TREE]); - let req = JsonRequest::new("wallet.query_row_single", params); - let rep = self.rpc_client.request(req).await?; - - let tree_bytes: Vec = serde_json::from_value(rep[0].clone())?; - let tree = deserialize(&tree_bytes)?; - Ok(tree) - } - - /// Reset the Money Merkle tree in the wallet - pub async fn reset_money_tree(&self) -> Result<()> { - eprintln!("Resetting Money Merkle tree"); - let mut tree = MerkleTree::new(100); - tree.append(MerkleNode::from(pallas::Base::ZERO)); - let _ = tree.mark().unwrap(); - self.put_money_tree(&tree).await?; - eprintln!("Successfully reset Money Merkle tree"); - - Ok(()) - } - - /// Reset the Money coins in the wallet - pub async fn reset_money_coins(&self) -> Result<()> { - eprintln!("Resetting coins"); - let query = format!("DELETE FROM {};", MONEY_COINS_TABLE); - let params = json!([query]); - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - eprintln!("Successfully reset coins"); - - Ok(()) - } - - /// Fetch known unspent balances from the wallet and return them as a hashmap. - pub async fn money_balance(&self) -> Result> { - let mut coins = self.get_coins(false).await?; - coins.retain(|x| x.0.note.spend_hook == pallas::Base::zero()); - - // Fill this map with balances - let mut balmap: HashMap = HashMap::new(); - - for coin in coins { - let mut value = coin.0.note.value; - - if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) { - value += prev; - } - - balmap.insert(coin.0.note.token_id.to_string(), value); - } - - Ok(balmap) - } - - /// Append data related to Money contract transactions into the wallet database. - pub async fn apply_tx_money_data(&self, tx: &Transaction, _confirm: bool) -> Result<()> { - let cid = *MONEY_CONTRACT_ID; - - let mut nullifiers: Vec = vec![]; - let mut outputs: Vec = vec![]; - let mut freezes: Vec = vec![]; - - for (i, call) in tx.calls.iter().enumerate() { - if call.contract_id == cid && call.data[0] == MoneyFunction::TransferV1 as u8 { - eprintln!("Found Money::TransferV1 in call {}", i); - let params: MoneyTransferParamsV1 = deserialize(&call.data[1..])?; - - for input in params.inputs { - nullifiers.push(input.nullifier); - } - - for output in params.outputs { - outputs.push(output); - } - - continue - } - - if call.contract_id == cid && call.data[0] == MoneyFunction::OtcSwapV1 as u8 { - eprintln!("Found Money::OtcSwapV1 in call {}", i); - let params: MoneyTransferParamsV1 = deserialize(&call.data[1..])?; - - for input in params.inputs { - nullifiers.push(input.nullifier); - } - - for output in params.outputs { - outputs.push(output); - } - - continue - } - - if call.contract_id == cid && call.data[0] == MoneyFunction::TokenMintV1 as u8 { - eprintln!("Found Money::MintV1 in call {}", i); - let params: MoneyTokenMintParamsV1 = deserialize(&call.data[1..])?; - outputs.push(params.output); - continue - } - - if call.contract_id == cid && call.data[0] == MoneyFunction::TokenFreezeV1 as u8 { - eprintln!("Found Money::FreezeV1 in call {}", i); - let params: MoneyTokenFreezeParamsV1 = deserialize(&call.data[1..])?; - let token_id = TokenId::derive_public(params.signature_public); - freezes.push(token_id); - } - } - - let secrets = self.get_money_secrets().await?; - let dao_secrets = self.get_dao_secrets().await?; - let mut tree = self.get_money_tree().await?; - - let mut owncoins = vec![]; - - for output in outputs { - let coin = output.coin; - - // Append the new coin to the Merkle tree. Every coin has to be added. - tree.append(MerkleNode::from(coin.inner())); - - // Attempt to decrypt the note - for secret in secrets.iter().chain(dao_secrets.iter()) { - if let Ok(note) = output.note.decrypt::(secret) { - eprintln!("Successfully decrypted a Money Note"); - eprintln!("Witnessing coin in Merkle tree"); - let leaf_position = tree.mark().unwrap(); - - let owncoin = OwnCoin { - coin, - note: note.clone(), - secret: *secret, - nullifier: Nullifier::from(poseidon_hash([secret.inner(), note.serial])), - leaf_position, - }; - - owncoins.push(owncoin); - } - } - } - - self.put_money_tree(&tree).await?; - if !nullifiers.is_empty() { - self.mark_spent_coins(&nullifiers).await?; - } - - // This is the SQL query we'll be executing to insert new coins - // into the wallet - let query = format!( - "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13);", - MONEY_COINS_TABLE, - MONEY_COINS_COL_COIN, - MONEY_COINS_COL_IS_SPENT, - MONEY_COINS_COL_SERIAL, - MONEY_COINS_COL_VALUE, - MONEY_COINS_COL_TOKEN_ID, - MONEY_COINS_COL_SPEND_HOOK, - MONEY_COINS_COL_USER_DATA, - MONEY_COINS_COL_VALUE_BLIND, - MONEY_COINS_COL_TOKEN_BLIND, - MONEY_COINS_COL_SECRET, - MONEY_COINS_COL_NULLIFIER, - MONEY_COINS_COL_LEAF_POSITION, - MONEY_COINS_COL_MEMO, - ); - - eprintln!("Found {} OwnCoin(s) in transaction", owncoins.len()); - for owncoin in &owncoins { - eprintln!("OwnCoin: {:?}", owncoin.coin); - let params = json!([ - query, - QueryType::Blob as u8, - serialize(&owncoin.coin), - QueryType::Integer as u8, - 0, // <-- is_spent - QueryType::Blob as u8, - serialize(&owncoin.note.serial), - QueryType::Blob as u8, - serialize(&owncoin.note.value), - QueryType::Blob as u8, - serialize(&owncoin.note.token_id), - QueryType::Blob as u8, - serialize(&owncoin.note.spend_hook), - QueryType::Blob as u8, - serialize(&owncoin.note.user_data), - QueryType::Blob as u8, - serialize(&owncoin.note.value_blind), - QueryType::Blob as u8, - serialize(&owncoin.note.token_blind), - QueryType::Blob as u8, - serialize(&owncoin.secret), - QueryType::Blob as u8, - serialize(&owncoin.nullifier), - QueryType::Blob as u8, - serialize(&owncoin.leaf_position), - QueryType::Blob as u8, - serialize(&owncoin.note.memo), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - - for token_id in freezes { - let query = format!( - "UPDATE {} SET {} = 1 WHERE {} = ?1;", - MONEY_TOKENS_TABLE, MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_TOKEN_ID, - ); - - let params = json!([query, QueryType::Blob as u8, serialize(&token_id)]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - } - - if !owncoins.is_empty() { - kaching().await; - } - - Ok(()) - } - - /// Get the last scanned slot from the wallet - pub async fn last_scanned_slot(&self) -> Result { - let query = - format!("SELECT {} FROM {};", MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE); - - let params = json!([query, QueryType::Integer as u8, MONEY_INFO_COL_LAST_SCANNED_SLOT]); - let req = JsonRequest::new("wallet.query_row_single", params); - let rep = self.rpc_client.request(req).await?; - - Ok(serde_json::from_value(rep[0].clone())?) - } - - /// Create an alias record for provided Token ID - pub async fn add_alias(&self, alias: String, token_id: TokenId) -> Result<()> { - eprintln!("Generating alias {} for Token: {}", alias, token_id); - let query = format!( - "INSERT OR REPLACE INTO {} ({}, {}) VALUES (?1, ?2);", - MONEY_ALIASES_TABLE, MONEY_ALIASES_COL_ALIAS, MONEY_ALIASES_COL_TOKEN_ID, - ); - - let params = json!([ - query, - QueryType::Blob as u8, - serialize(&alias), - QueryType::Blob as u8, - serialize(&token_id), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let rep = self.rpc_client.request(req).await?; - - if rep == true { - eprintln!("Successfully added new alias to wallet"); - } else { - eprintln!("[add_alias] Got unexpected reply from darkfid: {}", rep); - } - - Ok(()) - } - - /// Fetch all aliases from the wallet. - /// Optionally filter using alias name and/or token id. - pub async fn get_aliases( - &self, - alias_filter: Option, - token_id_filter: Option, - ) -> Result> { - let query = format!("SELECT * FROM {}", MONEY_ALIASES_TABLE); - let params = json!([ - query, - QueryType::Blob as u8, - MONEY_ALIASES_COL_ALIAS, - QueryType::Blob as u8, - MONEY_ALIASES_COL_TOKEN_ID, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - // The returned thing should be an array of found rows. - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[get_aliases] Unexpected response from darkfid: {}", rep)) - }; - - // Fill this map with aliases - let mut map: HashMap = HashMap::new(); - for row in rows { - let Some(row) = row.as_array() else { - return Err(anyhow!("[get_aliases] Unexpected response from darkfid: {}", rep)) - }; - - let alias_bytes: Vec = serde_json::from_value(row[0].clone())?; - let alias: String = deserialize(&alias_bytes)?; - if alias_filter.is_some() && alias_filter.as_ref().unwrap() != &alias { - continue - } - - let token_id_bytes: Vec = serde_json::from_value(row[1].clone())?; - let token_id: TokenId = deserialize(&token_id_bytes)?; - if token_id_filter.is_some() && token_id_filter.as_ref().unwrap() != &token_id { - continue - } - - map.insert(alias, token_id); - } - - Ok(map) - } - - /// Fetch all aliases from the wallet, mapped by token id. - pub async fn get_aliases_mapped_by_token(&self) -> Result> { - let aliases = self.get_aliases(None, None).await?; - let mut map: HashMap = HashMap::new(); - for (alias, token_id) in aliases { - let aliases_string = if let Some(prev) = map.get(&token_id.to_string()) { - format!("{}, {}", prev, alias) - } else { - alias - }; - - map.insert(token_id.to_string(), aliases_string); - } - - Ok(map) - } - - /// Retrieve token by provided string. - /// Input string represents either an alias or a token id. - pub async fn get_token(&self, input: String) -> Result { - // Check if input is an alias(max 5 characters) - if input.chars().count() <= 5 { - let aliases = self.get_aliases(Some(input.clone()), None).await?; - if let Some(token_id) = aliases.get(&input) { - return Ok(*token_id) - } - } - // Else parse input - Ok(TokenId::from_str(input.as_str())?) - } - - /// Create an alias record for provided Token ID - pub async fn remove_alias(&self, alias: String) -> Result<()> { - eprintln!("Removing alias: {}", alias); - let query = - format!("DELETE FROM {} WHERE {} = ?1;", MONEY_ALIASES_TABLE, MONEY_ALIASES_COL_ALIAS,); - - let params = json!([query, QueryType::Blob as u8, serialize(&alias),]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let rep = self.rpc_client.request(req).await?; - - if rep == true { - eprintln!("Successfully removed alias from wallet"); - } else { - eprintln!("[remove_alias] Got unexpected reply from darkfid: {}", rep); - } - - Ok(()) - } -} diff --git a/bin/drk/src/wallet_token.rs b/bin/drk/src/wallet_token.rs deleted file mode 100644 index e06ccb7ee..000000000 --- a/bin/drk/src/wallet_token.rs +++ /dev/null @@ -1,97 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use anyhow::{anyhow, Result}; -use darkfi::{rpc::jsonrpc::JsonRequest, wallet::walletdb::QueryType}; -use darkfi_money_contract::client::{ - MONEY_TOKENS_COL_IS_FROZEN, MONEY_TOKENS_COL_MINT_AUTHORITY, MONEY_TOKENS_COL_TOKEN_ID, - MONEY_TOKENS_TABLE, -}; -use darkfi_sdk::crypto::{SecretKey, TokenId}; -use darkfi_serial::{deserialize, serialize}; -use serde_json::json; - -use super::Drk; - -impl Drk { - /// Import a token mint authority into the wallet - pub async fn import_mint_authority(&self, mint_authority: SecretKey) -> Result<()> { - let token_id = TokenId::derive(mint_authority); - let is_frozen = 0; - - let query = format!( - "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);", - MONEY_TOKENS_TABLE, - MONEY_TOKENS_COL_MINT_AUTHORITY, - MONEY_TOKENS_COL_TOKEN_ID, - MONEY_TOKENS_COL_IS_FROZEN, - ); - - let params = json!([ - query, - QueryType::Blob as u8, - serialize(&mint_authority), - QueryType::Blob as u8, - serialize(&token_id), - QueryType::Integer as u8, - is_frozen, - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - pub async fn list_tokens(&self) -> Result> { - let mut ret = vec![]; - - let query = format!("SELECT * FROM {};", MONEY_TOKENS_TABLE); - - let params = json!([ - query, - QueryType::Blob as u8, - MONEY_TOKENS_COL_MINT_AUTHORITY, - QueryType::Blob as u8, - MONEY_TOKENS_COL_TOKEN_ID, - QueryType::Integer as u8, - MONEY_TOKENS_COL_IS_FROZEN, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[list_tokens] Unexpected response from darkfid: {}", rep)) - }; - - for row in rows { - let auth_bytes: Vec = serde_json::from_value(row[0].clone())?; - let mint_authority = deserialize(&auth_bytes)?; - - let token_bytes: Vec = serde_json::from_value(row[1].clone())?; - let token_id = deserialize(&token_bytes)?; - - let frozen: i32 = serde_json::from_value(row[2].clone())?; - - ret.push((token_id, mint_authority, frozen != 0)); - } - - Ok(ret) - } -} diff --git a/bin/drk/src/wallet_txs_history.rs b/bin/drk/src/wallet_txs_history.rs deleted file mode 100644 index f8ce636c9..000000000 --- a/bin/drk/src/wallet_txs_history.rs +++ /dev/null @@ -1,192 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use anyhow::{anyhow, Result}; -use darkfi::{rpc::jsonrpc::JsonRequest, tx::Transaction, wallet::walletdb::QueryType}; -use darkfi_serial::{deserialize, serialize}; -use serde_json::json; - -use super::Drk; - -// Wallet SQL table constant names. These have to represent the `wallet.sql` -// SQL schema. -const WALLET_TXS_HISTORY_TABLE: &str = "transactions_history"; -const WALLET_TXS_HISTORY_COL_TX_HASH: &str = "transaction_hash"; -const WALLET_TXS_HISTORY_COL_STATUS: &str = "status"; -const WALLET_TXS_HISTORY_COL_TX: &str = "tx"; - -impl Drk { - /// Fetch all transactions history records, excluding bytes column. - pub async fn get_txs_history(&self) -> Result> { - let mut ret = vec![]; - - let query = format!( - "SELECT {}, {} FROM {};", - WALLET_TXS_HISTORY_COL_TX_HASH, WALLET_TXS_HISTORY_COL_STATUS, WALLET_TXS_HISTORY_TABLE - ); - - let params = json!([ - query, - QueryType::Text as u8, - WALLET_TXS_HISTORY_COL_TX_HASH, - QueryType::Text as u8, - WALLET_TXS_HISTORY_COL_STATUS, - ]); - - let req = JsonRequest::new("wallet.query_row_multi", params); - let rep = self.rpc_client.request(req).await?; - - let Some(rows) = rep.as_array() else { - return Err(anyhow!("[txs_history] Unexpected response from darkfid: {}", rep)) - }; - - for row in rows { - let tx_hash: String = serde_json::from_value(row[0].clone())?; - let status: String = serde_json::from_value(row[1].clone())?; - ret.push((tx_hash, status)); - } - - Ok(ret) - } - - /// Get a transaction history record. - pub async fn get_tx_history_record( - &self, - tx_hash: &str, - ) -> Result<(String, String, Transaction)> { - let query = format!( - "SELECT * FROM {} WHERE {} = {};", - WALLET_TXS_HISTORY_TABLE, WALLET_TXS_HISTORY_COL_TX_HASH, tx_hash - ); - - let params = json!([ - query, - QueryType::Text as u8, - WALLET_TXS_HISTORY_COL_TX_HASH, - QueryType::Text as u8, - WALLET_TXS_HISTORY_COL_STATUS, - QueryType::Text as u8, - WALLET_TXS_HISTORY_COL_TX, - ]); - - let req = JsonRequest::new("wallet.query_row_single", params); - let rep = self.rpc_client.request(req).await?; - - let Some(arr) = rep.as_array() else { - return Err(anyhow!("[get_tx_history_record] Unexpected response from darkfid: {}", rep)) - }; - - if arr.len() != 3 { - return Err(anyhow!("Did not find transaction record with hash {}", tx_hash)) - } - - let tx_hash: String = serde_json::from_value(arr[0].clone())?; - - let status: String = serde_json::from_value(arr[1].clone())?; - - let tx_encoded: String = serde_json::from_value(arr[2].clone())?; - let tx_bytes: Vec = bs58::decode(&tx_encoded).into_vec()?; - let tx: Transaction = deserialize(&tx_bytes)?; - - Ok((tx_hash, status, tx)) - } - - /// Insert a [`Transaction`] history record into the wallet. - pub async fn insert_tx_history_record(&self, tx: &Transaction) -> Result<()> { - let query = format!( - "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);", - WALLET_TXS_HISTORY_TABLE, - WALLET_TXS_HISTORY_COL_TX_HASH, - WALLET_TXS_HISTORY_COL_STATUS, - WALLET_TXS_HISTORY_COL_TX, - ); - - let params = json!([ - query, - QueryType::Text as u8, - tx.hash().to_string(), - QueryType::Text as u8, - "Broadcasted", - QueryType::Text as u8, - bs58::encode(&serialize(tx)).into_string(), - ]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Update a transactions history record status to the given one. - pub async fn update_tx_history_record_status(&self, tx_hash: &str, status: &str) -> Result<()> { - let query = format!( - "UPDATE {} SET {} = ?1 WHERE {} = ?2;", - WALLET_TXS_HISTORY_TABLE, WALLET_TXS_HISTORY_COL_STATUS, WALLET_TXS_HISTORY_COL_TX_HASH, - ); - - let params = json!([query, QueryType::Text as u8, status, QueryType::Text as u8, tx_hash,]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Update given transactions history record statuses to the given one. - pub async fn update_tx_history_records_status( - &self, - txs: &Vec, - status: &str, - ) -> Result<()> { - if txs.is_empty() { - return Ok(()) - } - - let txs_hashes: Vec = txs.iter().map(|tx| tx.hash().to_string()).collect(); - let txs_hashes_string = format!("{:?}", txs_hashes).replace('[', "(").replace(']', ")"); - let query = format!( - "UPDATE {} SET {} = ?1 WHERE {} IN {};", - WALLET_TXS_HISTORY_TABLE, - WALLET_TXS_HISTORY_COL_STATUS, - WALLET_TXS_HISTORY_COL_TX_HASH, - txs_hashes_string - ); - - let params = json!([query, QueryType::Text as u8, status,]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } - - /// Update all transaction history records statuses to the given one. - pub async fn update_all_tx_history_records_status(&self, status: &str) -> Result<()> { - let query = format!( - "UPDATE {} SET {} = ?1", - WALLET_TXS_HISTORY_TABLE, WALLET_TXS_HISTORY_COL_STATUS, - ); - - let params = json!([query, QueryType::Text as u8, status,]); - - let req = JsonRequest::new("wallet.exec_sql", params); - let _ = self.rpc_client.request(req).await?; - - Ok(()) - } -} diff --git a/bin/drk2/src/walletdb.rs b/bin/drk/src/walletdb.rs similarity index 100% rename from bin/drk2/src/walletdb.rs rename to bin/drk/src/walletdb.rs diff --git a/bin/drk2/Cargo.toml b/bin/drk2/Cargo.toml deleted file mode 100644 index 01cb19870..000000000 --- a/bin/drk2/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "drk2" -version = "0.4.1" -homepage = "https://dark.fi" -description = "Command-line client for darkfid" -authors = ["Dyne.org foundation "] -repository = "https://github.com/darkrenaissance/darkfi" -license = "AGPL-3.0-only" -edition = "2021" - -[dependencies] -# Darkfi -darkfi = {path = "../../", features = ["async-daemonize", "bs58", "rpc"]} -darkfi_money_contract = {path = "../../src/contract/money", features = ["no-entrypoint", "client"]} -darkfi_dao_contract = {path = "../../src/contract/dao", features = ["no-entrypoint", "client"]} -darkfi-sdk = {path = "../../src/sdk", features = ["async"]} -darkfi-serial = {path = "../../src/serial"} - -# Misc -blake3 = "1.5.0" -bs58 = "0.5.0" -log = "0.4.20" -prettytable-rs = "0.10.0" -rand = "0.8.5" -rodio = {version = "0.17.3", default-features = false, features = ["minimp3"]} -rusqlite = {version = "0.30.0", features = ["sqlcipher"]} -url = "2.5.0" - -# Daemon -easy-parallel = "3.3.1" -signal-hook-async-std = "0.2.2" -signal-hook = "0.3.17" -simplelog = "0.12.1" -smol = "1.3.0" - -# Argument parsing -serde = {version = "1.0.195", features = ["derive"]} -structopt = "0.3.26" -structopt-toml = "0.5.1" diff --git a/bin/drk2/src/cli_util.rs b/bin/drk2/src/cli_util.rs deleted file mode 100644 index aaf2fcc2e..000000000 --- a/bin/drk2/src/cli_util.rs +++ /dev/null @@ -1,469 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -use std::{io::Cursor, process::exit, str::FromStr}; - -use rodio::{source::Source, Decoder, OutputStream}; -use structopt_toml::clap::{App, Arg, Shell, SubCommand}; - -use darkfi::{cli_desc, system::sleep, util::parse::decode_base10, Error, Result}; -use darkfi_sdk::crypto::TokenId; - -use crate::{money::BALANCE_BASE10_DECIMALS, Drk}; - -/// Auxiliary function to parse provided string into a values pair. -pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> { - let v: Vec<&str> = s.split(':').collect(); - if v.len() != 2 { - eprintln!("Invalid value pair. Use a pair such as 13.37:11.0"); - exit(1); - } - - let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true); - let val1 = decode_base10(v[1], BALANCE_BASE10_DECIMALS, true); - - if val0.is_err() || val1.is_err() { - eprintln!("Invalid value pair. Use a pair such as 13.37:11.0"); - exit(1); - } - - Ok((val0.unwrap(), val1.unwrap())) -} - -/// Auxiliary function to parse provided string into a tokens pair. -pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)> { - let v: Vec<&str> = s.split(':').collect(); - if v.len() != 2 { - eprintln!("Invalid token pair. Use a pair such as:"); - eprintln!("WCKD:MLDY"); - eprintln!("or"); - eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"); - exit(1); - } - - let tok0 = drk.get_token(v[0].to_string()).await; - let tok1 = drk.get_token(v[1].to_string()).await; - - if tok0.is_err() || tok1.is_err() { - eprintln!("Invalid token pair. Use a pair such as:"); - eprintln!("WCKD:MLDY"); - eprintln!("or"); - eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2"); - exit(1); - } - - Ok((tok0.unwrap(), tok1.unwrap())) -} - -/// Fun police go away -pub async fn kaching() { - const WALLET_MP3: &[u8] = include_bytes!("../wallet.mp3"); - - let cursor = Cursor::new(WALLET_MP3); - - let Ok((_stream, stream_handle)) = OutputStream::try_default() else { return }; - - let Ok(source) = Decoder::new(cursor) else { return }; - - if stream_handle.play_raw(source.convert_samples()).is_err() { - return - } - - sleep(2).await; -} - -/// Auxiliary function to generate provided shell completions. -pub fn generate_completions(shell: &str) -> Result<()> { - // Sub-commands - - // Kaching - let kaching = SubCommand::with_name("kaching").about("Fun"); - - // Ping - let ping = - SubCommand::with_name("ping").about("Send a ping request to the darkfid RPC endpoint"); - - // Completions - let shell_arg = Arg::with_name("shell").help("The Shell you want to generate script for"); - - let completions = SubCommand::with_name("completions") - .about("Generate a SHELL completion script and print to stdout") - .arg(shell_arg); - - // Wallet - let initialize = - Arg::with_name("initialize").long("initialize").help("Initialize wallet database"); - - let keygen = - Arg::with_name("keygen").long("keygen").help("Generate a new keypair in the wallet"); - - let balance = - Arg::with_name("balance").long("balance").help("Query the wallet for known balances"); - - let address = - Arg::with_name("address").long("address").help("Get the default address in the wallet"); - - let addresses = - Arg::with_name("addresses").long("addresses").help("Print all the addresses in the wallet"); - - let default_address = Arg::with_name("default-address") - .long("default-address") - .takes_value(true) - .help("Set the default address in the wallet"); - - let secrets = - Arg::with_name("secrets").long("secrets").help("Print all the secret keys from the wallet"); - - let import_secrets = Arg::with_name("import-secrets") - .long("import-secrets") - .help("Import secret keys from stdin into the wallet, separated by newlines"); - - let tree = Arg::with_name("tree").long("tree").help("Print the Merkle tree in the wallet"); - - let coins = Arg::with_name("coins").long("coins").help("Print all the coins in the wallet"); - - let wallet = SubCommand::with_name("wallet").about("Wallet operations").args(&vec![ - initialize, - keygen, - balance, - address, - addresses, - default_address, - secrets, - import_secrets, - tree, - coins, - ]); - - // Unspend - let coin = Arg::with_name("coin").help("base58-encoded coin to mark as unspent"); - - let unspend = SubCommand::with_name("unspend").about("Unspend a coin").arg(coin); - - // Transfer - let amount = Arg::with_name("amount").help("Amount to send"); - - let token = Arg::with_name("token").help("Token ID to send"); - - let recipient = Arg::with_name("recipient").help("Recipient address"); - - let transfer = SubCommand::with_name("transfer") - .about("Create a payment transaction") - .args(&vec![amount, token, recipient]); - - // Otc - let value_pair = Arg::with_name("value-pair") - .short("v") - .long("value-pair") - .takes_value(true) - .help("Value pair to send:recv (11.55:99.42)"); - - let token_pair = Arg::with_name("token-pair") - .short("t") - .long("token-pair") - .takes_value(true) - .help("Token pair to send:recv (f00:b4r)"); - - let init = SubCommand::with_name("init") - .about("Initialize the first half of the atomic swap") - .args(&vec![value_pair, token_pair]); - - let join = - SubCommand::with_name("join").about("Build entire swap tx given the first half from stdin"); - - let inspect = SubCommand::with_name("inspect") - .about("Inspect a swap half or the full swap tx from stdin"); - - let sign = SubCommand::with_name("sign") - .about("Sign a transaction given from stdin as the first-half"); - - let otc = SubCommand::with_name("otc") - .about("OTC atomic swap") - .subcommands(vec![init, join, inspect, sign]); - - // Inspect - let inspect = SubCommand::with_name("inspect").about("Inspect a transaction from stdin"); - - // Broadcast - let broadcast = - SubCommand::with_name("broadcast").about("Read a transaction from stdin and broadcast it"); - - // Subscribe - let subscribe = SubCommand::with_name("subscribe").about( - "This subscription will listen for incoming blocks from darkfid and look \ - through their transactions to see if there's any that interest us. \ - With `drk` we look at transactions calling the money contract so we can \ - find coins sent to us and fill our wallet with the necessary metadata.", - ); - - // DAO - let proposer_limit = Arg::with_name("proposer-limit") - .help("The minimum amount of governance tokens needed to open a proposal for this DAO"); - - let quorum = Arg::with_name("quorum") - .help("Minimal threshold of participating total tokens needed for a proposal to pass"); - - let approval_ratio = Arg::with_name("approval-ratio") - .help("The ratio of winning votes/total votes needed for a proposal to pass (2 decimals)"); - - let gov_token_id = Arg::with_name("gov-token-id").help("DAO's governance token ID"); - - let create = SubCommand::with_name("create").about("Create DAO parameters").args(&vec![ - proposer_limit, - quorum, - approval_ratio, - gov_token_id, - ]); - - let view = SubCommand::with_name("view").about("View DAO data from stdin"); - - let dao_name = Arg::with_name("dao-name").help("Named identifier for the DAO"); - - let import = - SubCommand::with_name("import").about("Import DAO data from stdin").args(&vec![dao_name]); - - let dao_alias = Arg::with_name("dao-alias").help("Numeric identifier for the DAO (optional)"); - - let list = SubCommand::with_name("list") - .about("List imported DAOs (or info about a specific one)") - .args(&vec![dao_alias]); - - let dao_alias = Arg::with_name("dao-alias").help("Name or numeric identifier for the DAO"); - - let balance = SubCommand::with_name("balance") - .about("Show the balance of a DAO") - .args(&vec![dao_alias.clone()]); - - let mint = SubCommand::with_name("mint") - .about("Mint an imported DAO on-chain") - .args(&vec![dao_alias.clone()]); - - let recipient = - Arg::with_name("recipient").help("Pubkey to send tokens to with proposal success"); - - let amount = Arg::with_name("amount").help("Amount to send from DAO with proposal success"); - - let token = Arg::with_name("token").help("Token ID to send from DAO with proposal success"); - - let propose = SubCommand::with_name("propose") - .about("Create a proposal for a DAO") - .args(&vec![dao_alias.clone(), recipient, amount, token]); - - let proposals = SubCommand::with_name("proposals") - .about("List DAO proposals") - .args(&vec![dao_alias.clone()]); - - let proposal_id = Arg::with_name("proposal-id").help("Numeric identifier for the proposal"); - - let proposal = SubCommand::with_name("proposal") - .about("View a DAO proposal data") - .args(&vec![dao_alias.clone(), proposal_id.clone()]); - - let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)"); - - let vote_weight = - Arg::with_name("vote-weight").help("Vote weight (amount of governance tokens)"); - - let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![ - dao_alias.clone(), - proposal_id.clone(), - vote, - vote_weight, - ]); - - let exec = SubCommand::with_name("exec") - .about("Execute a DAO proposal") - .args(&vec![dao_alias, proposal_id]); - - let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![ - create, view, import, list, balance, mint, propose, proposals, proposal, vote, exec, - ]); - - // Scan - let reset = Arg::with_name("reset") - .long("reset") - .help("Reset Merkle tree and start scanning from first block"); - - let list = Arg::with_name("list").long("list").help("List all available checkpoints"); - - let checkpoint = Arg::with_name("checkpoint") - .long("checkpoint") - .takes_value(true) - .help("Reset Merkle tree to checkpoint index and start scanning"); - - let scan = SubCommand::with_name("scan") - .about("Scan the blockchain and parse relevant transactions") - .args(&vec![reset, list, checkpoint]); - - // Explorer - let tx_hash = Arg::with_name("tx-hash").help("Transaction hash"); - - let full = Arg::with_name("full").long("full").help("Print the full transaction information"); - - let encode = Arg::with_name("encode").long("encode").help("Encode transaction to base58"); - - let fetch_tx = SubCommand::with_name("fetch-tx") - .about("Fetch a blockchain transaction by hash") - .args(&vec![tx_hash, full, encode]); - - let simulate_tx = - SubCommand::with_name("simulate-tx").about("Read a transaction from stdin and simulate it"); - - let tx_hash = Arg::with_name("tx-hash").help("Transaction hash"); - - let encode = Arg::with_name("encode") - .long("encode") - .help("Encode specific history record transaction to base58"); - - let txs_history = SubCommand::with_name("txs-history") - .about("Fetch broadcasted transactions history") - .args(&vec![tx_hash, encode]); - - let explorer = SubCommand::with_name("explorer") - .about("Explorer related subcommands") - .subcommands(vec![fetch_tx, simulate_tx, txs_history]); - - // Alias - let alias = Arg::with_name("alias").help("Token alias"); - - let token = Arg::with_name("token").help("Token to create alias for"); - - let add = SubCommand::with_name("add").about("Create a Token alias").args(&vec![alias, token]); - - let alias = Arg::with_name("alias") - .short("a") - .long("alias") - .takes_value(true) - .help("Token alias to search for"); - - let token = Arg::with_name("token") - .short("t") - .long("token") - .takes_value(true) - .help("Token to search alias for"); - - let show = SubCommand::with_name("show") - .about( - "Print alias info of optional arguments. \ - If no argument is provided, list all the aliases in the wallet.", - ) - .args(&vec![alias, token]); - - let alias = Arg::with_name("alias").help("Token alias to remove"); - - let remove = SubCommand::with_name("remove").about("Remove a Token alias").arg(alias); - - let alias = SubCommand::with_name("alias") - .about("Manage Token aliases") - .subcommands(vec![add, show, remove]); - - // Token - let import = SubCommand::with_name("import").about("Import a mint authority secret from stdin"); - - let generate_mint = - SubCommand::with_name("generate-mint").about("Generate a new mint authority"); - - let list = - SubCommand::with_name("list").about("List token IDs with available mint authorities"); - - let token = Arg::with_name("token").help("Token ID to mint"); - - let amount = Arg::with_name("amount").help("Amount to mint"); - - let recipient = Arg::with_name("recipient").help("Recipient of the minted tokens"); - - let mint = - SubCommand::with_name("mint").about("Mint tokens").args(&vec![token, amount, recipient]); - - let token = Arg::with_name("token").help("Token ID to freeze"); - - let freeze = SubCommand::with_name("freeze").about("Freeze a token mint").arg(token); - - let token = SubCommand::with_name("token").about("Token functionalities").subcommands(vec![ - import, - generate_mint, - list, - mint, - freeze, - ]); - - // Main arguments - let config = Arg::with_name("config") - .short("c") - .long("config") - .takes_value(true) - .help("Configuration file to use"); - - let wallet_path = Arg::with_name("wallet_path") - .long("wallet-path") - .takes_value(true) - .help("Path to wallet database"); - - let wallet_pass = Arg::with_name("wallet_pass") - .long("wallet-pass") - .takes_value(true) - .help("Password for the wallet database"); - - let endpoint = Arg::with_name("endpoint") - .short("e") - .long("endpoint") - .takes_value(true) - .help("darkfid JSON-RPC endpoint"); - - let command = vec![ - kaching, - ping, - completions, - wallet, - unspend, - transfer, - otc, - inspect, - broadcast, - subscribe, - dao, - scan, - explorer, - alias, - token, - ]; - - let log = Arg::with_name("log") - .short("l") - .long("log") - .takes_value(true) - .help("Set log file to ouput into"); - - let verbose = Arg::with_name("verbose") - .short("v") - .multiple(true) - .help("Increase verbosity (-vvv supported)"); - - let mut app = App::new("drk") - .about(cli_desc!()) - .args(&vec![config, wallet_path, wallet_pass, endpoint, log, verbose]) - .subcommands(command); - - let shell = match Shell::from_str(shell) { - Ok(s) => s, - Err(e) => return Err(Error::Custom(e)), - }; - - app.gen_completions_to("./drk", shell, &mut std::io::stdout()); - - Ok(()) -} diff --git a/bin/drk2/src/main.rs b/bin/drk2/src/main.rs deleted file mode 100644 index cae719b1b..000000000 --- a/bin/drk2/src/main.rs +++ /dev/null @@ -1,1579 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 Dyne.org foundation - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use std::{ - fs, - io::{stdin, Read}, - process::exit, - str::FromStr, - sync::Arc, - time::Instant, -}; - -use prettytable::{format, row, Table}; -use rand::rngs::OsRng; -use smol::stream::StreamExt; -use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml}; -use url::Url; - -use darkfi::{ - async_daemonize, cli_desc, - rpc::{client::RpcClient, jsonrpc::JsonRequest, util::JsonValue}, - tx::Transaction, - util::{ - parse::{decode_base10, encode_base10}, - path::expand_path, - }, - zk::halo2::Field, - Result, -}; -use darkfi_money_contract::model::Coin; -use darkfi_sdk::{ - crypto::{PublicKey, SecretKey, TokenId}, - pasta::{group::ff::PrimeField, pallas}, -}; -use darkfi_serial::{deserialize, serialize}; - -/// Error codes -mod error; - -/// darkfid JSON-RPC related methods -mod rpc; - -/// Payment methods -mod transfer; - -/// Swap methods -mod swap; -use swap::PartialSwapData; - -/// Token methods -mod token; - -/// CLI utility functions -mod cli_util; -use cli_util::{generate_completions, kaching, parse_token_pair, parse_value_pair}; - -/// Wallet functionality related to Money -mod money; -use money::BALANCE_BASE10_DECIMALS; - -/// Wallet functionality related to Dao -mod dao; -use dao::DaoParams; - -/// Wallet functionality related to transactions history -mod txs_history; - -/// Wallet database operations handler -mod walletdb; -use walletdb::{WalletDb, WalletPtr}; - -const CONFIG_FILE: &str = "drk_config.toml"; -const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml"); - -// Dev Note: when adding/modifying args here, -// don't forget to update cli_util::generate_completions() -#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)] -#[serde(default)] -#[structopt(name = "drk", about = cli_desc!())] -struct Args { - #[structopt(short, long)] - /// Configuration file to use - config: Option, - - #[structopt(long, default_value = "~/.local/darkfi/drk/wallet.db")] - /// Path to wallet database - wallet_path: String, - - #[structopt(long, default_value = "changeme")] - /// Password for the wallet database - wallet_pass: String, - - #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")] - /// darkfid JSON-RPC endpoint - endpoint: Url, - - #[structopt(subcommand)] - /// Sub command to execute - command: Subcmd, - - #[structopt(short, long)] - /// Set log file to ouput into - log: Option, - - #[structopt(short, parse(from_occurrences))] - /// Increase verbosity (-vvv supported) - verbose: u8, -} - -// Dev Note: when adding/modifying commands here, -// don't forget to update cli_util::generate_completions() -#[derive(Clone, Debug, Deserialize, StructOpt)] -enum Subcmd { - /// Fun - Kaching, - - /// Send a ping request to the darkfid RPC endpoint - Ping, - - /// Generate a SHELL completion script and print to stdout - Completions { - /// The Shell you want to generate script for - shell: String, - }, - - /// Wallet operations - Wallet { - #[structopt(long)] - /// Initialize wallet database - initialize: bool, - - #[structopt(long)] - /// Generate a new keypair in the wallet - keygen: bool, - - #[structopt(long)] - /// Query the wallet for known balances - balance: bool, - - #[structopt(long)] - /// Get the default address in the wallet - address: bool, - - #[structopt(long)] - /// Print all the addresses in the wallet - addresses: bool, - - #[structopt(long)] - /// Set the default address in the wallet - default_address: Option, - - #[structopt(long)] - /// Print all the secret keys from the wallet - secrets: bool, - - #[structopt(long)] - /// Import secret keys from stdin into the wallet, separated by newlines - import_secrets: bool, - - #[structopt(long)] - /// Print the Merkle tree in the wallet - tree: bool, - - #[structopt(long)] - /// Print all the coins in the wallet - coins: bool, - }, - - /// Unspend a coin - Unspend { - /// base58-encoded coin to mark as unspent - coin: String, - }, - - /// Create a payment transaction - Transfer { - /// Amount to send - amount: String, - - /// Token ID to send - token: String, - - /// Recipient address - recipient: String, - }, - - /// OTC atomic swap - Otc { - #[structopt(subcommand)] - /// Sub command to execute - command: OtcSubcmd, - }, - - /// Inspect a transaction from stdin - Inspect, - - /// Read a transaction from stdin and broadcast it - Broadcast, - - /// This subscription will listen for incoming blocks from darkfid and look - /// through their transactions to see if there's any that interest us. - /// With `drk` we look at transactions calling the money contract so we can - /// find coins sent to us and fill our wallet with the necessary metadata. - Subscribe, - - /// DAO functionalities - Dao { - #[structopt(subcommand)] - /// Sub command to execute - command: DaoSubcmd, - }, - - /// Scan the blockchain and parse relevant transactions - Scan { - #[structopt(long)] - /// Reset Merkle tree and start scanning from first block - reset: bool, - - #[structopt(long)] - /// List all available checkpoints - list: bool, - - #[structopt(long)] - /// Reset Merkle tree to checkpoint index and start scanning - checkpoint: Option, - }, - - /// Explorer related subcommands - Explorer { - #[structopt(subcommand)] - /// Sub command to execute - command: ExplorerSubcmd, - }, - - /// Manage Token aliases - Alias { - #[structopt(subcommand)] - /// Sub command to execute - command: AliasSubcmd, - }, - - /// Token functionalities - Token { - #[structopt(subcommand)] - /// Sub command to execute - command: TokenSubcmd, - }, -} - -#[derive(Clone, Debug, Deserialize, StructOpt)] -enum OtcSubcmd { - /// Initialize the first half of the atomic swap - Init { - /// Value pair to send:recv (11.55:99.42) - #[structopt(short, long)] - value_pair: String, - - /// Token pair to send:recv (f00:b4r) - #[structopt(short, long)] - token_pair: String, - }, - - /// Build entire swap tx given the first half from stdin - Join, - - /// Inspect a swap half or the full swap tx from stdin - Inspect, - - /// Sign a transaction given from stdin as the first-half - Sign, -} - -#[derive(Clone, Debug, Deserialize, StructOpt)] -enum DaoSubcmd { - /// Create DAO parameters - Create { - /// The minimum amount of governance tokens needed to open a proposal for this DAO - proposer_limit: String, - /// Minimal threshold of participating total tokens needed for a proposal to pass - quorum: String, - /// The ratio of winning votes/total votes needed for a proposal to pass (2 decimals) - approval_ratio: f64, - /// DAO's governance token ID - gov_token_id: String, - }, - - /// View DAO data from stdin - View, - - /// Import DAO data from stdin - Import { - /// Named identifier for the DAO - dao_name: String, - }, - - /// List imported DAOs (or info about a specific one) - List { - /// Numeric identifier for the DAO (optional) - dao_alias: Option, - }, - - /// Show the balance of a DAO - Balance { - /// Name or numeric identifier for the DAO - dao_alias: String, - }, - - /// Mint an imported DAO on-chain - Mint { - /// Name or numeric identifier for the DAO - dao_alias: String, - }, - - /// Create a proposal for a DAO - Propose { - /// Name or numeric identifier for the DAO - dao_alias: String, - - /// Pubkey to send tokens to with proposal success - recipient: String, - - /// Amount to send from DAO with proposal success - amount: String, - - /// Token ID to send from DAO with proposal success - token: String, - }, - - /// List DAO proposals - Proposals { - /// Name or numeric identifier for the DAO - dao_alias: String, - }, - - /// View a DAO proposal data - Proposal { - /// Name or numeric identifier for the DAO - dao_alias: String, - - /// Numeric identifier for the proposal - proposal_id: u64, - }, - - /// Vote on a given proposal - Vote { - /// Name or numeric identifier for the DAO - dao_alias: String, - - /// Numeric identifier for the proposal - proposal_id: u64, - - /// Vote (0 for NO, 1 for YES) - vote: u8, - - /// Vote weight (amount of governance tokens) - vote_weight: String, - }, - - /// Execute a DAO proposal - Exec { - /// Name or numeric identifier for the DAO - dao_alias: String, - - /// Numeric identifier for the proposal - proposal_id: u64, - }, -} - -#[derive(Clone, Debug, Deserialize, StructOpt)] -enum ExplorerSubcmd { - /// Fetch a blockchain transaction by hash - FetchTx { - /// Transaction hash - tx_hash: String, - - #[structopt(long)] - /// Print the full transaction information - full: bool, - - #[structopt(long)] - /// Encode transaction to base58 - encode: bool, - }, - - /// Read a transaction from stdin and simulate it - SimulateTx, - - /// Fetch broadcasted transactions history - TxsHistory { - /// Fetch specific history record (optional) - tx_hash: Option, - - #[structopt(long)] - /// Encode specific history record transaction to base58 - encode: bool, - }, -} - -#[derive(Clone, Debug, Deserialize, StructOpt)] -enum AliasSubcmd { - /// Create a Token alias - Add { - /// Token alias - alias: String, - - /// Token to create alias for - token: String, - }, - - /// Print alias info of optional arguments. - /// If no argument is provided, list all the aliases in the wallet. - Show { - /// Token alias to search for - #[structopt(short, long)] - alias: Option, - - /// Token to search alias for - #[structopt(short, long)] - token: Option, - }, - - /// Remove a Token alias - Remove { - /// Token alias to remove - alias: String, - }, -} - -#[derive(Clone, Debug, Deserialize, StructOpt)] -enum TokenSubcmd { - /// Import a mint authority secret from stdin - Import, - - /// Generate a new mint authority - GenerateMint, - - /// List token IDs with available mint authorities - List, - - /// Mint tokens - Mint { - /// Token ID to mint - token: String, - - /// Amount to mint - amount: String, - - /// Recipient of the minted tokens - recipient: String, - }, - - /// Freeze a token mint - Freeze { - /// Token ID to freeze - token: String, - }, -} - -/// CLI-util structure -pub struct Drk { - /// Wallet database operations handler - pub wallet: WalletPtr, - /// JSON-RPC client to execute requests to darkfid daemon - pub rpc_client: RpcClient, -} - -impl Drk { - async fn new( - wallet_path: String, - wallet_pass: String, - endpoint: Url, - ex: Arc>, - ) -> Result { - // Initialize wallet - let wallet_path = expand_path(&wallet_path)?; - if !wallet_path.exists() { - if let Some(parent) = wallet_path.parent() { - fs::create_dir_all(parent)?; - } - } - let wallet = match WalletDb::new(Some(wallet_path), Some(&wallet_pass)) { - Ok(w) => w, - Err(e) => { - eprintln!("Error initializing wallet: {e:?}"); - exit(2); - } - }; - - // Initialize rpc client - let rpc_client = RpcClient::new(endpoint, ex).await?; - - Ok(Self { wallet, rpc_client }) - } - - /// Initialize wallet with tables for drk - async fn initialize_wallet(&self) -> Result<()> { - let wallet_schema = include_str!("../wallet.sql"); - if let Err(e) = self.wallet.exec_batch_sql(wallet_schema).await { - eprintln!("Error initializing wallet: {e:?}"); - exit(2); - } - - Ok(()) - } - - /// Auxilliary function to ping configured darkfid daemon for liveness. - async fn ping(&self) -> Result<()> { - eprintln!("Executing ping request to darkfid..."); - let latency = Instant::now(); - let req = JsonRequest::new("ping", JsonValue::Array(vec![])); - let rep = self.rpc_client.oneshot_request(req).await?; - let latency = latency.elapsed(); - eprintln!("Got reply: {rep:?}"); - eprintln!("Latency: {latency:?}"); - Ok(()) - } -} - -async_daemonize!(realmain); -async fn realmain(args: Args, ex: Arc>) -> Result<()> { - match args.command { - Subcmd::Kaching => { - kaching().await; - Ok(()) - } - - Subcmd::Ping => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - drk.ping().await - } - - Subcmd::Completions { shell } => generate_completions(&shell), - - Subcmd::Wallet { - initialize, - keygen, - balance, - address, - addresses, - default_address, - secrets, - import_secrets, - tree, - coins, - } => { - if !initialize && - !keygen && - !balance && - !address && - !addresses && - default_address.is_none() && - !secrets && - !tree && - !coins && - !import_secrets - { - eprintln!("Error: You must use at least one flag for this subcommand"); - eprintln!("Run with \"wallet -h\" to see the subcommand usage."); - exit(2); - } - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - - if initialize { - drk.initialize_wallet().await?; - if let Err(e) = drk.initialize_money().await { - eprintln!("Failed to initialize Money: {e:?}"); - exit(2); - } - if let Err(e) = drk.initialize_dao().await { - eprintln!("Failed to initialize DAO: {e:?}"); - exit(2); - } - return Ok(()) - } - - if keygen { - if let Err(e) = drk.money_keygen().await { - eprintln!("Failed to generate keypair: {e:?}"); - exit(2); - } - return Ok(()) - } - - if balance { - let balmap = drk.money_balance().await?; - - let aliases_map = drk.get_aliases_mapped_by_token().await?; - - // Create a prettytable with the new data: - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["Token ID", "Aliases", "Balance"]); - for (token_id, balance) in balmap.iter() { - let aliases = match aliases_map.get(token_id) { - Some(a) => a, - None => "-", - }; - - table.add_row(row![ - token_id, - aliases, - encode_base10(*balance, BALANCE_BASE10_DECIMALS) - ]); - } - - if table.is_empty() { - eprintln!("No unspent balances found"); - } else { - eprintln!("{table}"); - } - - return Ok(()) - } - - if address { - let address = match drk.default_address().await { - Ok(a) => a, - Err(e) => { - eprintln!("Failed to fetch default address: {e:?}"); - exit(2); - } - }; - - eprintln!("{address}"); - - return Ok(()) - } - - if addresses { - let addresses = drk.addresses().await?; - - // Create a prettytable with the new data: - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["Key ID", "Public Key", "Secret Key", "Is Default"]); - for (key_id, public_key, secret_key, is_default) in addresses { - let is_default = match is_default { - 1 => "*", - _ => "", - }; - table.add_row(row![key_id, public_key, secret_key, is_default]); - } - - if table.is_empty() { - eprintln!("No addresses found"); - } else { - eprintln!("{table}"); - } - - return Ok(()) - } - - if let Some(idx) = default_address { - if let Err(e) = drk.set_default_address(idx).await { - eprintln!("Failed to set default address: {e:?}"); - exit(2); - } - return Ok(()) - } - - if secrets { - let v = drk.get_money_secrets().await?; - - for i in v { - eprintln!("{i}"); - } - - return Ok(()) - } - - if import_secrets { - let mut secrets = vec![]; - let lines = stdin().lines(); - for (i, line) in lines.enumerate() { - if let Ok(line) = line { - let bytes = bs58::decode(&line.trim()).into_vec()?; - let Ok(secret) = deserialize(&bytes) else { - eprintln!("Warning: Failed to deserialize secret on line {i}"); - continue - }; - secrets.push(secret); - } - } - - let pubkeys = match drk.import_money_secrets(secrets).await { - Ok(p) => p, - Err(e) => { - eprintln!("Failed to import secret keys into wallet: {e:?}"); - exit(2); - } - }; - - for key in pubkeys { - eprintln!("{key}"); - } - - return Ok(()) - } - - if tree { - let tree = drk.get_money_tree().await?; - - eprintln!("{tree:#?}"); - - return Ok(()) - } - - if coins { - let coins = drk.get_coins(true).await?; - - let aliases_map = drk.get_aliases_mapped_by_token().await?; - - if coins.is_empty() { - return Ok(()) - } - - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row![ - "Coin", - "Spent", - "Token ID", - "Aliases", - "Value", - "Spend Hook", - "User Data" - ]); - let zero = pallas::Base::zero(); - for coin in coins { - let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) { - Some(a) => a, - None => "-", - }; - - let spend_hook = if coin.0.note.spend_hook != zero { - bs58::encode(&serialize(&coin.0.note.spend_hook)).into_string().to_string() - } else { - String::from("-") - }; - - let user_data = if coin.0.note.user_data != zero { - bs58::encode(&serialize(&coin.0.note.user_data)).into_string().to_string() - } else { - String::from("-") - }; - - table.add_row(row![ - bs58::encode(&serialize(&coin.0.coin.inner())).into_string().to_string(), - coin.1, - coin.0.note.token_id, - aliases, - format!("{} ({})", coin.0.note.value, encode_base10(coin.0.note.value, 8)), - spend_hook, - user_data - ]); - } - - eprintln!("{table}"); - - return Ok(()) - } - - unreachable!() - } - - Subcmd::Unspend { coin } => { - let bytes: [u8; 32] = match bs58::decode(&coin).into_vec()?.try_into() { - Ok(b) => b, - Err(e) => { - eprintln!("Invalid coin: {e:?}"); - exit(2); - } - }; - - let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { - Some(v) => v, - None => { - eprintln!("Invalid coin"); - exit(2); - } - }; - - let coin = Coin::from(elem); - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - if let Err(e) = drk.unspend_coin(&coin).await { - eprintln!("Failed to mark coin as unspent: {e:?}"); - exit(2); - } - - Ok(()) - } - - Subcmd::Transfer { amount, token, recipient } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - - if let Err(e) = f64::from_str(&amount) { - eprintln!("Invalid amount: {e:?}"); - exit(2); - } - - let rcpt = match PublicKey::from_str(&recipient) { - Ok(r) => r, - Err(e) => { - eprintln!("Invalid recipient: {e:?}"); - exit(2); - } - }; - - let token_id = match drk.get_token(token).await { - Ok(t) => t, - Err(e) => { - eprintln!("Invalid token alias: {e:?}"); - exit(2); - } - }; - - let tx = match drk.transfer(&amount, token_id, rcpt).await { - Ok(t) => t, - Err(e) => { - eprintln!("Failed to create payment transaction: {e:?}"); - exit(2); - } - }; - - println!("{}", bs58::encode(&serialize(&tx)).into_string()); - - Ok(()) - } - - Subcmd::Otc { command } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - - match command { - OtcSubcmd::Init { value_pair, token_pair } => { - let (vp_send, vp_recv) = parse_value_pair(&value_pair)?; - let (tp_send, tp_recv) = parse_token_pair(&drk, &token_pair).await?; - - let half = match drk.init_swap(vp_send, tp_send, vp_recv, tp_recv).await { - Ok(h) => h, - Err(e) => { - eprintln!("Failed to create swap transaction half: {e:?}"); - exit(2); - } - }; - - println!("{}", bs58::encode(&serialize(&half)).into_string()); - Ok(()) - } - - OtcSubcmd::Join => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let partial: PartialSwapData = deserialize(&bytes)?; - - let tx = match drk.join_swap(partial).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to create a join swap transaction: {e:?}"); - exit(2); - } - }; - - println!("{}", bs58::encode(&serialize(&tx)).into_string()); - Ok(()) - } - - OtcSubcmd::Inspect => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - - if let Err(e) = drk.inspect_swap(bytes).await { - eprintln!("Failed to inspect swap: {e:?}"); - exit(2); - }; - - Ok(()) - } - - OtcSubcmd::Sign => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let mut tx: Transaction = deserialize(&bytes)?; - - if let Err(e) = drk.sign_swap(&mut tx).await { - eprintln!("Failed to sign joined swap transaction: {e:?}"); - exit(2); - }; - - println!("{}", bs58::encode(&serialize(&tx)).into_string()); - Ok(()) - } - } - } - - Subcmd::Dao { command } => match command { - DaoSubcmd::Create { proposer_limit, quorum, approval_ratio, gov_token_id } => { - if let Err(e) = f64::from_str(&proposer_limit) { - eprintln!("Invalid proposer limit: {e:?}"); - exit(2); - } - if let Err(e) = f64::from_str(&quorum) { - eprintln!("Invalid quorum: {e:?}"); - exit(2); - } - - let proposer_limit = decode_base10(&proposer_limit, BALANCE_BASE10_DECIMALS, true)?; - let quorum = decode_base10(&quorum, BALANCE_BASE10_DECIMALS, true)?; - - if approval_ratio > 1.0 { - eprintln!("Error: Approval ratio cannot be >1.0"); - exit(2); - } - - let approval_ratio_base = 100_u64; - let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64; - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let gov_token_id = match drk.get_token(gov_token_id).await { - Ok(g) => g, - Err(e) => { - eprintln!("Invalid Token ID: {e:?}"); - exit(2); - } - }; - - let secret_key = SecretKey::random(&mut OsRng); - let bulla_blind = pallas::Base::random(&mut OsRng); - - let dao_params = DaoParams { - proposer_limit, - quorum, - approval_ratio_base, - approval_ratio_quot, - gov_token_id, - secret_key, - bulla_blind, - }; - - let encoded = bs58::encode(&serialize(&dao_params)).into_string(); - eprintln!("{encoded}"); - - Ok(()) - } - - DaoSubcmd::View => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let dao_params: DaoParams = deserialize(&bytes)?; - eprintln!("{dao_params}"); - - Ok(()) - } - - DaoSubcmd::Import { dao_name } => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let dao_params: DaoParams = deserialize(&bytes)?; - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - - if let Err(e) = drk.import_dao(dao_name, dao_params).await { - eprintln!("Failed to import DAO: {e:?}"); - exit(2); - } - - Ok(()) - } - - DaoSubcmd::List { dao_alias } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - // We cannot use .map() since get_dao_id() uses ? - let dao_id = match dao_alias { - Some(alias) => Some(drk.get_dao_id(&alias).await?), - None => None, - }; - - if let Err(e) = drk.dao_list(dao_id).await { - eprintln!("Failed to list DAO: {e:?}"); - exit(2); - } - - Ok(()) - } - - DaoSubcmd::Balance { dao_alias } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let balmap = match drk.dao_balance(dao_id).await { - Ok(b) => b, - Err(e) => { - eprintln!("Failed to fetch DAO balance: {e:?}"); - exit(2); - } - }; - - let aliases_map = match drk.get_aliases_mapped_by_token().await { - Ok(a) => a, - Err(e) => { - eprintln!("Failed to fetch wallet aliases: {e:?}"); - exit(2); - } - }; - - // Create a prettytable with the new data: - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["Token ID", "Aliases", "Balance"]); - for (token_id, balance) in balmap.iter() { - let aliases = match aliases_map.get(token_id) { - Some(a) => a, - None => "-", - }; - - table.add_row(row![ - token_id, - aliases, - encode_base10(*balance, BALANCE_BASE10_DECIMALS) - ]); - } - - if table.is_empty() { - eprintln!("No unspent balances found"); - } else { - eprintln!("{table}"); - } - - Ok(()) - } - - DaoSubcmd::Mint { dao_alias } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let tx = match drk.dao_mint(dao_id).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to mint DAO: {e:?}"); - exit(2); - } - }; - eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); - Ok(()) - } - - DaoSubcmd::Propose { dao_alias, recipient, amount, token } => { - if let Err(e) = f64::from_str(&amount) { - eprintln!("Invalid amount: {e:?}"); - exit(2); - } - let amount = decode_base10(&amount, BALANCE_BASE10_DECIMALS, true)?; - let rcpt = match PublicKey::from_str(&recipient) { - Ok(r) => r, - Err(e) => { - eprintln!("Invalid recipient: {e:?}"); - exit(2); - } - }; - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - let token_id = match drk.get_token(token).await { - Ok(t) => t, - Err(e) => { - eprintln!("Invalid token alias: {e:?}"); - exit(2); - } - }; - - let tx = match drk.dao_propose(dao_id, rcpt, amount, token_id).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to create DAO proposal: {e:?}"); - exit(2); - } - }; - eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); - Ok(()) - } - - DaoSubcmd::Proposals { dao_alias } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let proposals = drk.get_dao_proposals(dao_id).await?; - - for proposal in proposals { - eprintln!("[{}] {:?}", proposal.id, proposal.bulla()); - } - - Ok(()) - } - - DaoSubcmd::Proposal { dao_alias, proposal_id } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let proposals = drk.get_dao_proposals(dao_id).await?; - let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else { - eprintln!("No such DAO proposal found"); - exit(2); - }; - - eprintln!("{proposal}"); - - let votes = drk.get_dao_proposal_votes(proposal_id).await?; - eprintln!("votes:"); - for vote in votes { - let option = if vote.vote_option { "yes" } else { "no " }; - eprintln!(" {option} {}", vote.all_vote_value); - } - - Ok(()) - } - - DaoSubcmd::Vote { dao_alias, proposal_id, vote, vote_weight } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - if let Err(e) = f64::from_str(&vote_weight) { - eprintln!("Invalid vote weight: {e:?}"); - exit(2); - } - let weight = decode_base10(&vote_weight, BALANCE_BASE10_DECIMALS, true)?; - - if vote > 1 { - eprintln!("Vote can be either 0 (NO) or 1 (YES)"); - exit(2); - } - let vote = vote != 0; - - let tx = match drk.dao_vote(dao_id, proposal_id, vote, weight).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to create DAO Vote transaction: {e:?}"); - exit(2); - } - }; - - // TODO: Write our_vote in the proposal sql. - - eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); - - Ok(()) - } - - DaoSubcmd::Exec { dao_alias, proposal_id } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - let dao = drk.get_dao_by_id(dao_id).await?; - let proposal = drk.get_dao_proposal_by_id(proposal_id).await?; - assert!(proposal.dao_bulla == dao.bulla()); - - let tx = match drk.dao_exec(dao, proposal).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to execute DAO proposal: {e:?}"); - exit(2); - } - }; - eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); - - Ok(()) - } - }, - - Subcmd::Inspect => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let tx: Transaction = deserialize(&bytes)?; - eprintln!("{tx:#?}"); - Ok(()) - } - - Subcmd::Broadcast => { - eprintln!("Reading transaction from stdin..."); - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let tx = deserialize(&bytes)?; - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - - let txid = match drk.broadcast_tx(&tx).await { - Ok(t) => t, - Err(e) => { - eprintln!("Failed to broadcast transaction: {e:?}"); - exit(2); - } - }; - - eprintln!("Transaction ID: {txid}"); - - Ok(()) - } - - Subcmd::Subscribe => { - let drk = - Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) - .await?; - - if let Err(e) = drk.subscribe_blocks(args.endpoint, ex).await { - eprintln!("Block subscription failed: {e:?}"); - exit(2); - } - - Ok(()) - } - - Subcmd::Scan { reset, list, checkpoint } => { - let drk = - Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) - .await?; - - if reset { - eprintln!("Reset requested."); - if let Err(e) = drk.scan_blocks(true).await { - eprintln!("Failed during scanning: {e:?}"); - exit(2); - } - eprintln!("Finished scanning blockchain"); - - return Ok(()) - } - - if list { - eprintln!("List requested."); - // TODO: implement - unimplemented!() - } - - if let Some(c) = checkpoint { - eprintln!("Checkpoint requested: {c}"); - // TODO: implement - unimplemented!() - } - - if let Err(e) = drk.scan_blocks(false).await { - eprintln!("Failed during scanning: {e:?}"); - exit(2); - } - eprintln!("Finished scanning blockchain"); - - Ok(()) - } - - Subcmd::Explorer { command } => match command { - ExplorerSubcmd::FetchTx { tx_hash, full, encode } => { - let tx_hash = blake3::Hash::from_hex(&tx_hash)?; - - let drk = - Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) - .await?; - - let tx = match drk.get_tx(&tx_hash).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to fetch transaction: {e:?}"); - exit(2); - } - }; - - let Some(tx) = tx else { - eprintln!("Transaction was not found"); - exit(1); - }; - - // Make sure the tx is correct - assert_eq!(tx.hash()?, tx_hash); - - if encode { - eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); - exit(1) - } - - eprintln!("Transaction ID: {tx_hash}"); - if full { - eprintln!("{tx:?}"); - } - - Ok(()) - } - - ExplorerSubcmd::SimulateTx => { - eprintln!("Reading transaction from stdin..."); - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let bytes = bs58::decode(&buf.trim()).into_vec()?; - let tx = deserialize(&bytes)?; - - let drk = - Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) - .await?; - - let is_valid = match drk.simulate_tx(&tx).await { - Ok(b) => b, - Err(e) => { - eprintln!("Failed to simulate tx: {e:?}"); - exit(2); - } - }; - - eprintln!("Transaction ID: {}", tx.hash()?); - eprintln!("State: {}", if is_valid { "valid" } else { "invalid" }); - - Ok(()) - } - - ExplorerSubcmd::TxsHistory { tx_hash, encode } => { - let drk = - Drk::new(args.wallet_path, args.wallet_pass, args.endpoint.clone(), ex.clone()) - .await?; - - if let Some(c) = tx_hash { - let (tx_hash, status, tx) = drk.get_tx_history_record(&c).await?; - - if encode { - println!("{}", bs58::encode(&serialize(&tx)).into_string()); - exit(1) - } - - eprintln!("Transaction ID: {tx_hash}"); - eprintln!("Status: {status}"); - eprintln!("{tx:?}"); - - return Ok(()) - } - - let map = match drk.get_txs_history().await { - Ok(m) => m, - Err(e) => { - eprintln!("Failed to retrieve transactions history records: {e:?}"); - exit(2); - } - }; - - // Create a prettytable with the new data: - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["Transaction Hash", "Status"]); - for (txs_hash, status) in map.iter() { - table.add_row(row![txs_hash, status]); - } - - if table.is_empty() { - eprintln!("No transactions found"); - } else { - eprintln!("{table}"); - } - - Ok(()) - } - }, - - Subcmd::Alias { command } => match command { - AliasSubcmd::Add { alias, token } => { - if alias.chars().count() > 5 { - eprintln!("Error: Alias exceeds 5 characters"); - exit(2); - } - - let token_id = match TokenId::from_str(token.as_str()) { - Ok(t) => t, - Err(e) => { - eprintln!("Invalid Token ID: {e:?}"); - exit(2); - } - }; - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - if let Err(e) = drk.add_alias(alias, token_id).await { - eprintln!("Failed to add alias: {e:?}"); - exit(2); - } - - Ok(()) - } - - AliasSubcmd::Show { alias, token } => { - let token_id = match token { - Some(t) => match TokenId::from_str(t.as_str()) { - Ok(t) => Some(t), - Err(e) => { - eprintln!("Invalid Token ID: {e:?}"); - exit(2); - } - }, - None => None, - }; - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let map = drk.get_aliases(alias, token_id).await?; - - // Create a prettytable with the new data: - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["Alias", "Token ID"]); - for (alias, token_id) in map.iter() { - table.add_row(row![alias, token_id]); - } - - if table.is_empty() { - eprintln!("No aliases found"); - } else { - eprintln!("{table}"); - } - - Ok(()) - } - - AliasSubcmd::Remove { alias } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - if let Err(e) = drk.remove_alias(alias).await { - eprintln!("Failed to remove alias: {e:?}"); - exit(2); - } - - Ok(()) - } - }, - - Subcmd::Token { command } => match command { - TokenSubcmd::Import => { - let mut buf = String::new(); - stdin().read_to_string(&mut buf)?; - let mint_authority = match SecretKey::from_str(buf.trim()) { - Ok(ma) => ma, - Err(e) => { - eprintln!("Invalid secret key: {e:?}"); - exit(2); - } - }; - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - if let Err(e) = drk.import_mint_authority(mint_authority).await { - eprintln!("Importing mint authority failed: {e:?}"); - exit(2); - }; - - let token_id = TokenId::derive(mint_authority); - eprintln!("Successfully imported mint authority for token ID: {token_id}"); - - Ok(()) - } - - TokenSubcmd::GenerateMint => { - let mint_authority = SecretKey::random(&mut OsRng); - - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - - if let Err(e) = drk.import_mint_authority(mint_authority).await { - eprintln!("Importing mint authority failed: {e:?}"); - exit(2); - }; - - let token_id = TokenId::derive(mint_authority); - eprintln!("Successfully imported mint authority for token ID: {token_id}"); - - Ok(()) - } - - TokenSubcmd::List => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let tokens = drk.list_tokens().await?; - let aliases_map = match drk.get_aliases_mapped_by_token().await { - Ok(map) => map, - Err(e) => { - eprintln!("Failed to fetch wallet aliases: {e:?}"); - exit(2); - } - }; - - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["Token ID", "Aliases", "Mint Authority", "Frozen"]); - - for (token_id, authority, frozen) in tokens { - let aliases = match aliases_map.get(&token_id.to_string()) { - Some(a) => a, - None => "-", - }; - - table.add_row(row![token_id, aliases, authority, frozen]); - } - - if table.is_empty() { - eprintln!("No tokens found"); - } else { - eprintln!("{table}"); - } - - Ok(()) - } - - // TODO: Mint directly into DAO treasury - TokenSubcmd::Mint { token, amount, recipient } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - - if let Err(e) = f64::from_str(&amount) { - eprintln!("Invalid amount: {e:?}"); - exit(2); - } - - let rcpt = match PublicKey::from_str(&recipient) { - Ok(r) => r, - Err(e) => { - eprintln!("Invalid recipient: {e:?}"); - exit(2); - } - }; - - let token_id = match drk.get_token(token).await { - Ok(t) => t, - Err(e) => { - eprintln!("Invalid Token ID: {e:?}"); - exit(2); - } - }; - - let tx = match drk.mint_token(&amount, rcpt, token_id).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to create token mint transaction: {e:?}"); - exit(2); - } - }; - - eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); - - Ok(()) - } - - TokenSubcmd::Freeze { token } => { - let drk = Drk::new(args.wallet_path, args.wallet_pass, args.endpoint, ex).await?; - let token_id = match drk.get_token(token).await { - Ok(t) => t, - Err(e) => { - eprintln!("Invalid Token ID: {e:?}"); - exit(2); - } - }; - - let tx = match drk.freeze_token(token_id).await { - Ok(tx) => tx, - Err(e) => { - eprintln!("Failed to create token freeze transaction: {e:?}"); - exit(2); - } - }; - - eprintln!("{}", bs58::encode(&serialize(&tx)).into_string()); - - Ok(()) - } - }, - } -} diff --git a/bin/drk2/wallet.mp3 b/bin/drk2/wallet.mp3 deleted file mode 100644 index 691f88463bb54fc8422d7bb2d4df8a9ff46427f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29037 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