From a9772bfa3a25e7d57350d0646fc4f99e8a708b0d Mon Sep 17 00:00:00 2001 From: KashProtocol Date: Wed, 3 Jan 2024 22:28:37 +0800 Subject: [PATCH] Cherry-picked Cumulative PR for Wallet & Metrics Refactoring --- Cargo.lock | 865 ++++++---- Cargo.toml | 52 +- cli/Cargo.toml | 1 + cli/src/cli.rs | 102 +- cli/src/error.rs | 3 + cli/src/extensions/mod.rs | 2 + cli/src/extensions/transaction.rs | 180 +++ cli/src/imports.rs | 10 +- cli/src/lib.rs | 4 +- cli/src/metrics/data.rs | 263 --- cli/src/metrics/metrics.rs | 240 --- cli/src/metrics/mod.rs | 6 - cli/src/modules/account.rs | 28 +- cli/src/modules/connect.rs | 4 +- cli/src/modules/estimate.rs | 4 +- cli/src/modules/export.rs | 21 +- cli/src/modules/history.rs | 14 +- cli/src/modules/message.rs | 11 +- cli/src/modules/metrics.rs | 110 ++ cli/src/modules/mod.rs | 4 + cli/src/modules/monitor.rs | 16 +- cli/src/modules/node.rs | 7 +- cli/src/modules/ping.rs | 2 +- cli/src/modules/rpc.rs | 15 +- cli/src/modules/wallet.rs | 6 +- cli/src/wizards/account.rs | 57 +- cli/src/wizards/import.rs | 18 +- cli/src/wizards/wallet.rs | 47 +- components/consensusmanager/src/session.rs | 8 + consensus/core/benches/serde_benchmark.rs | 2 +- consensus/core/src/api/mod.rs | 8 + consensus/core/src/config/params.rs | 2 +- consensus/core/src/sign.rs | 71 +- consensus/core/src/subnets.rs | 8 +- consensus/core/src/tx.rs | 19 +- consensus/core/src/tx/script_public_key.rs | 8 +- consensus/src/consensus/mod.rs | 8 + .../transaction_validator_populated.rs | 2 +- consensus/wasm/src/keypair.rs | 4 +- consensus/wasm/src/outpoint.rs | 4 +- consensus/wasm/src/signer.rs | 4 +- consensus/wasm/src/transaction.rs | 4 +- consensus/wasm/src/txscript.rs | 4 +- consensus/wasm/src/utxo.rs | 18 +- core/src/log/mod.rs | 2 +- crypto/addresses/src/lib.rs | 55 +- crypto/txscript/src/opcodes/mod.rs | 4 +- indexes/core/src/indexed_utxos.rs | 2 +- kashd/src/args.rs | 115 +- kashd/src/daemon.rs | 2 +- kos/Cargo.toml | 2 +- kos/src/imports.rs | 2 +- kos/src/metrics/ipc.rs | 2 +- kos/src/metrics/metrics.rs | 8 +- kos/src/metrics/toolbar.rs | 2 +- kos/src/modules/metrics.rs | 3 +- kos/src/terminal/terminal.rs | 10 +- metrics/core/Cargo.toml | 19 + metrics/core/src/data.rs | 875 ++++++++++ metrics/core/src/error.rs | 29 + metrics/core/src/lib.rs | 185 +++ metrics/core/src/result.rs | 1 + notify/src/collector.rs | 2 +- protocol/p2p/src/core/hub.rs | 5 + rothschild/src/main.rs | 3 +- rpc/core/src/api/ctl.rs | 4 +- rpc/core/src/api/rpc.rs | 14 +- rpc/core/src/model/message.rs | 71 +- rpc/core/src/model/tx.rs | 2 +- rpc/grpc/core/proto/rpc.proto | 46 +- rpc/grpc/core/src/convert/message.rs | 10 +- rpc/grpc/core/src/convert/metrics.rs | 84 +- rpc/service/Cargo.toml | 2 +- rpc/service/src/service.rs | 83 +- rpc/wrpc/client/src/client.rs | 20 +- rpc/wrpc/client/src/wasm.rs | 6 +- rpc/wrpc/core/Cargo.toml | 13 - rpc/wrpc/core/src/lib.rs | 8 - rpc/wrpc/proxy/Cargo.toml | 1 - rpc/wrpc/proxy/src/main.rs | 14 +- rpc/wrpc/server/Cargo.toml | 1 - rpc/wrpc/server/src/server.rs | 4 +- rpc/wrpc/server/src/service.rs | 21 +- simpa/src/simulator/miner.rs | 2 +- .../src/consensus_integration_tests.rs | 2 +- testing/integration/src/rpc_tests.rs | 28 +- wallet/bip32/Cargo.toml | 2 + wallet/bip32/src/attrs.rs | 3 +- wallet/bip32/src/child_number.rs | 3 +- wallet/bip32/src/error.rs | 3 + wallet/bip32/src/lib.rs | 2 +- wallet/bip32/src/mnemonic/mod.rs | 2 +- wallet/bip32/src/mnemonic/phrase.rs | 68 +- wallet/bip32/src/prefix.rs | 3 +- wallet/bip32/src/xkey.rs | 2 +- wallet/bip32/src/xprivate_key.rs | 4 +- wallet/bip32/src/xpublic_key.rs | 115 +- wallet/core/Cargo.toml | 4 + wallet/core/src/account/descriptor.rs | 181 +++ wallet/core/src/account/kind.rs | 115 ++ wallet/core/src/{runtime => }/account/mod.rs | 274 ++-- wallet/core/src/account/variants/bip32.rs | 259 +++ wallet/core/src/account/variants/keypair.rs | 192 +++ wallet/core/src/account/variants/legacy.rs | 237 +++ wallet/core/src/account/variants/mod.rs | 15 + wallet/core/src/account/variants/multisig.rs | 272 ++++ .../account/variants/resident.rs | 46 +- wallet/core/src/api/message.rs | 465 ++++++ wallet/core/src/api/mod.rs | 11 + wallet/core/src/api/traits.rs | 380 +++++ wallet/core/src/api/transport.rs | 148 ++ wallet/core/src/derivation/gen0/hd.rs | 59 +- wallet/core/src/derivation/gen1/hd.rs | 31 +- wallet/core/src/derivation/gen1/import.rs | 1 - wallet/core/src/derivation/mod.rs | 88 +- wallet/core/src/derivation/traits.rs | 6 +- wallet/core/src/deterministic.rs | 159 ++ wallet/core/src/encryption.rs | 122 +- wallet/core/src/error.rs | 67 +- wallet/core/src/events.rs | 139 +- wallet/core/src/factory.rs | 59 + wallet/core/src/imports.rs | 32 +- wallet/core/src/lib.rs | 54 +- wallet/core/src/message.rs | 4 + wallet/core/src/prelude.rs | 22 + wallet/core/src/result.rs | 4 + wallet/core/src/rpc.rs | 12 +- wallet/core/src/runtime/account/id.rs | 113 -- wallet/core/src/runtime/account/kind.rs | 63 - .../src/runtime/account/variants/bip32.rs | 95 -- .../src/runtime/account/variants/keypair.rs | 70 - .../src/runtime/account/variants/legacy.rs | 130 -- .../core/src/runtime/account/variants/mod.rs | 11 - .../src/runtime/account/variants/multisig.rs | 107 -- wallet/core/src/runtime/mod.rs | 11 - wallet/core/src/runtime/wallet.rs | 1148 -------------- wallet/core/src/secret.rs | 8 +- wallet/core/src/serializer.rs | 64 + wallet/core/src/settings.rs | 6 +- wallet/core/src/storage/account.rs | 234 ++- wallet/core/src/storage/address.rs | 7 +- wallet/core/src/storage/binding.rs | 13 +- wallet/core/src/storage/hint.rs | 9 +- wallet/core/src/storage/id.rs | 9 +- wallet/core/src/storage/interface.rs | 162 +- wallet/core/src/storage/keydata.rs | 391 ----- wallet/core/src/storage/keydata/assoc.rs | 112 ++ wallet/core/src/storage/keydata/data.rs | 316 ++++ wallet/core/src/storage/keydata/id.rs | 77 + wallet/core/src/storage/keydata/info.rs | 52 + wallet/core/src/storage/keydata/mod.rs | 15 + wallet/core/src/storage/local/cache.rs | 75 +- wallet/core/src/storage/local/collection.rs | 27 +- wallet/core/src/storage/local/interface.rs | 356 +++-- wallet/core/src/storage/local/mod.rs | 137 +- wallet/core/src/storage/local/payload.rs | 75 +- wallet/core/src/storage/local/storage.rs | 26 +- wallet/core/src/storage/local/streams.rs | 40 +- .../src/storage/local/transaction/fsio.rs | 165 +- .../src/storage/local/transaction/indexdb.rs | 66 +- .../core/src/storage/local/transaction/mod.rs | 4 + wallet/core/src/storage/local/wallet.rs | 135 +- wallet/core/src/storage/metadata.rs | 39 +- wallet/core/src/storage/mod.rs | 72 +- wallet/core/src/storage/storable.rs | 9 + wallet/core/src/storage/transaction.rs | 437 ----- wallet/core/src/storage/transaction/data.rs | 394 +++++ wallet/core/src/storage/transaction/kind.rs | 85 + wallet/core/src/storage/transaction/mod.rs | 13 + wallet/core/src/storage/transaction/record.rs | 518 ++++++ wallet/core/src/storage/transaction/utxo.rs | 35 + wallet/core/src/tests/keys.rs | 8 + wallet/core/src/tests/mod.rs | 13 + wallet/core/src/tests/rpc_core_mock.rs | 258 +++ wallet/core/src/tests/storage.rs | 31 + wallet/core/src/tx/consensus.rs | 5 + wallet/core/src/tx/fees.rs | 8 +- wallet/core/src/tx/generator/generator.rs | 76 +- wallet/core/src/tx/generator/mod.rs | 7 +- wallet/core/src/tx/generator/pending.rs | 96 +- wallet/core/src/tx/generator/settings.rs | 28 +- wallet/core/src/tx/generator/signer.rs | 29 +- wallet/core/src/tx/generator/summary.rs | 11 +- wallet/core/src/tx/generator/test.rs | 30 +- wallet/core/src/tx/mass.rs | 7 +- wallet/core/src/tx/mod.rs | 4 + wallet/core/src/tx/payment.rs | 11 +- wallet/core/src/types.rs | 9 + wallet/core/src/utils.rs | 31 +- wallet/core/src/{runtime => utxo}/balance.rs | 42 +- wallet/core/src/utxo/binding.rs | 6 +- wallet/core/src/utxo/context.rs | 471 ++++-- wallet/core/src/utxo/iterator.rs | 4 + wallet/core/src/utxo/mod.rs | 16 +- wallet/core/src/utxo/outgoing.rs | 105 ++ wallet/core/src/utxo/pending.rs | 12 +- wallet/core/src/utxo/processor.rs | 222 ++- wallet/core/src/utxo/reference.rs | 57 +- wallet/core/src/utxo/scan.rs | 130 +- wallet/core/src/utxo/settings.rs | 38 +- wallet/core/src/utxo/stream.rs | 4 + wallet/core/src/{runtime => utxo}/sync.rs | 10 +- wallet/core/src/utxo/test.rs | 45 + wallet/core/src/wallet/api.rs | 361 +++++ wallet/core/src/wallet/args.rs | 147 ++ wallet/core/src/{runtime => wallet}/maps.rs | 6 +- wallet/core/src/wallet/mod.rs | 1412 +++++++++++++++++ wallet/core/src/wasm/balance.rs | 2 +- wallet/core/src/wasm/mod.rs | 4 + .../core/src/wasm/tx/generator/generator.rs | 41 +- wallet/core/src/wasm/tx/generator/pending.rs | 6 +- wallet/core/src/wasm/tx/mass.rs | 2 +- wallet/core/src/wasm/tx/utils.rs | 6 +- wallet/core/src/wasm/utxo/processor.rs | 2 +- wallet/core/src/wasm/wallet/account.rs | 68 +- wallet/core/src/wasm/wallet/keydata.rs | 5 +- wallet/core/src/wasm/wallet/storage.rs | 26 +- wallet/core/src/wasm/wallet/wallet.rs | 341 ++-- wallet/core/src/wasm/xprivatekey.rs | 2 +- wallet/core/src/wasm/xpublickey.rs | 2 +- wallet/macros/Cargo.toml | 24 + wallet/macros/src/handler.rs | 60 + wallet/macros/src/lib.rs | 28 + wallet/macros/src/wallet/client.rs | 94 ++ wallet/macros/src/wallet/mod.rs | 4 + wallet/macros/src/wallet/server.rs | 93 ++ wallet/macros/src/wallet/wasm.rs | 172 ++ wasm/npm/package.json | 6 +- 228 files changed, 13035 insertions(+), 5329 deletions(-) create mode 100644 cli/src/extensions/mod.rs create mode 100644 cli/src/extensions/transaction.rs delete mode 100644 cli/src/metrics/metrics.rs delete mode 100644 cli/src/metrics/mod.rs create mode 100644 cli/src/modules/metrics.rs create mode 100644 metrics/core/Cargo.toml create mode 100644 metrics/core/src/data.rs create mode 100644 metrics/core/src/error.rs create mode 100644 metrics/core/src/lib.rs create mode 100644 metrics/core/src/result.rs delete mode 100644 rpc/wrpc/core/src/lib.rs create mode 100644 wallet/core/src/account/descriptor.rs create mode 100644 wallet/core/src/account/kind.rs rename wallet/core/src/{runtime => }/account/mod.rs (81%) create mode 100644 wallet/core/src/account/variants/bip32.rs create mode 100644 wallet/core/src/account/variants/keypair.rs create mode 100644 wallet/core/src/account/variants/legacy.rs create mode 100644 wallet/core/src/account/variants/mod.rs create mode 100644 wallet/core/src/account/variants/multisig.rs rename wallet/core/src/{runtime => }/account/variants/resident.rs (50%) create mode 100644 wallet/core/src/api/message.rs create mode 100644 wallet/core/src/api/mod.rs create mode 100644 wallet/core/src/api/traits.rs create mode 100644 wallet/core/src/api/transport.rs create mode 100644 wallet/core/src/deterministic.rs create mode 100644 wallet/core/src/factory.rs create mode 100644 wallet/core/src/prelude.rs delete mode 100644 wallet/core/src/runtime/account/kind.rs delete mode 100644 wallet/core/src/runtime/account/variants/bip32.rs delete mode 100644 wallet/core/src/runtime/account/variants/keypair.rs delete mode 100644 wallet/core/src/runtime/account/variants/legacy.rs delete mode 100644 wallet/core/src/runtime/account/variants/mod.rs delete mode 100644 wallet/core/src/runtime/account/variants/multisig.rs delete mode 100644 wallet/core/src/runtime/mod.rs delete mode 100644 wallet/core/src/runtime/wallet.rs create mode 100644 wallet/core/src/serializer.rs delete mode 100644 wallet/core/src/storage/keydata.rs create mode 100644 wallet/core/src/storage/keydata/assoc.rs create mode 100644 wallet/core/src/storage/keydata/data.rs create mode 100644 wallet/core/src/storage/keydata/id.rs create mode 100644 wallet/core/src/storage/keydata/info.rs create mode 100644 wallet/core/src/storage/keydata/mod.rs create mode 100644 wallet/core/src/storage/storable.rs delete mode 100644 wallet/core/src/storage/transaction.rs create mode 100644 wallet/core/src/storage/transaction/data.rs create mode 100644 wallet/core/src/storage/transaction/kind.rs create mode 100644 wallet/core/src/storage/transaction/mod.rs create mode 100644 wallet/core/src/storage/transaction/record.rs create mode 100644 wallet/core/src/storage/transaction/utxo.rs create mode 100644 wallet/core/src/tests/keys.rs create mode 100644 wallet/core/src/tests/mod.rs create mode 100644 wallet/core/src/tests/rpc_core_mock.rs create mode 100644 wallet/core/src/tests/storage.rs create mode 100644 wallet/core/src/types.rs rename wallet/core/src/{runtime => utxo}/balance.rs (73%) create mode 100644 wallet/core/src/utxo/outgoing.rs rename wallet/core/src/{runtime => utxo}/sync.rs (96%) create mode 100644 wallet/core/src/utxo/test.rs create mode 100644 wallet/core/src/wallet/api.rs create mode 100644 wallet/core/src/wallet/args.rs rename wallet/core/src/{runtime => wallet}/maps.rs (92%) create mode 100644 wallet/core/src/wallet/mod.rs create mode 100644 wallet/macros/Cargo.toml create mode 100644 wallet/macros/src/handler.rs create mode 100644 wallet/macros/src/lib.rs create mode 100644 wallet/macros/src/wallet/client.rs create mode 100644 wallet/macros/src/wallet/mod.rs create mode 100644 wallet/macros/src/wallet/server.rs create mode 100644 wallet/macros/src/wallet/wasm.rs diff --git a/Cargo.lock b/Cargo.lock index 936b1c30a..056599f88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.4.8" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom 0.2.11", + "once_cell", + "version_check", +] [[package]] name = "ahash" @@ -51,7 +56,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if 1.0.0", - "getrandom 0.2.10", + "getrandom 0.2.11", "once_cell", "version_check", "zerocopy", @@ -109,9 +114,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ "anstyle", "anstyle-parse", @@ -129,30 +134,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -223,12 +228,12 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336d835910fab747186c56586562cb46f42809c2843ef3a84f47509009522838" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", - "event-listener 3.0.1", + "event-listener 4.0.0", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -236,30 +241,30 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ - "async-lock", + "async-lock 3.2.0", "async-task", "concurrent-queue", "fastrand 2.0.1", - "futures-lite", + "futures-lite 2.1.0", "slab", ] [[package]] name = "async-global-executor" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 1.9.0", + "async-channel 2.1.1", "async-executor", - "async-io", - "async-lock", + "async-io 2.2.2", + "async-lock 3.2.0", "blocking", - "futures-lite", + "futures-lite 2.1.0", "once_cell", ] @@ -269,20 +274,39 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ - "async-lock", + "async-lock 2.8.0", "autocfg", "cfg-if 1.0.0", "concurrent-queue", - "futures-lite", + "futures-lite 1.13.0", "log", "parking", - "polling", + "polling 2.8.0", "rustix 0.37.27", "slab", "socket2 0.4.10", "waker-fn", ] +[[package]] +name = "async-io" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afaa937395a620e33dc6a742c593c01aced20aa376ffb0f628121198578ccc7" +dependencies = [ + "async-lock 3.2.0", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-io", + "futures-lite 2.1.0", + "parking", + "polling 3.3.1", + "rustix 0.38.28", + "slab", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "async-lock" version = "2.8.0" @@ -292,6 +316,17 @@ dependencies = [ "event-listener 2.5.3", ] +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.0", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-std" version = "1.12.0" @@ -301,13 +336,13 @@ dependencies = [ "async-attributes", "async-channel 1.9.0", "async-global-executor", - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite", + "futures-lite 1.13.0", "gloo-timers", "kv-log-macro", "log", @@ -338,14 +373,14 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "async-task" -version = "4.5.0" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" +checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" [[package]] name = "async-trait" @@ -355,14 +390,14 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "atomic-polyfill" -version = "0.1.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" dependencies = [ "critical-section", ] @@ -535,7 +570,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -581,35 +616,35 @@ dependencies = [ [[package]] name = "blocking" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel 1.9.0", - "async-lock", + "async-channel 2.1.1", + "async-lock 3.2.0", "async-task", "fastrand 2.0.1", "futures-io", - "futures-lite", + "futures-lite 2.1.0", "piper", "tracing", ] [[package]] name = "borsh" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dda7dc709193c0d86a1a51050a926dc3df1cf262ec46a23a25dba421ea1924" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" dependencies = [ "borsh-derive", - "hashbrown 0.9.1", + "hashbrown 0.11.2", ] [[package]] name = "borsh-derive" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684155372435f578c0fa1acd13ebbb182cc19d6b38b64ae7901da4393217d264" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", @@ -620,9 +655,9 @@ dependencies = [ [[package]] name = "borsh-derive-internal" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2102f62f8b6d3edeab871830782285b64cc1830168094db05c8e458f209bc5c3" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" dependencies = [ "proc-macro2", "quote", @@ -631,9 +666,9 @@ dependencies = [ [[package]] name = "borsh-schema-derive-internal" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196c978c4c9b0b142d446ef3240690bf5a8a33497074a113ff9a337ccb750483" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" dependencies = [ "proc-macro2", "quote", @@ -841,9 +876,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.7" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", @@ -851,9 +886,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.7" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstream", "anstyle", @@ -870,7 +905,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -887,9 +922,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] @@ -929,6 +964,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + [[package]] name = "convert_case" version = "0.6.0" @@ -940,9 +981,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -950,9 +991,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -981,7 +1022,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.7", + "clap 4.4.11", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1014,9 +1055,9 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -1024,9 +1065,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", @@ -1035,22 +1076,21 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" dependencies = [ "autocfg", "cfg-if 1.0.0", "crossbeam-utils", "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" dependencies = [ "cfg-if 1.0.0", ] @@ -1108,7 +1148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if 1.0.0", - "hashbrown 0.14.2", + "hashbrown 0.14.3", "lock_api", "once_cell", "parking_lot_core", @@ -1116,9 +1156,18 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", +] [[package]] name = "derivative" @@ -1152,9 +1201,9 @@ checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" [[package]] name = "deunicode" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1abaf4d861455be59f64fd2b55606cb151fce304ede7165f410243ce96bde6" +checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a" [[package]] name = "dhat" @@ -1278,12 +1327,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1294,9 +1343,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "3.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" dependencies = [ "concurrent-queue", "parking", @@ -1305,11 +1354,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 3.0.1", + "event-listener 4.0.0", "pin-project-lite", ] @@ -1352,12 +1401,33 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedstr" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f9274e2c5ec70bcbd4b7c181ebfcfdff70f23e480cfa0d446efc1033c7200e" +dependencies = [ + "serde", +] + [[package]] name = "flate2" version = "1.0.28" @@ -1391,9 +1461,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1461,6 +1531,19 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-lite" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.29" @@ -1469,7 +1552,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -1534,9 +1617,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1547,9 +1630,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -1571,9 +1654,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -1581,7 +1664,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1605,11 +1688,11 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash 0.4.8", + "ahash 0.7.7", ] [[package]] @@ -1620,15 +1703,15 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heapless" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", "hash32", @@ -1685,18 +1768,18 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1705,9 +1788,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1740,9 +1823,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1755,7 +1838,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1799,9 +1882,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1843,7 +1926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.3", ] [[package]] @@ -1914,7 +1997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.3", - "rustix 0.38.21", + "rustix 0.38.28", "windows-sys 0.48.0", ] @@ -1938,9 +2021,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" @@ -2003,9 +2086,10 @@ dependencies = [ name = "kash-bip32" version = "0.13.1" dependencies = [ + "borsh", "bs58", "faster-hex 0.6.1", - "getrandom 0.2.10", + "getrandom 0.2.11", "hmac", "js-sys", "kash-utils", @@ -2015,6 +2099,7 @@ dependencies = [ "rand_core 0.6.4", "ripemd", "secp256k1", + "serde", "sha2", "subtle", "thiserror", @@ -2041,6 +2126,7 @@ dependencies = [ "kash-consensus-core", "kash-core", "kash-daemon", + "kash-metrics-core", "kash-rpc-core", "kash-utils", "kash-wallet-core", @@ -2087,7 +2173,7 @@ dependencies = [ name = "kash-consensus" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "bincode", "criterion", "crossbeam-channel", @@ -2136,7 +2222,7 @@ dependencies = [ "criterion", "faster-hex 0.6.1", "futures-util", - "getrandom 0.2.10", + "getrandom 0.2.11", "itertools 0.11.0", "js-sys", "kash-addresses", @@ -2166,7 +2252,7 @@ dependencies = [ name = "kash-consensus-notify" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "cfg-if 1.0.0", "derive_more", "futures", @@ -2287,7 +2373,7 @@ dependencies = [ name = "kash-grpc-client" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-stream", "async-trait", "faster-hex 0.6.1", @@ -2315,7 +2401,7 @@ dependencies = [ name = "kash-grpc-core" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-stream", "async-trait", "faster-hex 0.6.1", @@ -2344,7 +2430,7 @@ dependencies = [ name = "kash-grpc-server" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-stream", "async-trait", "faster-hex 0.6.1", @@ -2372,7 +2458,7 @@ dependencies = [ "tokio-stream", "tonic", "triggered", - "uuid 1.5.0", + "uuid 1.6.1", ] [[package]] @@ -2400,7 +2486,7 @@ dependencies = [ name = "kash-index-core" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-trait", "derive_more", "futures", @@ -2419,7 +2505,7 @@ dependencies = [ name = "kash-index-processor" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-trait", "derive_more", "futures", @@ -2471,6 +2557,22 @@ dependencies = [ "kash-hashes", ] +[[package]] +name = "kash-metrics-core" +version = "0.13.1" +dependencies = [ + "async-trait", + "borsh", + "futures", + "kash-core", + "kash-rpc-core", + "separator", + "serde", + "thiserror", + "workflow-core", + "workflow-log", +] + [[package]] name = "kash-mining" version = "0.13.1" @@ -2522,7 +2624,7 @@ dependencies = [ name = "kash-notify" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-trait", "borsh", "derive_more", @@ -2561,6 +2663,7 @@ dependencies = [ "kash-consensus-core", "kash-core", "kash-daemon", + "kash-metrics-core", "kash-rpc-core", "kash-wallet-core", "nw-sys", @@ -2607,7 +2710,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", - "uuid 1.5.0", + "uuid 1.6.1", ] [[package]] @@ -2637,7 +2740,7 @@ dependencies = [ "tokio-stream", "tonic", "tonic-build", - "uuid 1.5.0", + "uuid 1.6.1", ] [[package]] @@ -2670,7 +2773,7 @@ dependencies = [ name = "kash-rpc-core" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-trait", "borsh", "derive_more", @@ -2695,7 +2798,7 @@ dependencies = [ "serde_json", "smallvec", "thiserror", - "uuid 1.5.0", + "uuid 1.6.1", "wasm-bindgen", "workflow-core", "workflow-wasm", @@ -2737,16 +2840,16 @@ dependencies = [ "kash-utils", "kash-utils-tower", "kash-utxoindex", - "kash-wrpc-core", "log", "tokio", + "workflow-rpc", ] [[package]] name = "kash-testing-integration" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "bincode", "criterion", "crossbeam-channel", @@ -2832,7 +2935,7 @@ dependencies = [ name = "kash-utils" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-trait", "bincode", "borsh", @@ -2852,7 +2955,7 @@ dependencies = [ "thiserror", "tokio", "triggered", - "uuid 1.5.0", + "uuid 1.6.1", ] [[package]] @@ -2921,7 +3024,9 @@ name = "kash-wallet-core" version = "0.13.1" dependencies = [ "aes", + "ahash 0.8.6", "argon2", + "async-channel 2.1.1", "async-std", "async-trait", "base64 0.21.5", @@ -2934,6 +3039,7 @@ dependencies = [ "downcast", "evpkdf", "faster-hex 0.6.1", + "fixedstr", "futures", "heapless", "hmac", @@ -2951,6 +3057,7 @@ dependencies = [ "kash-txscript", "kash-txscript-errors", "kash-utils", + "kash-wallet-macros", "kash-wrpc-client", "md-5", "pad", @@ -2982,6 +3089,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kash-wallet-macros" +version = "0.13.1" +dependencies = [ + "convert_case 0.5.0", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "xxhash-rust", +] + [[package]] name = "kash-wasm" version = "0.13.1" @@ -3032,21 +3152,16 @@ dependencies = [ "workflow-wasm", ] -[[package]] -name = "kash-wrpc-core" -version = "0.13.1" - [[package]] name = "kash-wrpc-proxy" version = "0.13.1" dependencies = [ "async-trait", - "clap 4.4.7", + "clap 4.4.11", "kash-consensus-core", "kash-grpc-client", "kash-rpc-core", "kash-rpc-macros", - "kash-wrpc-core", "kash-wrpc-server", "num_cpus", "thiserror", @@ -3071,7 +3186,6 @@ dependencies = [ "kash-rpc-macros", "kash-rpc-service", "kash-utils", - "kash-wrpc-core", "log", "num_cpus", "openssl", @@ -3095,8 +3209,8 @@ dependencies = [ name = "kashd" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", - "clap 4.4.7", + "async-channel 2.1.1", + "clap 4.4.11", "dhat", "dirs", "futures-util", @@ -3163,9 +3277,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libloading" @@ -3183,6 +3297,17 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "librocksdb-sys" version = "0.11.0+8.1.1" @@ -3244,9 +3369,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "local-ip-address" @@ -3435,9 +3560,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", @@ -3676,6 +3801,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "numtoa" version = "0.2.4" @@ -3706,9 +3840,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" @@ -3724,9 +3858,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" dependencies = [ "bitflags 2.4.1", "cfg-if 1.0.0", @@ -3745,7 +3879,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3756,18 +3890,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.6+3.1.4" +version = "300.2.1+3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" dependencies = [ "cc", "libc", @@ -3824,7 +3958,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets 0.48.5", ] @@ -3846,7 +3980,7 @@ checksum = "70df726c43c645ef1dde24c7ae14692036ebe5457c92c5f0ec4cfceb99634ff6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3884,9 +4018,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -3915,7 +4049,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -3963,6 +4097,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "polling" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +dependencies = [ + "cfg-if 1.0.0", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.28", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -3976,9 +4124,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -3993,7 +4147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -4030,18 +4184,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", "prost-derive", @@ -4049,9 +4203,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck", @@ -4064,29 +4218,29 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.38", + "syn 2.0.41", "tempfile", "which", ] [[package]] name = "prost-derive" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "prost-types" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ "prost", ] @@ -4159,7 +4313,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] @@ -4216,15 +4370,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -4236,12 +4381,12 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ - "getrandom 0.2.10", - "redox_syscall 0.2.16", + "getrandom 0.2.11", + "libredox", "thiserror", ] @@ -4276,12 +4421,12 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ring" -version = "0.17.5" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", - "getrandom 0.2.10", + "getrandom 0.2.11", "libc", "spin", "untrusted", @@ -4320,7 +4465,7 @@ dependencies = [ name = "rothschild" version = "0.13.1" dependencies = [ - "clap 4.4.7", + "clap 4.4.11", "faster-hex 0.6.1", "itertools 0.11.0", "kash-addresses", @@ -4372,22 +4517,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.10", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring", @@ -4397,9 +4542,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64 0.21.5", ] @@ -4422,9 +4567,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "same-file" @@ -4532,9 +4677,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.190" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -4551,9 +4696,9 @@ dependencies = [ [[package]] name = "serde-wasm-bindgen" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ba92964781421b6cef36bf0d7da26d201e96d84e1b10e7ae6ed416e516906d" +checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132" dependencies = [ "js-sys", "serde", @@ -4562,13 +4707,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -4666,8 +4811,8 @@ dependencies = [ name = "simpa" version = "0.13.1" dependencies = [ - "async-channel 2.0.0", - "clap 4.4.7", + "async-channel 2.1.1", + "clap 4.4.11", "dhat", "futures", "futures-util", @@ -4711,9 +4856,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" dependencies = [ "serde", ] @@ -4817,9 +4962,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" dependencies = [ "proc-macro2", "quote", @@ -4850,16 +4995,16 @@ checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.1", - "redox_syscall 0.4.1", - "rustix 0.38.21", + "redox_syscall", + "rustix 0.38.28", "windows-sys 0.48.0", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -4886,22 +5031,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -4920,6 +5065,37 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -4947,9 +5123,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ "backtrace", "bytes", @@ -4975,13 +5151,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5093,7 +5269,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5166,7 +5342,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] @@ -5186,9 +5362,9 @@ checksum = "ce148eae0d1a376c1b94ae651fc3261d9cb8294788b962b7382066376503a2d1" [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" @@ -5227,9 +5403,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -5291,9 +5467,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -5318,16 +5494,16 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", ] [[package]] name = "uuid" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.11", "rand 0.8.5", "serde", ] @@ -5350,6 +5526,18 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "vergen" +version = "8.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1290fd64cc4e7d3c9b07d7f333ce0ce0007253e32870e632624835cc80b83939" +dependencies = [ + "anyhow", + "rustc_version", + "rustversion", + "time", +] + [[package]] name = "version_check" version = "0.9.4" @@ -5416,7 +5604,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", "wasm-bindgen-shared", ] @@ -5450,7 +5638,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5504,7 +5692,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.21", + "rustix 0.38.28", ] [[package]] @@ -5565,6 +5753,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -5595,6 +5792,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5607,6 +5819,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -5619,6 +5837,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5631,6 +5855,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5643,6 +5873,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5655,6 +5891,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +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" @@ -5667,6 +5909,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5679,6 +5927,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "workflow-async-trait" version = "0.1.68" @@ -5692,9 +5946,9 @@ dependencies = [ [[package]] name = "workflow-chrome" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808d0e743c97552ee0ed5150007cae8c15bb507e9d58a0b1323a8be09c92ad9a" +checksum = "21ebac6262dc1583565f97b4e3c87191b388c39440a7fdee1bb763a813f80837" dependencies = [ "cfg-if 1.0.0", "chrome-sys", @@ -5707,19 +5961,20 @@ dependencies = [ [[package]] name = "workflow-core" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d960ca704f3ea26e696c371df88091ec188cba86b31b823f63326ecf9173b7" +checksum = "4b47e41ad98580f80ddafd2020c18810731e34da77ac16347982772f15d9ba18" dependencies = [ - "async-channel 2.0.0", + "async-channel 2.1.1", "async-std", "borsh", "bs58", "cfg-if 1.0.0", + "chrono", "dirs", "faster-hex 0.8.1", "futures", - "getrandom 0.2.10", + "getrandom 0.2.11", "instant", "js-sys", "rand 0.8.5", @@ -5728,6 +5983,7 @@ dependencies = [ "thiserror", "tokio", "triggered", + "vergen", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -5737,9 +5993,9 @@ dependencies = [ [[package]] name = "workflow-core-macros" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b743709438413cf5b9c187a163ef6f73a55920947a335d3367f2127fbe1603ba" +checksum = "ab418ecc3a4578d3d68820d4b4e5c35917b19be98fc0e7b1ed79b7225e75384f" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -5754,9 +6010,9 @@ dependencies = [ [[package]] name = "workflow-d3" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a1c33d6966850dcbe16b7a63d64751086a9e5b811aea183993bf812843b94f" +checksum = "1aec0ecd0f3cb188d8e80e8bbd3f3d4c9e0e4d3e643f250bd83ab6a73a87e17e" dependencies = [ "atomic_float", "js-sys", @@ -5771,9 +6027,9 @@ dependencies = [ [[package]] name = "workflow-dom" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3283462c546148d47f25633bfe2271135b3d2b8a8bf9e5fee4d91b4e5a92b2" +checksum = "18c01a4c1d4c72b04ad470c83433d52cc85e5801b681b4773254b53ba615c614" dependencies = [ "futures", "js-sys", @@ -5789,9 +6045,9 @@ dependencies = [ [[package]] name = "workflow-log" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e3027bd920784edea24f96213728bac8e19bc94278cad81fd4518ebf9cba12" +checksum = "d82efcf0da005b982fc3b811175b7960aef286c1ab83edbc90fc2864c0da4aa2" dependencies = [ "cfg-if 1.0.0", "console", @@ -5805,9 +6061,9 @@ dependencies = [ [[package]] name = "workflow-macro-tools" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553fcac6e1900600adf3137a621c200117f6f7a81c91b489634c778cfd9fc438" +checksum = "4b5a3578b59ffd5eec99e4ff886d5e2bb6989d06068b0db2aa6611ec3b1dcebd" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -5818,9 +6074,9 @@ dependencies = [ [[package]] name = "workflow-node" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a5ef29b1200a4076be3032a54891cbd8267a45a52469e0faae30f11a763da6" +checksum = "1ce39ed278dd2095ac0549f7d8817d9c786f0370c112d553a91a9ee4c56feb18" dependencies = [ "borsh", "futures", @@ -5839,9 +6095,9 @@ dependencies = [ [[package]] name = "workflow-nw" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d35104cac52de5e9353cf3970329b5620e165ce319de2813ed62360023b632b2" +checksum = "ad8d13f534b29db4c946fa135b850175a8c04176e0a266cf265548e32936d094" dependencies = [ "ahash 0.8.6", "async-trait", @@ -5863,9 +6119,9 @@ dependencies = [ [[package]] name = "workflow-panic-hook" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ac55687ef271f5b7e805170e6429c261a97c114d5b14d9fb153054c129d22a" +checksum = "8cd36afe3c94147bb6777003b9dc5452afc5d25d02681922563080e76030e667" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen", @@ -5888,9 +6144,9 @@ dependencies = [ [[package]] name = "workflow-rpc" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f92426a186e5ef112afa35bf988c884b6f34fbf23665054fc7da54b0a125ee2" +checksum = "1534cc0e6e3e7ebd3a6a6adc555de2127f70c432567dc787d6c16dc7ce2a6d2e" dependencies = [ "ahash 0.8.6", "async-std", @@ -5899,7 +6155,7 @@ dependencies = [ "downcast-rs", "futures", "futures-util", - "getrandom 0.2.10", + "getrandom 0.2.11", "manual_future", "rand 0.8.5", "serde", @@ -5917,9 +6173,9 @@ dependencies = [ [[package]] name = "workflow-rpc-macros" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f904fda04b32be54f132a69634be79898da0d944dbe07792a00087c6ef8069d" +checksum = "21c82002491ec14cc23bb14f7bb179617e8dbb8a738532477fa843d3cb51cd95" dependencies = [ "parse-variants", "proc-macro-error", @@ -5930,15 +6186,16 @@ dependencies = [ [[package]] name = "workflow-store" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7357def5065ee1e925f48af268ba90c832176d5d94d0c9f1f5682dd1b9efbc" +checksum = "2cb49681ba575966c19f5684e5b2908a5f7b8678cb20644bcb51396d66ad6eb3" dependencies = [ "async-std", "base64 0.21.5", "cfg-if 1.0.0", "chrome-sys", "faster-hex 0.8.1", + "filetime", "home", "js-sys", "lazy_static", @@ -5957,9 +6214,9 @@ dependencies = [ [[package]] name = "workflow-task" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37cb53d40377228b9beed840a5edfbd73b6117f17024908711431058e89df4af" +checksum = "b84fbeb7f8b036295a11fed4665c514fb55aea0b721fb8d9d933f88238d1e0c0" dependencies = [ "futures", "thiserror", @@ -5969,9 +6226,9 @@ dependencies = [ [[package]] name = "workflow-task-macros" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55bc7451b834e1c2f0ce5eafa0b9a714217bf9ebfc58849979bc9515f18a423f" +checksum = "5086f968f664b6bdcd37a293b4baa6914b752feded93679177b16a863c111fcc" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -5985,9 +6242,9 @@ dependencies = [ [[package]] name = "workflow-terminal" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41047d47b5a24cbadc7cf623bf4488964207216b3f1c3be6a163639bfec8b7ce" +checksum = "c6f1977765e333b303166a60322833078583f6fd32b0c6251c54776b59b980d2" dependencies = [ "async-std", "async-trait", @@ -6014,9 +6271,9 @@ dependencies = [ [[package]] name = "workflow-terminal-macros" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9caa3dc058ae138d5a01c3645b1478c020b7e5bef6f482f1f45a9a767b222e33" +checksum = "da768384e9a5e1fe6a8f2b6389b58d500954f8c2ff8cce8622c3947388593601" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -6030,9 +6287,9 @@ dependencies = [ [[package]] name = "workflow-wasm" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2d3d443c72e1025ae82c3c8d46cc45988652a37ba3f74406ae78231353ac23" +checksum = "b099355ef033f582eb6884425b354dadf010bb8327e7269090bf804703f0f858" dependencies = [ "cfg-if 1.0.0", "faster-hex 0.8.1", @@ -6051,9 +6308,9 @@ dependencies = [ [[package]] name = "workflow-wasm-macros" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc012f660e74cc464d7f86429c5012777f1c8967c3042539fc6b8b70358bed4" +checksum = "5ef6486895e291567e1ac8d17a5948ed0676f0ffb304eaf7f5391ad102149cc0" dependencies = [ "js-sys", "proc-macro-error", @@ -6065,12 +6322,12 @@ dependencies = [ [[package]] name = "workflow-websocket" -version = "0.8.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd17b12dbbe351ae9c0a602614dbfdfa1f86ee916fbb661a92e901f5ae9544c" +checksum = "682c3315971aeefd9dc3a0a261355c14157192c1c8716468ab61bff7f4803d5d" dependencies = [ "ahash 0.8.6", - "async-channel 2.0.0", + "async-channel 2.1.1", "async-std", "async-trait", "cfg-if 1.0.0", @@ -6124,29 +6381,29 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.20" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd66a62464e3ffd4e37bd09950c2b9dd6c4f8767380fabba0d523f9a775bc85a" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.20" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255c4596d41e6916ced49cfafea18727b24d67878fa180ddfd69b9df34fd1726" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.41", ] [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zstd-sys" diff --git a/Cargo.toml b/Cargo.toml index e663219df..9d57c63d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "daemon", "cli", "core", + "wallet/macros", "wallet/core", "wallet/native", "wallet/wasm", @@ -32,7 +33,6 @@ members = [ "rpc/grpc/core", "rpc/grpc/client", "rpc/grpc/server", - "rpc/wrpc/core", "rpc/wrpc/server", "rpc/wrpc/client", "rpc/wrpc/proxy", @@ -52,6 +52,7 @@ members = [ "utils/tower", "rothschild", "metrics/perf_monitor", + "metrics/core", ] [workspace.package] @@ -73,11 +74,10 @@ include = [ [workspace.dependencies] # kash-testing-integration = { version = "0.13.1", path = "testing/integration" } -kash-os = { version = "0.13.1", path = "kash-os" } -kash-daemon = { version = "0.13.1", path = "daemon" } kash-addresses = { version = "0.13.1", path = "crypto/addresses" } kash-addressmanager = { version = "0.13.1", path = "components/addressmanager" } kash-bip32 = { version = "0.13.1", path = "wallet/bip32" } +kash-cli = { version = "0.13.1", path = "cli" } kash-connectionmanager = { version = "0.13.1", path = "components/connectionmanager" } kash-consensus = { version = "0.13.1", path = "consensus" } kash-consensus-core = { version = "0.13.1", path = "consensus/core" } @@ -85,6 +85,7 @@ kash-consensus-notify = { version = "0.13.1", path = "consensus/notify" } kash-consensus-wasm = { version = "0.13.1", path = "consensus/wasm" } kash-consensusmanager = { version = "0.13.1", path = "components/consensusmanager" } kash-core = { version = "0.13.1", path = "core" } +kash-daemon = { version = "0.13.1", path = "daemon" } kash-database = { version = "0.13.1", path = "database" } kash-grpc-client = { version = "0.13.1", path = "rpc/grpc/client" } kash-grpc-core = { version = "0.13.1", path = "rpc/grpc/core" } @@ -94,12 +95,15 @@ kash-index-core = { version = "0.13.1", path = "indexes/core" } kash-index-processor = { version = "0.13.1", path = "indexes/processor" } kash-math = { version = "0.13.1", path = "math" } kash-merkle = { version = "0.13.1", path = "crypto/merkle" } +kash-metrics-core = { version = "0.13.0", path = "metrics/core" } kash-mining = { version = "0.13.1", path = "mining" } kash-mining-errors = { version = "0.13.1", path = "mining/errors" } kash-muhash = { version = "0.13.1", path = "crypto/muhash" } kash-notify = { version = "0.13.1", path = "notify" } +kash-os = { version = "0.13.1", path = "kash-os" } kash-p2p-flows = { version = "0.13.1", path = "protocol/flows" } kash-p2p-lib = { version = "0.13.1", path = "protocol/p2p" } +kash-perf-monitor = { version = "0.13.1", path = "metrics/perf_monitor" } kash-pow = { version = "0.13.1", path = "consensus/pow" } kash-rpc-core = { version = "0.13.1", path = "rpc/core" } kash-rpc-macros = { version = "0.13.1", path = "rpc/macros" } @@ -110,20 +114,20 @@ kash-utils = { version = "0.13.1", path = "utils" } kash-utils-tower = { version = "0.13.1", path = "utils/tower" } kash-utxoindex = { version = "0.13.1", path = "indexes/utxoindex" } kash-wallet = { version = "0.13.1", path = "wallet/native" } -kash-cli = { version = "0.13.1", path = "cli" } kash-wallet-cli-wasm = { version = "0.13.1", path = "wallet/wasm" } kash-wallet-core = { version = "0.13.1", path = "wallet/core" } +kash-wallet-macros = { version = "0.13.0", path = "wallet/macros" } kash-wasm = { version = "0.13.1", path = "wasm" } -kash-wrpc-core = { version = "0.13.1", path = "rpc/wrpc/core" } kash-wrpc-client = { version = "0.13.1", path = "rpc/wrpc/client" } +kash-wrpc-core = { version = "0.13.1", path = "rpc/wrpc/core" } kash-wrpc-proxy = { version = "0.13.1", path = "rpc/wrpc/proxy" } kash-wrpc-server = { version = "0.13.1", path = "rpc/wrpc/server" } kash-wrpc-wasm = { version = "0.13.1", path = "rpc/wrpc/wasm" } kashd = { version = "0.13.1", path = "kashd" } -kash-perf-monitor = { version = "0.13.1", path = "metrics/perf_monitor" } # external aes = "0.8.3" +ahash = "0.8.6" argon2 = "0.5.2" async-channel = "2.0.0" async-std = { version = "1.12.0", features = ['attributes'] } @@ -155,11 +159,10 @@ enum-primitive-derive = "0.2.2" event-listener = "2.5.3" # TODO "3.0.1" evpkdf = "0.2.0" faster-hex = "0.6.1" # TODO "0.8.1" - fails unit tests +fixedstr = { version = "0.5.4", features = ["serde"] } flate2 = "1.0.28" futures = { version = "0.3.29" } -futures-util = { version = "0.3.29", default-features = false, features = [ - "alloc", -] } +futures-util = { version = "0.3.29", default-features = false, features = [ "alloc", ] } getrandom = { version = "0.2.10", features = ["js"] } h2 = "0.3.21" heapless = "0.7.16" @@ -243,16 +246,16 @@ hyper = "0.14.27" workflow-perf-monitor = { version = "0.0.2" } # workflow dependencies -workflow-d3 = { version = "0.8.1" } -workflow-nw = { version = "0.8.1" } -workflow-log = { version = "0.8.1" } -workflow-core = { version = "0.8.1" } -workflow-wasm = { version = "0.8.1" } -workflow-dom = { version = "0.8.1" } -workflow-rpc = { version = "0.8.1" } -workflow-node = { version = "0.8.1" } -workflow-store = { version = "0.8.1" } -workflow-terminal = { version = "0.8.1" } +workflow-d3 = { version = "0.10.2" } +workflow-nw = { version = "0.10.2" } +workflow-log = { version = "0.10.2" } +workflow-core = { version = "0.10.2" } +workflow-wasm = { version = "0.10.2" } +workflow-dom = { version = "0.10.2" } +workflow-rpc = { version = "0.10.2" } +workflow-node = { version = "0.10.2" } +workflow-store = { version = "0.10.2" } +workflow-terminal = { version = "0.10.2" } nw-sys = "0.1.6" # if below is enabled, this means that there is an ongoing work @@ -268,6 +271,17 @@ nw-sys = "0.1.6" # workflow-node = { path = "../workflow-rs/node" } # workflow-store = { path = "../workflow-rs/store" } # workflow-terminal = { path = "../workflow-rs/terminal" } +# --- +# workflow-d3 = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-nw = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-log = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-core = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-wasm = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-dom = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-rpc = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-node = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-store = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } +# workflow-terminal = { git = "https://github.com/workflow-rs/workflow-rs.git", branch = "master" } # https://github.com/aspectron/nw-sys # nw-sys = { path = "../nw-sys" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 85bc25f29..c3ff1f2e6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -36,6 +36,7 @@ kash-bip32.workspace = true kash-consensus-core.workspace = true kash-core.workspace = true kash-daemon.workspace = true +kash-metrics-core.workspace = true kash-rpc-core.workspace = true kash-utils.workspace = true kash-wallet-core.workspace = true diff --git a/cli/src/cli.rs b/cli/src/cli.rs index efadb3a8a..4f6100cea 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,16 +1,13 @@ use crate::error::Error; use crate::helpers::*; use crate::imports::*; -pub use crate::metrics; use crate::modules::miner::Miner; use crate::modules::node::Node; use crate::notifier::{Notification, Notifier}; use crate::result::Result; use kash_daemon::{DaemonEvent, DaemonKind, Daemons}; use kash_wallet_core::rpc::DynRpcApi; -use kash_wallet_core::runtime::{Account, BalanceStrings}; use kash_wallet_core::storage::{IdT, PrvKeyDataInfo}; -use kash_wallet_core::{runtime::Wallet, Events}; use kash_wrpc_client::KashRpcClient; use workflow_core::channel::*; use workflow_core::time::Instant; @@ -192,6 +189,7 @@ impl KashCli { } pub fn register_metrics(self: &Arc) -> Result<()> { + use crate::modules::metrics; register_handlers!(self, self.handlers(), [metrics]); Ok(()) } @@ -282,6 +280,7 @@ impl KashCli { if let Ok(msg) = msg { match *msg { + Events::Error { message } => { terrorln!(this,"{message}"); }, Events::UtxoProcStart => {}, Events::UtxoProcStop => {}, Events::UtxoProcError { message } => { @@ -336,95 +335,117 @@ impl KashCli { }, Events::AccountSelection { .. } => { }, + Events::WalletCreate { .. } => { }, Events::WalletError { .. } => { }, - Events::WalletOpen | - Events::WalletReload => { - - // load all accounts - if let Err(err) = this.wallet().activate_all_stored_accounts().await { - terrorln!(this, "{err}"); - } + // Events::WalletReady { .. } => { }, + Events::WalletOpen { .. } | + Events::WalletReload { .. } => { }, + Events::WalletClose => { + this.term().refresh_prompt(); + }, + Events::PrvKeyDataCreate { .. } => { }, + Events::AccountDeactivation { .. } => { }, + Events::AccountActivation { .. } => { // list all accounts this.list().await.unwrap_or_else(|err|terrorln!(this, "{err}")); // load default account if only one account exists this.wallet().autoselect_default_account_if_single().await.ok(); this.term().refresh_prompt(); - - }, - Events::WalletClose => { - this.term().refresh_prompt(); }, + Events::AccountCreate { .. } => { }, + Events::AccountUpdate { .. } => { }, Events::DAAScoreChange { current_daa_score } => { if this.is_mutted() && this.flags.get(Track::Daa) { tprintln!(this, "{NOTIFY} DAA: {current_daa_score}"); } }, + Events::Discovery { .. } => { } Events::Reorg { record } => { if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) { let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_with_state(&this.wallet,Some("reorg"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("reorg"),include_utxos).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, - Events::External { + Events::Stasis { record } => { - if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) { + // Pending and coinbase stasis fall under the same `Track` category + if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) { let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_with_state(&this.wallet,Some("external"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("stasis"),include_utxos).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, + // Events::External { + // record + // } => { + // if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) { + // let include_utxos = this.flags.get(Track::Utxo); + // let tx = record.format_with_state(&this.wallet,Some("external"),include_utxos).await; + // tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); + // } + // }, Events::Pending { - record, is_outgoing : _ + record } => { if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Pending)) { let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_with_state(&this.wallet,Some("pending"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("pending"),include_utxos).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, Events::Maturity { - record, is_outgoing : _ - } => { - if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) { - let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_with_state(&this.wallet,Some("confirmed"),include_utxos).await; - tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); - } - }, - Events::Outgoing { record } => { if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) { let include_utxos = this.flags.get(Track::Utxo); - let tx = record.format_with_state(&this.wallet,Some("confirmed"),include_utxos).await; + let tx = record.format_transaction_with_state(&this.wallet,Some("confirmed"),include_utxos).await; tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); } }, + // Events::Outgoing { + // record + // } => { + // if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) { + // let include_utxos = this.flags.get(Track::Utxo); + // let tx = record.format_with_state(&this.wallet,Some("confirmed"),include_utxos).await; + // tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); + // } + // }, + // Events::Change { + // record + // } => { + // if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Tx)) { + // let include_utxos = this.flags.get(Track::Utxo); + // let tx = record.format_with_state(&this.wallet,Some("change"),include_utxos).await; + // tx.iter().for_each(|line|tprintln!(this,"{NOTIFY} {line}")); + // } + // }, Events::Balance { balance, id, - mature_utxo_size, - pending_utxo_size, } => { if !this.is_mutted() || (this.is_mutted() && this.flags.get(Track::Balance)) { let network_id = this.wallet.network_id().expect("missing network type"); let network_type = NetworkType::from(network_id); - let balance = BalanceStrings::from((&balance,&network_type, None)); + let balance_strings = BalanceStrings::from((&balance,&network_type, None)); let id = id.short(); - let pending_utxo_info = if pending_utxo_size > 0 { - format!("({pending_utxo_size} pending)") + let mature_utxo_count = balance.as_ref().map(|balance|balance.mature_utxo_count.separated_string()).unwrap_or("N/A".to_string()); + let pending_utxo_count = balance.as_ref().map(|balance|balance.pending_utxo_count).unwrap_or(0); + + let pending_utxo_info = if pending_utxo_count > 0 { + format!("({} pending)", pending_utxo_count) } else { "".to_string() }; - let utxo_info = style(format!("{} UTXOs {pending_utxo_info}", mature_utxo_size.separated_string())).dim(); + let utxo_info = style(format!("{mature_utxo_count} UTXOs {pending_utxo_info}")).dim(); - tprintln!(this, "{NOTIFY} {} {id}: {balance} {utxo_info}",style("balance".pad_to_width(8)).blue()); + tprintln!(this, "{NOTIFY} {} {id}: {balance_strings} {utxo_info}",style("balance".pad_to_width(8)).blue()); } this.term().refresh_prompt(); @@ -757,9 +778,10 @@ impl Cli for KashCli { } } - if let Some(name) = self.wallet.name() { - if name != "kash" { - prompt.push(name); + if let Some(descriptor) = self.wallet.descriptor() { + let title = descriptor.title.unwrap_or(descriptor.filename); + if title.to_lowercase().as_str() != "kash" { + prompt.push(title); } if let Ok(account) = self.wallet.account() { diff --git a/cli/src/error.rs b/cli/src/error.rs index 7768c2f48..8553d31fb 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -116,6 +116,9 @@ pub enum Error { #[error("private key {0} already exists")] PrivateKeyAlreadyExists(String), + + #[error(transparent)] + MetricsError(kash_metrics_core::error::Error), } impl Error { diff --git a/cli/src/extensions/mod.rs b/cli/src/extensions/mod.rs new file mode 100644 index 000000000..43f43f2c3 --- /dev/null +++ b/cli/src/extensions/mod.rs @@ -0,0 +1,2 @@ +pub mod transaction; +pub use transaction::*; diff --git a/cli/src/extensions/transaction.rs b/cli/src/extensions/transaction.rs new file mode 100644 index 000000000..dffe1732e --- /dev/null +++ b/cli/src/extensions/transaction.rs @@ -0,0 +1,180 @@ +use crate::imports::*; +use kash_consensus_core::tx::{TransactionInput, TransactionOutpoint}; +use kash_wallet_core::storage::Binding; +use kash_wallet_core::storage::{TransactionData, TransactionKind, TransactionRecord}; +use workflow_log::style; + +pub trait TransactionTypeExtension { + fn style(&self, s: &str) -> String; + fn style_with_sign(&self, s: &str, history: bool) -> String; +} + +impl TransactionTypeExtension for TransactionKind { + fn style(&self, s: &str) -> String { + match self { + TransactionKind::Incoming => style(s).green().to_string(), + TransactionKind::Outgoing => style(s).red().to_string(), + TransactionKind::External => style(s).red().to_string(), + TransactionKind::Batch => style(s).dim().to_string(), + TransactionKind::Reorg => style(s).dim().to_string(), + TransactionKind::Stasis => style(s).dim().to_string(), + TransactionKind::TransferIncoming => style(s).green().to_string(), + TransactionKind::TransferOutgoing => style(s).red().to_string(), + TransactionKind::Change => style(s).dim().to_string(), + } + } + + fn style_with_sign(&self, s: &str, history: bool) -> String { + match self { + TransactionKind::Incoming => style("+".to_string() + s).green().to_string(), + TransactionKind::TransferIncoming => style("+".to_string() + s).green().to_string(), + TransactionKind::Outgoing => style("-".to_string() + s).red().to_string(), + TransactionKind::TransferOutgoing => style("-".to_string() + s).red().to_string(), + TransactionKind::External => style("-".to_string() + s).red().to_string(), + TransactionKind::Batch => style("".to_string() + s).dim().to_string(), + TransactionKind::Reorg => { + if history { + style("".to_string() + s).dim() + } else { + style("-".to_string() + s).red() + } + } + .to_string(), + TransactionKind::Stasis => style("".to_string() + s).dim().to_string(), + _ => style(s).dim().to_string(), + } + } +} + +#[async_trait] +pub trait TransactionExtension { + async fn format_transaction(&self, wallet: &Arc, include_utxos: bool) -> Vec; + async fn format_transaction_with_state(&self, wallet: &Arc, state: Option<&str>, include_utxos: bool) -> Vec; + async fn format_transaction_with_args( + &self, + wallet: &Arc, + state: Option<&str>, + current_daa_score: Option, + include_utxos: bool, + history: bool, + account: Option>, + ) -> Vec; +} + +#[async_trait] +impl TransactionExtension for TransactionRecord { + async fn format_transaction(&self, wallet: &Arc, include_utxos: bool) -> Vec { + self.format_transaction_with_args(wallet, None, None, include_utxos, false, None).await + } + + async fn format_transaction_with_state(&self, wallet: &Arc, state: Option<&str>, include_utxos: bool) -> Vec { + self.format_transaction_with_args(wallet, state, None, include_utxos, false, None).await + } + + async fn format_transaction_with_args( + &self, + wallet: &Arc, + state: Option<&str>, + current_daa_score: Option, + include_utxos: bool, + history: bool, + account: Option>, + ) -> Vec { + let TransactionRecord { id, binding, block_daa_score, transaction_data, .. } = self; + + let name = match binding { + Binding::Custom(id) => style(id.short()).cyan(), + Binding::Account(account_id) => { + let account = if let Some(account) = account { + Some(account) + } else { + wallet.get_account_by_id(account_id).await.ok().flatten() + }; + + if let Some(account) = account { + style(account.name_with_id()).cyan() + } else { + style(account_id.short() + " ??").magenta() + } + } + }; + + let transaction_type = transaction_data.kind(); + let kind = transaction_type.style(&transaction_type.to_string()); + + let maturity = current_daa_score.map(|score| self.maturity(score).to_string()).unwrap_or_default(); + + let block_daa_score = block_daa_score.separated_string(); + let state = state.unwrap_or(&maturity); + let mut lines = vec![format!("{name} {id} @{block_daa_score} DAA - {kind} {state}")]; + + let suffix = kash_suffix(&self.network_id.network_type); + + match transaction_data { + TransactionData::Reorg { utxo_entries, aggregate_input_value } + | TransactionData::Stasis { utxo_entries, aggregate_input_value } + | TransactionData::Incoming { utxo_entries, aggregate_input_value } + | TransactionData::External { utxo_entries, aggregate_input_value } + | TransactionData::Change { utxo_entries, aggregate_input_value, .. } => { + let aggregate_input_value = + transaction_type.style_with_sign(sompi_to_kash_string(*aggregate_input_value).as_str(), history); + lines.push(format!("{:>4}UTXOs: {} Total: {}", "", utxo_entries.len(), aggregate_input_value)); + if include_utxos { + for utxo_entry in utxo_entries { + let address = + style(utxo_entry.address.as_ref().map(|addr| addr.to_string()).unwrap_or_else(|| "n/a".to_string())) + .blue(); + let index = utxo_entry.index; + let is_coinbase = if utxo_entry.is_coinbase { + style(format!("coinbase utxo [{index}]")).dim() + } else { + style(format!("standard utxo [{index}]")).dim() + }; + let amount = transaction_type.style_with_sign(sompi_to_kash_string(utxo_entry.amount).as_str(), history); + + lines.push(format!("{:>4}{address}", "")); + lines.push(format!("{:>4}{amount} {suffix} {is_coinbase}", "")); + } + } + } + TransactionData::Outgoing { fees, aggregate_input_value, transaction, payment_value, change_value, .. } + | TransactionData::Batch { fees, aggregate_input_value, transaction, payment_value, change_value, .. } + | TransactionData::TransferIncoming { fees, aggregate_input_value, transaction, payment_value, change_value, .. } + | TransactionData::TransferOutgoing { fees, aggregate_input_value, transaction, payment_value, change_value, .. } => { + if let Some(payment_value) = payment_value { + lines.push(format!( + "{:>4}Payment: {} Used: {} Fees: {} Change: {} UTXOs: [{}↠{}]", + "", + style(sompi_to_kash_string(*payment_value)).red(), + style(sompi_to_kash_string(*aggregate_input_value)).blue(), + style(sompi_to_kash_string(*fees)).red(), + style(sompi_to_kash_string(*change_value)).green(), + transaction.inputs.len(), + transaction.outputs.len(), + )); + } else { + lines.push(format!( + "{:>4}Sweep: {} Fees: {} Change: {} UTXOs: [{}↠{}]", + "", + style(sompi_to_kash_string(*aggregate_input_value)).blue(), + style(sompi_to_kash_string(*fees)).red(), + style(sompi_to_kash_string(*change_value)).green(), + transaction.inputs.len(), + transaction.outputs.len(), + )); + } + + if include_utxos { + for input in transaction.inputs.iter() { + let TransactionInput { previous_outpoint, signature_script: _, sequence, sig_op_count } = input; + let TransactionOutpoint { transaction_id, index } = previous_outpoint; + + lines.push(format!("{:>4}{sequence:>2}: {transaction_id}:{index} SigOps: {sig_op_count}", "")); + } + } + } + } + + lines + } +} diff --git a/cli/src/imports.rs b/cli/src/imports.rs index 381efd87f..b596f42ba 100644 --- a/cli/src/imports.rs +++ b/cli/src/imports.rs @@ -1,5 +1,6 @@ pub use crate::cli::KashCli; pub use crate::error::Error; +pub use crate::extensions::*; pub(crate) use crate::helpers; pub use crate::notifier::Notification; pub use crate::result::Result; @@ -13,14 +14,9 @@ pub use kash_consensus_core::network::{NetworkId, NetworkType}; pub use kash_daemon::DaemonEvent; pub use kash_utils::hex::*; pub use kash_wallet_core::derivation::gen0::import::*; -pub use kash_wallet_core::storage::interface::{AccessContext, Interface}; -pub use kash_wallet_core::storage::{AccessContextT, AccountKind, IdT, PrvKeyDataId, PrvKeyDataInfo}; -pub use kash_wallet_core::tx::PaymentOutputs; +pub use kash_wallet_core::prelude::*; +pub use kash_wallet_core::settings::{DefaultSettings, SettingsStore, WalletSettings}; pub use kash_wallet_core::utils::*; -pub use kash_wallet_core::{runtime::wallet::AccountCreateArgs, runtime::Wallet, secret::Secret}; -pub use kash_wallet_core::{ - Address, ConnectOptions, ConnectStrategy, DefaultSettings, Events, SettingsStore, SyncState, WalletSettings, -}; pub use pad::PadStr; pub use regex::Regex; pub use separator::Separatable; diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 1fd44dc90..c09c0e977 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -2,11 +2,11 @@ extern crate self as kash_cli; mod cli; pub mod error; +pub mod extensions; mod helpers; mod imports; mod matchers; -pub mod metrics; -mod modules; +pub mod modules; mod notifier; pub mod result; pub mod utils; diff --git a/cli/src/metrics/data.rs b/cli/src/metrics/data.rs index c4a26882b..e69de29bb 100644 --- a/cli/src/metrics/data.rs +++ b/cli/src/metrics/data.rs @@ -1,263 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use separator::Separatable; -use serde::{Deserialize, Serialize}; -use workflow_core::enums::Describe; - -#[derive(Describe, Debug, Clone, Eq, PartialEq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum Metric { - // CpuCores is used to normalize CpuUsage metric - // CpuCores - CpuUsage, - ResidentSetSizeBytes, - VirtualMemorySizeBytes, - FdNum, - DiskIoReadBytes, - DiskIoWriteBytes, - DiskIoReadPerSec, - DiskIoWritePerSec, - // --- - BlocksSubmitted, - HeaderCount, - DepCounts, - BodyCounts, - TxnCounts, - Tps, - ChainBlockCounts, - MassCounts, - BlockCount, - TipHashes, - Difficulty, - PastMedianTime, - VirtualParentHashes, - VirtualDaaScore, -} - -impl Metric { - pub fn group(&self) -> &'static str { - match self { - Metric::CpuUsage - | Metric::ResidentSetSizeBytes - | Metric::VirtualMemorySizeBytes - | Metric::FdNum - | Metric::DiskIoReadBytes - | Metric::DiskIoWriteBytes - | Metric::DiskIoReadPerSec - | Metric::DiskIoWritePerSec => "system", - // -- - Metric::BlocksSubmitted - | Metric::HeaderCount - | Metric::DepCounts - | Metric::BodyCounts - | Metric::TxnCounts - | Metric::Tps - | Metric::ChainBlockCounts - | Metric::MassCounts - | Metric::BlockCount - | Metric::TipHashes - | Metric::Difficulty - | Metric::PastMedianTime - | Metric::VirtualParentHashes - | Metric::VirtualDaaScore => "kash", - } - } -} - -#[derive(Default, Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub struct MetricsData { - pub unixtime: f64, - - // --- - pub resident_set_size_bytes: u64, - pub virtual_memory_size_bytes: u64, - pub cpu_cores: u64, - pub cpu_usage: f64, - pub fd_num: u64, - pub disk_io_read_bytes: u64, - pub disk_io_write_bytes: u64, - pub disk_io_read_per_sec: f64, - pub disk_io_write_per_sec: f64, - // --- - pub blocks_submitted: u64, - pub header_counts: u64, - pub dep_counts: u64, - pub body_counts: u64, - pub txs_counts: u64, - pub chain_block_counts: u64, - pub mass_counts: u64, - // --- - pub block_count: u64, - pub tip_hashes: usize, - pub difficulty: f64, - pub past_median_time: u64, - pub virtual_parent_hashes: usize, - pub virtual_daa_score: u64, -} - -impl MetricsData { - pub fn new(unixtime: f64) -> Self { - Self { unixtime, ..Default::default() } - } -} - -#[derive(Default, Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub struct MetricsSnapshot { - pub data: MetricsData, - - pub unixtime: f64, - pub duration: f64, - // --- - pub resident_set_size_bytes: f64, - pub virtual_memory_size_bytes: f64, - pub cpu_cores: f64, - pub cpu_usage: f64, - pub fd_num: f64, - pub disk_io_read_bytes: f64, - pub disk_io_write_bytes: f64, - pub disk_io_read_per_sec: f64, - pub disk_io_write_per_sec: f64, - // --- - pub blocks_submitted: f64, - pub header_counts: f64, - pub dep_counts: f64, - pub body_counts: f64, - pub txs_counts: f64, - pub tps: f64, - pub chain_block_counts: f64, - pub mass_counts: f64, - // --- - pub block_count: f64, - pub tip_hashes: f64, - pub difficulty: f64, - pub past_median_time: f64, - pub virtual_parent_hashes: f64, - pub virtual_daa_score: f64, -} - -impl MetricsSnapshot { - pub fn get(&self, metric: &Metric) -> f64 { - match metric { - // CpuCores - Metric::CpuUsage => self.cpu_usage / self.cpu_cores, - Metric::ResidentSetSizeBytes => self.resident_set_size_bytes, - Metric::VirtualMemorySizeBytes => self.virtual_memory_size_bytes, - Metric::FdNum => self.fd_num, - Metric::DiskIoReadBytes => self.disk_io_read_bytes, - Metric::DiskIoWriteBytes => self.disk_io_write_bytes, - Metric::DiskIoReadPerSec => self.disk_io_read_per_sec, - Metric::DiskIoWritePerSec => self.disk_io_write_per_sec, - - // --- - Metric::BlocksSubmitted => self.blocks_submitted, - Metric::HeaderCount => self.header_counts, - Metric::DepCounts => self.dep_counts, - Metric::BodyCounts => self.body_counts, - Metric::TxnCounts => self.txs_counts, - Metric::Tps => self.tps, - Metric::ChainBlockCounts => self.chain_block_counts, - Metric::MassCounts => self.mass_counts, - Metric::BlockCount => self.block_count, - Metric::TipHashes => self.tip_hashes, - Metric::Difficulty => self.difficulty, - Metric::PastMedianTime => self.past_median_time, - Metric::VirtualParentHashes => self.virtual_parent_hashes, - Metric::VirtualDaaScore => self.virtual_daa_score, - } - } - - pub fn format(&self, metric: &Metric, si: bool) -> String { - match metric { - Metric::CpuUsage => format!("CPU: {:1.2}%", self.cpu_usage / self.cpu_cores * 100.0), - Metric::ResidentSetSizeBytes => { - // workflow_log::log_info!("Resident Memory: {}", self.resident_set_size_bytes); - format!("Resident Memory: {}", as_mb(self.resident_set_size_bytes, si)) - } - Metric::VirtualMemorySizeBytes => { - format!("Virtual Memory: {}", as_mb(self.virtual_memory_size_bytes, si)) - } - Metric::FdNum => format!("File Handles: {}", self.fd_num.separated_string()), - Metric::DiskIoReadBytes => format!("Storage Read: {}", as_mb(self.disk_io_read_bytes, si)), - Metric::DiskIoWriteBytes => format!("Storage Write: {}", as_mb(self.disk_io_write_bytes, si)), - Metric::DiskIoReadPerSec => format!("Storage Read: {}/s", as_kb(self.disk_io_read_per_sec, si)), - Metric::DiskIoWritePerSec => format!("Storage Write: {}/s", as_kb(self.disk_io_write_per_sec, si)), - // -- - Metric::BlocksSubmitted => format!("Blocks Submitted: {}", self.blocks_submitted.separated_string()), - Metric::HeaderCount => format!("Headers: {}", self.header_counts.separated_string()), - Metric::DepCounts => format!("Dependencies: {}", self.dep_counts.separated_string()), - Metric::BodyCounts => format!("Body Counts: {}", self.body_counts.separated_string()), - Metric::TxnCounts => format!("Transactions: {}", self.txs_counts.separated_string()), - Metric::Tps => format!("TPS: {}", (((self.tps * 100.0) as u64) as f64 / 100.0).separated_string()), - Metric::ChainBlockCounts => format!("Chain Blocks: {}", self.chain_block_counts.separated_string()), - Metric::MassCounts => format!("Mass Counts: {}", self.mass_counts.separated_string()), - Metric::BlockCount => format!("Blocks: {}", self.block_count.separated_string()), - Metric::TipHashes => format!("Tip Hashes: {}", self.tip_hashes.separated_string()), - Metric::Difficulty => { - format!("Difficulty: {}", self.difficulty.separated_string()) - } - Metric::PastMedianTime => format!("Past Median Time: {}", self.past_median_time.separated_string()), - Metric::VirtualParentHashes => format!("Virtual Parent Hashes: {}", self.virtual_parent_hashes.separated_string()), - Metric::VirtualDaaScore => format!("Virtual DAA Score: {}", self.virtual_daa_score.separated_string()), - } - } -} - -impl From<(&MetricsData, &MetricsData)> for MetricsSnapshot { - fn from((a, b): (&MetricsData, &MetricsData)) -> Self { - let duration = b.unixtime - a.unixtime; - let tps = (b.txs_counts - a.txs_counts) as f64 * 1000. / duration; - Self { - unixtime: b.unixtime, - duration, - // --- - cpu_usage: b.cpu_usage, - cpu_cores: b.cpu_cores as f64, - resident_set_size_bytes: b.resident_set_size_bytes as f64, - virtual_memory_size_bytes: b.virtual_memory_size_bytes as f64, - fd_num: b.fd_num as f64, - disk_io_read_bytes: b.disk_io_read_bytes as f64, - disk_io_write_bytes: b.disk_io_write_bytes as f64, - disk_io_read_per_sec: b.disk_io_read_per_sec, - disk_io_write_per_sec: b.disk_io_write_per_sec, - // --- - blocks_submitted: b.blocks_submitted as f64, - header_counts: b.header_counts as f64, - dep_counts: b.dep_counts as f64, - body_counts: b.body_counts as f64, - txs_counts: b.txs_counts as f64, - tps, - chain_block_counts: b.chain_block_counts as f64, - mass_counts: b.mass_counts as f64, - // --- - block_count: b.block_count as f64, - tip_hashes: b.tip_hashes as f64, - difficulty: b.difficulty, - past_median_time: b.past_median_time as f64, - virtual_parent_hashes: b.virtual_parent_hashes as f64, - virtual_daa_score: b.virtual_daa_score as f64, - - data: b.clone(), - } - } -} - -fn as_kb(bytes: f64, si: bool) -> String { - let unit = if si { 1000. } else { 1024. }; - let suffix = if si { " KB" } else { " KiB" }; - let kb = ((bytes / unit * 100.) as u64) as f64 / 100.; - (kb).separated_string() + suffix -} - -fn as_mb(bytes: f64, si: bool) -> String { - // bytes.separated_string() - let unit = if si { 1000. } else { 1024. }; - let suffix = if si { " MB" } else { " MiB" }; - let gb = ((bytes / unit / unit * 100.) as u64) as f64 / 100.; - (gb).separated_string() + suffix -} - -fn _as_gb(bytes: f64, si: bool) -> String { - // bytes.separated_string() - let unit = if si { 1000. } else { 1024. }; - let suffix = if si { " GB" } else { " GiB" }; - let gb = ((bytes / unit / unit / unit * 100.) as u64) as f64 / 100.; - (gb).separated_string() + suffix -} diff --git a/cli/src/metrics/metrics.rs b/cli/src/metrics/metrics.rs deleted file mode 100644 index d6f8e34b2..000000000 --- a/cli/src/metrics/metrics.rs +++ /dev/null @@ -1,240 +0,0 @@ -use super::{MetricsData, MetricsSnapshot}; -use crate::imports::*; -use futures::{future::join_all, pin_mut}; -use kash_rpc_core::{api::rpc::RpcApi, GetMetricsResponse}; -use std::pin::Pin; -use workflow_core::{runtime::is_nw, task::interval}; -pub type MetricsSinkFn = - Arc Pin>)>> + 'static>>; - -#[derive(Describe, Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[serde(rename_all = "lowercase")] -pub enum MetricsSettings { - #[describe("Mute logs")] - Mute, -} - -#[async_trait] -impl DefaultSettings for MetricsSettings { - async fn defaults() -> Vec<(Self, Value)> { - // let mut settings = vec![(Self::Mute, "false".to_string())]; - // settings - vec![] - } -} - -pub struct Metrics { - settings: SettingsStore, - mute: Arc, - task_ctl: DuplexChannel, - rpc: Arc>>>, - // target : Arc>>>, - sink: Arc>>, - data: Arc>>, -} - -impl Default for Metrics { - fn default() -> Self { - Metrics { - settings: SettingsStore::try_new("metrics").expect("Failed to create miner settings store"), - mute: Arc::new(AtomicBool::new(true)), - task_ctl: DuplexChannel::oneshot(), - rpc: Arc::new(Mutex::new(None)), - sink: Arc::new(Mutex::new(None)), - data: Arc::new(Mutex::new(None)), - } - } -} - -#[async_trait] -impl Handler for Metrics { - fn verb(&self, _ctx: &Arc) -> Option<&'static str> { - Some("metrics") - } - - fn help(&self, _ctx: &Arc) -> &'static str { - "Manage metrics monitoring" - } - - async fn start(self: Arc, ctx: &Arc) -> cli::Result<()> { - let ctx = ctx.clone().downcast_arc::()?; - - self.settings.try_load().await.ok(); - if let Some(mute) = self.settings.get(MetricsSettings::Mute) { - self.mute.store(mute, Ordering::Relaxed); - } - - self.rpc.lock().unwrap().replace(ctx.wallet().rpc_api().clone()); - - self.start_task(&ctx).await?; - Ok(()) - } - - async fn stop(self: Arc, _ctx: &Arc) -> cli::Result<()> { - self.stop_task().await?; - Ok(()) - } - - async fn handle(self: Arc, ctx: &Arc, argv: Vec, cmd: &str) -> cli::Result<()> { - let ctx = ctx.clone().downcast_arc::()?; - self.main(ctx, argv, cmd).await.map_err(|e| e.into()) - } -} - -impl Metrics { - fn rpc(&self) -> Option> { - self.rpc.lock().unwrap().clone() - } - - pub fn register_sink(&self, target: MetricsSinkFn) { - self.sink.lock().unwrap().replace(target); - } - - pub fn unregister_sink(&self) { - self.sink.lock().unwrap().take(); - } - - pub fn sink(&self) -> Option { - self.sink.lock().unwrap().clone() - } - - async fn main(self: Arc, ctx: Arc, mut argv: Vec, _cmd: &str) -> Result<()> { - if argv.is_empty() { - return self.display_help(ctx, argv).await; - } - match argv.remove(0).as_str() { - "open" => {} - v => { - tprintln!(ctx, "unknown command: '{v}'\r\n"); - - return self.display_help(ctx, argv).await; - } - } - - Ok(()) - } - - pub async fn start_task(self: &Arc, ctx: &Arc) -> Result<()> { - let this = self.clone(); - let ctx = ctx.clone(); - - let task_ctl_receiver = self.task_ctl.request.receiver.clone(); - let task_ctl_sender = self.task_ctl.response.sender.clone(); - - *this.data.lock().unwrap() = Some(MetricsData::new(unixtime_as_millis_f64())); - - spawn(async move { - let interval = interval(Duration::from_secs(1)); - pin_mut!(interval); - - loop { - select! { - _ = task_ctl_receiver.recv().fuse() => { - break; - }, - _ = interval.next().fuse() => { - - if !ctx.is_connected() { - continue; - } - - let last_data = this.data.lock().unwrap().take().unwrap(); - this.data.lock().unwrap().replace(MetricsData::new(unixtime_as_millis_f64())); - if let Some(rpc) = this.rpc() { - let samples = vec![ - this.sample_metrics(rpc.clone()).boxed(), - this.sample_gbdi(rpc.clone()).boxed(), - this.sample_cpi(rpc.clone()).boxed(), - ]; - - join_all(samples).await; - } - - if let Some(sink) = this.sink() { - let snapshot = MetricsSnapshot::from((&last_data, this.data.lock().unwrap().as_ref().unwrap())); - sink(snapshot).await.ok(); - } - } - } - } - - task_ctl_sender.send(()).await.unwrap(); - }); - Ok(()) - } - - pub async fn stop_task(&self) -> Result<()> { - self.task_ctl.signal(()).await.expect("Metrics::stop_task() signal error"); - Ok(()) - } - - pub async fn display_help(self: &Arc, ctx: Arc, _argv: Vec) -> Result<()> { - // disable help in non-nw environments - if !is_nw() { - return Ok(()); - } - - ctx.term().help(&[("open", "Open metrics window"), ("close", "Close metrics window")], None)?; - - Ok(()) - } - - // --- samplers - - async fn sample_metrics(self: &Arc, rpc: Arc) -> Result<()> { - if let Ok(metrics) = rpc.get_metrics(true, true).await { - let GetMetricsResponse { server_time: _, consensus_metrics, process_metrics } = metrics; - - let mut data = self.data.lock().unwrap(); - let data = data.as_mut().unwrap(); - if let Some(consensus_metrics) = consensus_metrics { - data.blocks_submitted = consensus_metrics.blocks_submitted; - data.header_counts = consensus_metrics.header_counts; - data.dep_counts = consensus_metrics.dep_counts; - data.body_counts = consensus_metrics.body_counts; - data.txs_counts = consensus_metrics.txs_counts; - data.chain_block_counts = consensus_metrics.chain_block_counts; - data.mass_counts = consensus_metrics.mass_counts; - } - - if let Some(process_metrics) = process_metrics { - data.resident_set_size_bytes = process_metrics.resident_set_size; - data.virtual_memory_size_bytes = process_metrics.virtual_memory_size; - data.cpu_cores = process_metrics.core_num; - data.cpu_usage = process_metrics.cpu_usage; - data.fd_num = process_metrics.fd_num; - data.disk_io_read_bytes = process_metrics.disk_io_read_bytes; - data.disk_io_write_bytes = process_metrics.disk_io_write_bytes; - data.disk_io_read_per_sec = process_metrics.disk_io_read_per_sec; - data.disk_io_write_per_sec = process_metrics.disk_io_write_per_sec; - } - } - - Ok(()) - } - - async fn sample_gbdi(self: &Arc, rpc: Arc) -> Result<()> { - if let Ok(gdbi) = rpc.get_block_dag_info().await { - let mut data = self.data.lock().unwrap(); - let data = data.as_mut().unwrap(); - data.block_count = gdbi.block_count; - // data.header_count = gdbi.header_count; - data.tip_hashes = gdbi.tip_hashes.len(); - data.difficulty = gdbi.difficulty; - data.past_median_time = gdbi.past_median_time; - data.virtual_parent_hashes = gdbi.virtual_parent_hashes.len(); - data.virtual_daa_score = gdbi.virtual_daa_score; - } - - Ok(()) - } - - async fn sample_cpi(self: &Arc, rpc: Arc) -> Result<()> { - if let Ok(_cpi) = rpc.get_connected_peer_info().await { - // let mut data = self.data.lock().unwrap(); - // - TODO - fold peers into inbound / outbound... - } - - Ok(()) - } -} diff --git a/cli/src/metrics/mod.rs b/cli/src/metrics/mod.rs deleted file mode 100644 index b3bf55d66..000000000 --- a/cli/src/metrics/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod data; -#[allow(clippy::module_inception)] -pub mod metrics; - -pub use data::{Metric, MetricsData, MetricsSnapshot}; -pub use metrics::Metrics; diff --git a/cli/src/modules/account.rs b/cli/src/modules/account.rs index cec7f00f6..a4a567ab3 100644 --- a/cli/src/modules/account.rs +++ b/cli/src/modules/account.rs @@ -1,3 +1,7 @@ +use kash_wallet_core::account::BIP32_ACCOUNT_KIND; +use kash_wallet_core::account::LEGACY_ACCOUNT_KIND; +use kash_wallet_core::account::MULTISIG_ACCOUNT_KIND; + use crate::imports::*; use crate::wizards; @@ -26,20 +30,20 @@ impl Account { tprintln!(ctx, "usage: 'account name ' or 'account name remove'"); return Ok(()); } else { - let (secret, _) = ctx.ask_wallet_secret(None).await?; + let (wallet_secret, _) = ctx.ask_wallet_secret(None).await?; let _ = ctx.notifier().show(Notification::Processing).await; let account = ctx.select_account().await?; let name = argv.remove(0); if name == "remove" { - account.rename(secret, None).await?; + account.rename(&wallet_secret, None).await?; } else { - account.rename(secret, Some(name.as_str())).await?; + account.rename(&wallet_secret, Some(name.as_str())).await?; } } } "create" => { let account_kind = if argv.is_empty() { - AccountKind::Bip32 + BIP32_ACCOUNT_KIND.into() } else { let kind = argv.remove(0); kind.parse::()? @@ -109,11 +113,11 @@ impl Account { Secret::new(ctx.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec()); let ctx_ = ctx.clone(); wallet - .import_gen0_keydata( - import_secret, - wallet_secret, + .import_legacy_keydata( + &import_secret, + &wallet_secret, None, - Some(Arc::new(move |processed: usize, balance, txid| { + Some(Arc::new(move |processed: usize, _, balance, txid| { if let Some(txid) = txid { tprintln!( ctx_, @@ -152,15 +156,15 @@ impl Account { let account_kind = argv.remove(0); let account_kind = account_kind.parse::()?; - match account_kind { - AccountKind::Legacy | AccountKind::Bip32 => { + match account_kind.as_ref() { + LEGACY_ACCOUNT_KIND | BIP32_ACCOUNT_KIND => { if !argv.is_empty() { tprintln!(ctx, "too many arguments: {}\r\n", argv.join(" ")); return Ok(()); } crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &argv).await?; } - AccountKind::MultiSig => { + MULTISIG_ACCOUNT_KIND => { crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &argv).await?; } _ => { @@ -253,7 +257,7 @@ impl Account { window, sweep, &abortable, - Some(Arc::new(move |processed: usize, balance, txid| { + Some(Arc::new(move |processed: usize, _, balance, txid| { if let Some(txid) = txid { tprintln!( ctx_, diff --git a/cli/src/modules/connect.rs b/cli/src/modules/connect.rs index c7488c28e..1d654c1db 100644 --- a/cli/src/modules/connect.rs +++ b/cli/src/modules/connect.rs @@ -10,7 +10,9 @@ impl Connect { if let Some(wrpc_client) = ctx.wallet().wrpc_client().as_ref() { let url = argv.first().cloned().or_else(|| ctx.wallet().settings().get(WalletSettings::Server)); let network_type = ctx.wallet().network_id()?; - let url = wrpc_client.parse_url_with_network_type(url, network_type.into()).map_err(|e| e.to_string())?; + let url = url + .map(|url| wrpc_client.parse_url_with_network_type(url, network_type.into()).map_err(|e| e.to_string())) + .transpose()?; let options = ConnectOptions { block_async_connect: true, strategy: ConnectStrategy::Fallback, url, ..Default::default() }; wrpc_client.connect(options).await.map_err(|e| e.to_string())?; } else { diff --git a/cli/src/modules/estimate.rs b/cli/src/modules/estimate.rs index a21333113..2821c3a0d 100644 --- a/cli/src/modules/estimate.rs +++ b/cli/src/modules/estimate.rs @@ -19,8 +19,8 @@ impl Estimate { } let tx_action_str = &argv[0]; - let tx_action = - TransactionAction::try_from(tx_action_str).map_err(|_| Error::custom(format!("Invalid transaction action: {}", tx_action_str)))?; + let tx_action = TransactionAction::try_from(tx_action_str) + .map_err(|_| Error::custom(format!("Invalid transaction action: {}", tx_action_str)))?; let amount_sompi = try_parse_required_nonzero_kash_as_sompi_u64(argv.get(1))?; let priority_fee_sompi = try_parse_optional_kash_as_sompi_i64(argv.get(2))?.unwrap_or(0); diff --git a/cli/src/modules/export.rs b/cli/src/modules/export.rs index cb3a08f7f..b1229bd90 100644 --- a/cli/src/modules/export.rs +++ b/cli/src/modules/export.rs @@ -1,5 +1,5 @@ use crate::imports::*; -use kash_wallet_core::runtime::{Account, MultiSig}; +use kash_wallet_core::account::{multisig::MultiSig, Account, MULTISIG_ACCOUNT_KIND}; #[derive(Default, Handler)] #[help("Export transactions, a wallet or a private key")] @@ -18,7 +18,7 @@ impl Export { match what.as_str() { "mnemonic" => { let account = ctx.account().await?; - if matches!(account.account_kind(), AccountKind::MultiSig) { + if account.account_kind() == MULTISIG_ACCOUNT_KIND { let account = account.downcast_arc::()?; export_multisig_account(ctx, account).await } else { @@ -31,7 +31,7 @@ impl Export { } async fn export_multisig_account(ctx: Arc, account: Arc) -> Result<()> { - match &account.prv_key_data_ids { + match &account.prv_key_data_ids() { None => Err(Error::KeyDataNotFound), Some(v) if v.is_empty() => Err(Error::KeyDataNotFound), Some(prv_key_data_ids) => { @@ -40,14 +40,13 @@ async fn export_multisig_account(ctx: Arc, account: Arc) -> R return Err(Error::WalletSecretRequired); } - tprintln!(ctx, "required signatures: {}", account.minimum_signatures); + tprintln!(ctx, "required signatures: {}", account.minimum_signatures()); tprintln!(ctx, ""); - let access_ctx: Arc = Arc::new(AccessContext::new(wallet_secret)); let prv_key_data_store = ctx.store().as_prv_key_data_store()?; let mut generated_xpub_keys = Vec::with_capacity(prv_key_data_ids.len()); for (id, prv_key_data_id) in prv_key_data_ids.iter().enumerate() { - let prv_key_data = prv_key_data_store.load_key_data(&access_ctx, prv_key_data_id).await?.unwrap(); + let prv_key_data = prv_key_data_store.load_key_data(&wallet_secret, prv_key_data_id).await?.unwrap(); let mnemonic = prv_key_data.as_mnemonic(None).unwrap().unwrap(); tprintln!(ctx, "mnemonic {}:", id + 1); @@ -55,12 +54,11 @@ async fn export_multisig_account(ctx: Arc, account: Arc) -> R tprintln!(ctx, "{}", mnemonic.phrase()); tprintln!(ctx, ""); - let xpub_key = prv_key_data.create_xpub(None, AccountKind::MultiSig, 0).await?; // todo it can be done concurrently - let xpub_prefix = kash_bip32::Prefix::XPUB; - generated_xpub_keys.push(xpub_key.to_string(Some(xpub_prefix))); + let xpub_key = prv_key_data.create_xpub(None, MULTISIG_ACCOUNT_KIND.into(), 0).await?; // todo it can be done concurrently + generated_xpub_keys.push(xpub_key); } - let additional = account.xpub_keys.iter().filter(|xpub| !generated_xpub_keys.contains(xpub)); + let additional = account.xpub_keys().iter().filter(|xpub| !generated_xpub_keys.contains(xpub)); additional.enumerate().for_each(|(idx, xpub)| { if idx == 0 { tprintln!(ctx, "additional xpubs: "); @@ -80,8 +78,7 @@ async fn export_single_key_account(ctx: Arc, account: Arc) return Err(Error::WalletSecretRequired); } - let access_ctx: Arc = Arc::new(AccessContext::new(wallet_secret)); - let prv_key_data = ctx.store().as_prv_key_data_store()?.load_key_data(&access_ctx, prv_key_data_id).await?; + let prv_key_data = ctx.store().as_prv_key_data_store()?.load_key_data(&wallet_secret, prv_key_data_id).await?; let Some(keydata) = prv_key_data else { return Err(Error::KeyDataNotFound) }; let payment_secret = if keydata.payload.is_encrypted() { let payment_secret = Secret::new(ctx.term().ask(true, "Enter payment password: ").await?.trim().as_bytes().to_vec()); diff --git a/cli/src/modules/history.rs b/cli/src/modules/history.rs index 9fea8c354..5fd51957d 100644 --- a/cli/src/modules/history.rs +++ b/cli/src/modules/history.rs @@ -33,8 +33,9 @@ impl History { let store = ctx.wallet().store().as_transaction_record_store()?; match store.load_single(&binding, &network_id, &txid).await { Ok(tx) => { - let lines = - tx.format_with_args(&ctx.wallet(), None, current_daa_score, true, true, Some(account.clone())).await; + let lines = tx + .format_transaction_with_args(&ctx.wallet(), None, current_daa_score, true, true, Some(account.clone())) + .await; lines.iter().for_each(|line| tprintln!(ctx, "{line}")); } Err(_) => { @@ -108,7 +109,14 @@ impl History { match store.load_single(&binding, &network_id, &id).await { Ok(tx) => { let lines = tx - .format_with_args(&ctx.wallet(), None, current_daa_score, include_utxo, true, Some(account.clone())) + .format_transaction_with_args( + &ctx.wallet(), + None, + current_daa_score, + include_utxo, + true, + Some(account.clone()), + ) .await; lines.iter().for_each(|line| tprintln!(ctx, "{line}")); } diff --git a/cli/src/modules/message.rs b/cli/src/modules/message.rs index 971917782..6df6854fa 100644 --- a/cli/src/modules/message.rs +++ b/cli/src/modules/message.rs @@ -1,6 +1,9 @@ use kash_addresses::Version; use kash_bip32::secp256k1::XOnlyPublicKey; -use kash_wallet_core::message::{sign_message, verify_message, PersonalMessage}; +use kash_wallet_core::{ + account::{BIP32_ACCOUNT_KIND, KEYPAIR_ACCOUNT_KIND}, + message::{sign_message, verify_message, PersonalMessage}, +}; use crate::imports::*; @@ -126,8 +129,8 @@ impl Message { async fn get_address_private_key(self: Arc, ctx: &Arc, kash_address: Address) -> Result<[u8; 32]> { let account = ctx.wallet().account()?; - match account.account_kind() { - AccountKind::Bip32 => { + match account.account_kind().as_ref() { + BIP32_ACCOUNT_KIND => { let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?; let keydata = account.prv_key_data(wallet_secret).await?; let account = account.clone().as_derivation_capable().expect("expecting derivation capable"); @@ -142,7 +145,7 @@ impl Message { Err(Error::custom("Could not find address in any derivation path in account")) } - AccountKind::Keypair => { + KEYPAIR_ACCOUNT_KIND => { let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(Some(&account)).await?; let keydata = account.prv_key_data(wallet_secret).await?; let decrypted_privkey = keydata.payload.decrypt(payment_secret.as_ref()).unwrap(); diff --git a/cli/src/modules/metrics.rs b/cli/src/modules/metrics.rs new file mode 100644 index 000000000..73d1dfd9d --- /dev/null +++ b/cli/src/modules/metrics.rs @@ -0,0 +1,110 @@ +use crate::imports::*; +use kash_metrics_core::{Metrics as MetricsProcessor, MetricsSinkFn}; +use workflow_core::runtime::is_nw; + +#[derive(Describe, Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[serde(rename_all = "lowercase")] +pub enum MetricsSettings { + #[describe("Mute logs")] + Mute, +} + +#[async_trait] +impl DefaultSettings for MetricsSettings { + async fn defaults() -> Vec<(Self, Value)> { + vec![] + } +} + +pub struct Metrics { + settings: SettingsStore, + mute: Arc, + metrics: Arc, +} + +impl Default for Metrics { + fn default() -> Self { + Metrics { + settings: SettingsStore::try_new("metrics").expect("Failed to create miner settings store"), + mute: Arc::new(AtomicBool::new(true)), + metrics: Arc::new(MetricsProcessor::default()), + } + } +} + +#[async_trait] +impl Handler for Metrics { + fn verb(&self, _ctx: &Arc) -> Option<&'static str> { + Some("metrics") + } + + fn help(&self, _ctx: &Arc) -> &'static str { + "Manage metrics monitoring" + } + + async fn start(self: Arc, ctx: &Arc) -> cli::Result<()> { + let ctx = ctx.clone().downcast_arc::()?; + + self.settings.try_load().await.ok(); + if let Some(mute) = self.settings.get(MetricsSettings::Mute) { + self.mute.store(mute, Ordering::Relaxed); + } + + self.metrics.set_rpc(Some(ctx.wallet().rpc_api().clone())); + + Ok(()) + } + + async fn stop(self: Arc, _ctx: &Arc) -> cli::Result<()> { + self.metrics.stop_task().await.map_err(|err| err.to_string())?; + Ok(()) + } + + async fn handle(self: Arc, ctx: &Arc, argv: Vec, cmd: &str) -> cli::Result<()> { + let ctx = ctx.clone().downcast_arc::()?; + self.main(ctx, argv, cmd).await.map_err(|e| e.into()) + } +} + +impl Metrics { + pub fn register_sink(&self, target: MetricsSinkFn) { + self.metrics.register_sink(target); + } + + pub fn unregister_sink(&self) { + self.metrics.unregister_sink(); + } + + pub fn sink(&self) -> Option { + self.metrics.sink() + } + + async fn main(self: Arc, ctx: Arc, mut argv: Vec, _cmd: &str) -> Result<()> { + if argv.is_empty() { + return self.display_help(ctx, argv).await; + } + match argv.remove(0).as_str() { + "open" => {} + v => { + tprintln!(ctx, "unknown command: '{v}'\r\n"); + + return self.display_help(ctx, argv).await; + } + } + + Ok(()) + } + + pub async fn display_help(self: &Arc, ctx: Arc, _argv: Vec) -> Result<()> { + // disable help in non-nw environments + if !is_nw() { + return Ok(()); + } + + ctx.term().help(&[("open", "Open metrics window"), ("close", "Close metrics window")], None)?; + + Ok(()) + } + + // --- samplers +} diff --git a/cli/src/modules/mod.rs b/cli/src/modules/mod.rs index a027d5e9f..1a370af18 100644 --- a/cli/src/modules/mod.rs +++ b/cli/src/modules/mod.rs @@ -42,6 +42,10 @@ pub mod track; pub mod transfer; pub mod wallet; +// this module is registered manually within +// applications that support metrics +pub mod metrics; + // TODO // broadcast // create-unsigned-tx diff --git a/cli/src/modules/monitor.rs b/cli/src/modules/monitor.rs index d48fdc266..53d5c7023 100644 --- a/cli/src/modules/monitor.rs +++ b/cli/src/modules/monitor.rs @@ -1,5 +1,4 @@ use crate::imports::*; -use kash_wallet_core::runtime::balance::*; use workflow_core::channel::*; use workflow_terminal::clear::*; use workflow_terminal::cursor::*; @@ -110,16 +109,21 @@ impl Monitor { let events = events.lock().unwrap(); events.iter().for_each(|event| match event.deref() { Events::DAAScoreChange { .. } => {} - Events::Balance { balance, id, mature_utxo_size, pending_utxo_size } => { + Events::Balance { balance, id } => { let network_id = wallet.network_id().expect("missing network type"); let network_type = NetworkType::from(network_id); - let balance = BalanceStrings::from((balance, &network_type, None)); + let balance_strings = BalanceStrings::from((balance, &network_type, None)); let id = id.short(); - let pending_utxo_info = if *pending_utxo_size > 0 { format!("({pending_utxo_size} pending)") } else { "".to_string() }; - let utxo_info = style(format!("{} UTXOs {pending_utxo_info}", mature_utxo_size.separated_string())).dim(); + let mature_utxo_count = + balance.as_ref().map(|balance| balance.mature_utxo_count.separated_string()).unwrap_or("N/A".to_string()); + let pending_utxo_count = balance.as_ref().map(|balance| balance.pending_utxo_count).unwrap_or(0); - tprintln!(ctx, "{} {id}: {balance} {utxo_info}", style("balance".pad_to_width(8)).blue()); + let pending_utxo_info = + if pending_utxo_count > 0 { format!("({pending_utxo_count} pending)") } else { "".to_string() }; + let utxo_info = style(format!("{mature_utxo_count} UTXOs {pending_utxo_info}")).dim(); + + tprintln!(ctx, "{} {id}: {balance_strings} {utxo_info}", style("balance".pad_to_width(8)).blue()); } _ => {} }); diff --git a/cli/src/modules/node.rs b/cli/src/modules/node.rs index d1b717aa3..a23074012 100644 --- a/cli/src/modules/node.rs +++ b/cli/src/modules/node.rs @@ -111,10 +111,13 @@ impl Node { kashd.configure(self.create_config(&ctx).await?).await?; kashd.start().await?; - // temporary setup for autoconnect + // temporary setup for auto-connect let url = ctx.wallet().settings().get(WalletSettings::Server); let network_type = ctx.wallet().network_id()?; - if let Some(url) = wrpc_client.parse_url_with_network_type(url, network_type.into()).map_err(|e| e.to_string())? { + if let Some(url) = url + .map(|url| wrpc_client.parse_url_with_network_type(url, network_type.into()).map_err(|e| e.to_string())) + .transpose()? + { // log_info!("connecting to url: {}", url); if url.contains("127.0.0.1") || url.contains("localhost") { spawn(async move { diff --git a/cli/src/modules/ping.rs b/cli/src/modules/ping.rs index 2bcc482f3..accbd12d3 100644 --- a/cli/src/modules/ping.rs +++ b/cli/src/modules/ping.rs @@ -7,7 +7,7 @@ pub struct Ping; impl Ping { async fn main(self: Arc, ctx: &Arc, _argv: Vec, _cmd: &str) -> Result<()> { let ctx = ctx.clone().downcast_arc::()?; - if ctx.wallet().ping().await { + if ctx.wallet().ping(None).await.is_ok() { tprintln!(ctx, "ping ok"); } else { terrorln!(ctx, "ping error"); diff --git a/cli/src/modules/rpc.rs b/cli/src/modules/rpc.rs index cf92326e1..5eb2acdd0 100644 --- a/cli/src/modules/rpc.rs +++ b/cli/src/modules/rpc.rs @@ -38,7 +38,7 @@ impl Rpc { tprintln!(ctx, "ok"); } RpcApiOps::GetMetrics => { - let result = rpc.get_metrics(true, true).await?; + let result = rpc.get_metrics(true, true, true, true).await?; self.println(&ctx, result); } RpcApiOps::GetServerInfo => { @@ -84,10 +84,15 @@ impl Rpc { let result = rpc.get_connected_peer_info_call(GetConnectedPeerInfoRequest {}).await?; self.println(&ctx, result); } - // RpcApiOps::AddPeer => { - // let result = rpc.add_peer_call(AddPeerRequest { }).await?; - // self.println(&ctx, result); - // } + RpcApiOps::AddPeer => { + if argv.is_empty() { + return Err(Error::custom("Usage: rpc addpeer [true|false for 'is_permanent']")); + } + let peer_address = argv.remove(0).parse::()?; + let is_permanent = argv.remove(0).parse::().unwrap_or(false); + let result = rpc.add_peer_call(AddPeerRequest { peer_address, is_permanent }).await?; + self.println(&ctx, result); + } // RpcApiOps::SubmitTransaction => { // let result = rpc.submit_transaction_call(SubmitTransactionRequest { }).await?; // self.println(&ctx, result); diff --git a/cli/src/modules/wallet.rs b/cli/src/modules/wallet.rs index beaf72b0b..923de66eb 100644 --- a/cli/src/modules/wallet.rs +++ b/cli/src/modules/wallet.rs @@ -64,9 +64,11 @@ impl Wallet { ctx.wallet().settings().get(WalletSettings::Wallet).clone() }; - let (secret, _) = ctx.ask_wallet_secret(None).await?; + let (wallet_secret, _) = ctx.ask_wallet_secret(None).await?; let _ = ctx.notifier().show(Notification::Processing).await; - ctx.wallet().load_and_activate(secret, name).await?; + let args = WalletOpenArgs::default_with_legacy_accounts(); + ctx.wallet().open(&wallet_secret, name, args).await?; + ctx.wallet().activate_accounts(None).await?; } "close" => { ctx.wallet().close().await?; diff --git a/cli/src/wizards/account.rs b/cli/src/wizards/account.rs index ec57ef8b8..0e166743d 100644 --- a/cli/src/wizards/account.rs +++ b/cli/src/wizards/account.rs @@ -1,9 +1,11 @@ use crate::cli::KashCli; use crate::imports::*; use crate::result::Result; -use kash_wallet_core::runtime::wallet::MultisigCreateArgs; -use kash_wallet_core::runtime::PrvKeyDataCreateArgs; -use kash_wallet_core::storage::AccountKind; +use kash_bip32::{Language, Mnemonic, WordCount}; +use kash_wallet_core::account::MULTISIG_ACCOUNT_KIND; +// use kash_wallet_core::runtime::wallet::AccountCreateArgsBip32; +// use kash_wallet_core::runtime::{PrvKeyDataArgs, PrvKeyDataCreateArgs}; +// use kash_wallet_core::storage::AccountKind; pub(crate) async fn create( ctx: &Arc, @@ -14,16 +16,17 @@ pub(crate) async fn create( let term = ctx.term(); let wallet = ctx.wallet(); - let (title, name) = if let Some(name) = name { - (Some(name.to_string()), Some(name.to_string())) + // TODO @aspect + let word_count = WordCount::Words12; + + let name = if let Some(name) = name { + Some(name.to_string()) } else { - let title = term.ask(false, "Please enter account title (optional, press to skip): ").await?.trim().to_string(); - let name = title.replace(' ', "-").to_lowercase(); - (Some(title), Some(name)) + Some(term.ask(false, "Please enter account name (optional, press to skip): ").await?.trim().to_string()) }; - if matches!(account_kind, AccountKind::MultiSig) { - return create_multisig(ctx, title, name).await; + if account_kind == MULTISIG_ACCOUNT_KIND { + return create_multisig(ctx, name, word_count).await; } let wallet_secret = Secret::new(term.ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec()); @@ -42,30 +45,31 @@ pub(crate) async fn create( None }; - let account_args = AccountCreateArgs::new(name, title, account_kind, wallet_secret, payment_secret); - let account = wallet.create_bip32_account(prv_key_data_info.id, account_args).await?; + let account_create_args_bip32 = AccountCreateArgsBip32::new(name, None); + let account = + wallet.create_account_bip32(&wallet_secret, prv_key_data_info.id, payment_secret.as_ref(), account_create_args_bip32).await?; tprintln!(ctx, "\naccount created: {}\n", account.get_list_string()?); wallet.select(Some(&account)).await?; Ok(()) } -async fn create_multisig(ctx: &Arc, title: Option, name: Option) -> Result<()> { +async fn create_multisig(ctx: &Arc, account_name: Option, mnemonic_phrase_word_count: WordCount) -> Result<()> { let term = ctx.term(); let wallet = ctx.wallet(); let (wallet_secret, _) = ctx.ask_wallet_secret(None).await?; - let n_required: u16 = term.ask(false, "Enter the minimum number of signatures required: ").await?.parse()?; + let minimum_signatures: u16 = term.ask(false, "Enter the minimum number of signatures required: ").await?.parse()?; let prv_keys_len: usize = term.ask(false, "Enter the number of private keys to generate: ").await?.parse()?; - let mut prv_key_data_ids = Vec::with_capacity(prv_keys_len); - let mut mnemonics = Vec::with_capacity(prv_keys_len); + let mut prv_key_data_args = Vec::with_capacity(prv_keys_len); for _ in 0..prv_keys_len { - let prv_key_data_args = PrvKeyDataCreateArgs::new(None, wallet_secret.clone(), None); // can be optimized with Rc - let (prv_key_data_id, mnemonic) = wallet.create_prv_key_data(prv_key_data_args).await?; + let bip39_mnemonic = Mnemonic::random(mnemonic_phrase_word_count, Language::default())?.phrase().to_string(); + + let prv_key_data_create_args = PrvKeyDataCreateArgs::new(None, None, bip39_mnemonic); // can be optimized with Rc + let prv_key_data_id = wallet.create_prv_key_data(&wallet_secret, prv_key_data_create_args).await?; - prv_key_data_ids.push(prv_key_data_id); - mnemonics.push(mnemonic); + prv_key_data_args.push(PrvKeyDataArgs::new(prv_key_data_id, None)); } let additional_xpub_keys_len: usize = term.ask(false, "Enter the number of additional extended public keys: ").await?.parse()?; @@ -74,16 +78,9 @@ async fn create_multisig(ctx: &Arc, title: Option, name: Option let xpub_key = term.ask(false, &format!("Enter extended public {i} key: ")).await?; xpub_keys.push(xpub_key.trim().to_owned()); } - let account = wallet - .create_multisig_account(MultisigCreateArgs { - prv_key_data_ids, - name, - title, - wallet_secret, - additional_xpub_keys: xpub_keys, - minimum_signatures: n_required, - }) - .await?; + let account = + wallet.create_account_multisig(&wallet_secret, prv_key_data_args, xpub_keys, account_name, minimum_signatures).await?; + tprintln!(ctx, "\naccount created: {}\n", account.get_list_string()?); wallet.select(Some(&account)).await?; Ok(()) diff --git a/cli/src/wizards/import.rs b/cli/src/wizards/import.rs index d588e5633..bdce68fa2 100644 --- a/cli/src/wizards/import.rs +++ b/cli/src/wizards/import.rs @@ -3,7 +3,7 @@ use crate::imports::*; use crate::result::Result; use crate::KashCli; use kash_bip32::{Language, Mnemonic}; -use kash_wallet_core::storage::AccountKind; +use kash_wallet_core::account::{BIP32_ACCOUNT_KIND, LEGACY_ACCOUNT_KIND, MULTISIG_ACCOUNT_KIND}; use std::sync::Arc; pub async fn prompt_for_mnemonic(term: &Arc) -> Result> { @@ -53,15 +53,15 @@ pub(crate) async fn import_with_mnemonic(ctx: &Arc, account_kind: Accou let mnemonic = prompt_for_mnemonic(&term).await?; tprintln!(ctx); let length = mnemonic.len(); - match account_kind { - AccountKind::Legacy if length != 12 => Err(Error::Custom(format!("wrong mnemonic length ({length})"))), - AccountKind::Bip32 if length != 24 => Err(Error::Custom(format!("wrong mnemonic length ({length})"))), + match account_kind.as_ref() { + LEGACY_ACCOUNT_KIND if length != 12 => Err(Error::Custom(format!("wrong mnemonic length ({length})"))), + BIP32_ACCOUNT_KIND if length != 24 => Err(Error::Custom(format!("wrong mnemonic length ({length})"))), - AccountKind::Legacy | AccountKind::Bip32 | AccountKind::MultiSig => Ok(()), + LEGACY_ACCOUNT_KIND | BIP32_ACCOUNT_KIND | MULTISIG_ACCOUNT_KIND => Ok(()), _ => Err(Error::Custom("unsupported account kind".to_owned())), }?; - let payment_secret = if account_kind == AccountKind::Legacy { + let payment_secret = if account_kind == LEGACY_ACCOUNT_KIND { None } else { tpara!( @@ -92,8 +92,8 @@ pub(crate) async fn import_with_mnemonic(ctx: &Arc, account_kind: Accou let mnemonic = mnemonic.join(" "); let mnemonic = Mnemonic::new(mnemonic.trim(), Language::English)?; - let account = if !matches!(account_kind, AccountKind::MultiSig) { - wallet.import_with_mnemonic(wallet_secret, payment_secret.as_ref(), mnemonic, account_kind).await? + let account = if account_kind != MULTISIG_ACCOUNT_KIND { + wallet.import_with_mnemonic(&wallet_secret, payment_secret.as_ref(), mnemonic, account_kind).await? } else { let mut mnemonics_secrets = vec![(mnemonic, payment_secret)]; while matches!( @@ -123,7 +123,7 @@ pub(crate) async fn import_with_mnemonic(ctx: &Arc, account_kind: Accou } let n_required: u16 = term.ask(false, "Enter the minimum number of signatures required: ").await?.parse()?; - wallet.import_multisig_with_mnemonic(wallet_secret, mnemonics_secrets, n_required, additional_xpubs).await? + wallet.import_multisig_with_mnemonic(&wallet_secret, mnemonics_secrets, n_required, additional_xpubs).await? }; tprintln!(ctx, "\naccount imported: {}\n", account.get_list_string()?); diff --git a/cli/src/wizards/wallet.rs b/cli/src/wizards/wallet.rs index 5d53dac2f..2164d236a 100644 --- a/cli/src/wizards/wallet.rs +++ b/cli/src/wizards/wallet.rs @@ -1,13 +1,16 @@ use crate::cli::KashCli; use crate::imports::*; use crate::result::Result; -use kash_wallet_core::runtime::{PrvKeyDataCreateArgs, WalletCreateArgs}; -use kash_wallet_core::storage::{make_filename, AccessContextT, AccountKind, Hint}; +use kash_bip32::{Language, Mnemonic, WordCount}; +use kash_wallet_core::storage::{make_filename, Hint}; pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_mnemonic: bool) -> Result<()> { let term = ctx.term(); let wallet = ctx.wallet(); + // TODO @aspect + let word_count = WordCount::Words12; + if let Err(err) = wallet.network_id() { tprintln!(ctx); tprintln!(ctx, "Before creating a wallet, you need to select a Kash network."); @@ -29,9 +32,7 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_m } } - let account_title = term.ask(false, "Default account title: ").await?.trim().to_string(); - let account_name = account_title.replace(' ', "-").to_lowercase(); - let account_title = account_title.is_not_empty().then_some(account_title); + let account_name = term.ask(false, "Default account title: ").await?.trim().to_string(); let account_name = account_name.is_not_empty().then_some(account_name); tpara!( @@ -109,26 +110,31 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_m let prv_key_data_args = if import_with_mnemonic { let words = crate::wizards::import::prompt_for_mnemonic(&term).await?; - PrvKeyDataCreateArgs::new_with_mnemonic(None, wallet_secret.clone(), payment_secret.clone(), words.join(" ")) + PrvKeyDataCreateArgs::new(None, payment_secret.clone(), words.join(" ")) } else { - PrvKeyDataCreateArgs::new(None, wallet_secret.clone(), payment_secret.clone()) + PrvKeyDataCreateArgs::new( + None, + payment_secret.clone(), + Mnemonic::random(word_count, Language::default())?.phrase().to_string(), + ) }; + let mnemonic_phrase = prv_key_data_args.mnemonic.clone(); + let notifier = ctx.notifier().show(Notification::Processing).await; // suspend commits for multiple operations wallet.store().batch().await?; - let account_kind = AccountKind::Bip32; - let wallet_args = WalletCreateArgs::new(name.map(String::from), None, hint, wallet_secret.clone(), true); - let account_args = AccountCreateArgs::new(account_name, account_title, account_kind, wallet_secret.clone(), payment_secret); - let descriptor = ctx.wallet().create_wallet(wallet_args).await?; - let (prv_key_data_id, mnemonic) = wallet.create_prv_key_data(prv_key_data_args).await?; - let account = wallet.create_bip32_account(prv_key_data_id, account_args).await?; + let wallet_args = WalletCreateArgs::new(name.map(String::from), None, EncryptionKind::XChaCha20Poly1305, hint, true); + let (_wallet_descriptor, storage_descriptor) = ctx.wallet().create_wallet(&wallet_secret, wallet_args).await?; + let prv_key_data_id = wallet.create_prv_key_data(&wallet_secret, prv_key_data_args).await?; + + let account_args = AccountCreateArgsBip32::new(account_name, None); + let account = wallet.create_account_bip32(&wallet_secret, prv_key_data_id, payment_secret.as_ref(), account_args).await?; // flush data to storage - let access_ctx: Arc = Arc::new(AccessContext::new(wallet_secret.clone())); - wallet.store().flush(&access_ctx).await?; + wallet.store().flush(&wallet_secret).await?; notifier.hide(); @@ -153,23 +159,22 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_m // descriptor - ["", "Never share your mnemonic with anyone!", "---", "", "Your default wallet account mnemonic:", mnemonic.phrase()] + ["", "Never share your mnemonic with anyone!", "---", "", "Your default wallet account mnemonic:", mnemonic_phrase.as_str()] .into_iter() .for_each(|line| term.writeln(line)); } term.writeln(""); - if let Some(descriptor) = descriptor { - term.writeln(format!("Your wallet is stored in: {}", descriptor)); - term.writeln(""); - } + term.writeln(format!("Your wallet is stored in: {}", storage_descriptor)); + term.writeln(""); let receive_address = account.receive_address()?; term.writeln("Your default account deposit address:"); term.writeln(style(receive_address).blue().to_string()); term.writeln(""); - wallet.load_and_activate(wallet_secret, name.map(String::from)).await?; + wallet.open(&wallet_secret, name.map(String::from), WalletOpenArgs::default_with_legacy_accounts()).await?; + wallet.activate_accounts(None).await?; Ok(()) } diff --git a/components/consensusmanager/src/session.rs b/components/consensusmanager/src/session.rs index 00f1734c9..009f87cb1 100644 --- a/components/consensusmanager/src/session.rs +++ b/components/consensusmanager/src/session.rs @@ -222,6 +222,10 @@ impl ConsensusSessionOwned { self.clone().spawn_blocking(|c| c.get_virtual_parents()).await } + pub async fn async_get_virtual_parents_len(&self) -> usize { + self.clone().spawn_blocking(|c| c.get_virtual_parents_len()).await + } + pub async fn async_get_virtual_utxos( &self, from_outpoint: Option, @@ -235,6 +239,10 @@ impl ConsensusSessionOwned { self.clone().spawn_blocking(|c| c.get_tips()).await } + pub async fn async_get_tips_len(&self) -> usize { + self.clone().spawn_blocking(|c| c.get_tips_len()).await + } + pub async fn async_is_chain_ancestor_of(&self, low: Hash, high: Hash) -> ConsensusResult { self.clone().spawn_blocking(move |c| c.is_chain_ancestor_of(low, high)).await } diff --git a/consensus/core/benches/serde_benchmark.rs b/consensus/core/benches/serde_benchmark.rs index 371288e5e..a15951e78 100644 --- a/consensus/core/benches/serde_benchmark.rs +++ b/consensus/core/benches/serde_benchmark.rs @@ -2,7 +2,7 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use kash_consensus_core::asset_type::AssetType::KSH; use kash_consensus_core::subnets::SUBNETWORK_ID_COINBASE; use kash_consensus_core::tx::{ - ScriptPublicKey, Transaction, TransactionId, TransactionInput, TransactionAction, TransactionOutpoint, TransactionOutput, + ScriptPublicKey, Transaction, TransactionAction, TransactionId, TransactionInput, TransactionOutpoint, TransactionOutput, }; use smallvec::smallvec; use std::time::{Duration, Instant}; diff --git a/consensus/core/src/api/mod.rs b/consensus/core/src/api/mod.rs index 46db65a34..06ec2117b 100644 --- a/consensus/core/src/api/mod.rs +++ b/consensus/core/src/api/mod.rs @@ -145,6 +145,10 @@ pub trait ConsensusApi: Send + Sync { unimplemented!() } + fn get_virtual_parents_len(&self) -> usize { + unimplemented!() + } + fn get_virtual_utxos( &self, from_outpoint: Option, @@ -158,6 +162,10 @@ pub trait ConsensusApi: Send + Sync { unimplemented!() } + fn get_tips_len(&self) -> usize { + unimplemented!() + } + fn modify_coinbase_payload(&self, payload: Vec, miner_data: &MinerData) -> CoinbaseResult> { unimplemented!() } diff --git a/consensus/core/src/config/params.rs b/consensus/core/src/config/params.rs index c973730ca..bee10a6cb 100644 --- a/consensus/core/src/config/params.rs +++ b/consensus/core/src/config/params.rs @@ -197,7 +197,7 @@ impl Params { } /// Returns the depth at which the anticone of a chain block is final (i.e., is a permanently closed set). - /// Based on the analysis at https://github.com/kashnet/docs/blob/main/Reference/prunality/Prunality.pdf + /// Based on the analysis at /// and on the decomposition of merge depth (rule R-I therein) from finality depth (φ) pub fn anticone_finalization_depth(&self) -> u64 { let anticone_finalization_depth = self.finality_depth diff --git a/consensus/core/src/sign.rs b/consensus/core/src/sign.rs index 1b06772a3..99d438ad1 100644 --- a/consensus/core/src/sign.rs +++ b/consensus/core/src/sign.rs @@ -17,6 +17,65 @@ pub enum Error { #[error("Secp256k1 -> {0}")] Secp256k1Error(#[from] secp256k1::Error), + + #[error("The transaction is partially signed")] + PartiallySigned, + + #[error("The transaction is fully signed")] + FullySigned, +} + +/// A wrapper enum that represents the transaction signed state. A transaction +/// contained by this enum can be either fully signed or partially signed. +pub enum Signed { + Fully(SignableTransaction), + Partially(SignableTransaction), +} + +impl Signed { + /// Returns the transaction if it is fully signed, otherwise returns an error + pub fn fully_signed(self) -> std::result::Result { + match self { + Signed::Fully(tx) => Ok(tx), + Signed::Partially(_) => Err(Error::PartiallySigned), + } + } + + /// Returns the transaction if it is fully signed, otherwise returns the + /// transaction as an error `Err(tx)`. + #[allow(clippy::result_large_err)] + pub fn try_fully_signed(self) -> std::result::Result { + match self { + Signed::Fully(tx) => Ok(tx), + Signed::Partially(tx) => Err(tx), + } + } + + /// Returns the transaction if it is partially signed, otherwise fail with an error + pub fn partially_signed(self) -> std::result::Result { + match self { + Signed::Fully(_) => Err(Error::FullySigned), + Signed::Partially(tx) => Ok(tx), + } + } + + /// Returns the transaction if it is partially signed, otherwise returns the + /// transaction as an error `Err(tx)`. + #[allow(clippy::result_large_err)] + pub fn try_partially_signed(self) -> std::result::Result { + match self { + Signed::Fully(tx) => Err(tx), + Signed::Partially(tx) => Ok(tx), + } + } + + /// Returns the transaction regardless of whether it is fully or partially signed + pub fn unwrap(self) -> SignableTransaction { + match self { + Signed::Fully(tx) => tx, + Signed::Partially(tx) => tx, + } + } } /// Sign a transaction using schnorr @@ -63,7 +122,8 @@ pub fn sign_with_multiple(mut mutable_tx: SignableTransaction, privkeys: Vec<[u8 /// TODO (aspect) - merge this with `v1` fn above or refactor wallet core to use the script engine. /// Sign a transaction using schnorr -pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: Vec<[u8; 32]>) -> SignableTransaction { +#[allow(clippy::result_large_err)] +pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: Vec<[u8; 32]>) -> Signed { let mut map = BTreeMap::new(); for privkey in privkeys { let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, &privkey).unwrap(); @@ -73,6 +133,7 @@ pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: Vec< } let mut reused_values = SigHashReusedValues::new(); + let mut additional_signatures_required = false; for i in 0..mutable_tx.tx.inputs.len() { let script = mutable_tx.entries[i].as_ref().unwrap().script_public_key.script(); if let Some(schnorr_key) = map.get(&script.to_vec()) { @@ -81,9 +142,15 @@ pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: Vec< let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref(); // This represents OP_DATA_65 (since signature length is 64 bytes and SIGHASH_TYPE is one byte) mutable_tx.tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect(); + } else { + additional_signatures_required = true; } } - mutable_tx + if additional_signatures_required { + Signed::Partially(mutable_tx) + } else { + Signed::Fully(mutable_tx) + } } pub fn verify(tx: &impl crate::tx::VerifiableTransaction) -> Result<(), Error> { diff --git a/consensus/core/src/subnets.rs b/consensus/core/src/subnets.rs index 73dd3c5b4..37dc4c338 100644 --- a/consensus/core/src/subnets.rs +++ b/consensus/core/src/subnets.rs @@ -9,9 +9,15 @@ use kash_utils::{serde_impl_deser_fixed_bytes_ref, serde_impl_ser_fixed_bytes_re pub const SUBNETWORK_ID_SIZE: usize = 20; /// The domain representation of a Subnetwork ID -#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, BorshSerialize, BorshDeserialize, BorshSchema)] +#[derive(Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, BorshSerialize, BorshDeserialize, BorshSchema)] pub struct SubnetworkId([u8; SUBNETWORK_ID_SIZE]); +impl Debug for SubnetworkId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SubnetworkId").field("", &self.to_hex()).finish() + } +} + serde_impl_ser_fixed_bytes_ref!(SubnetworkId, SUBNETWORK_ID_SIZE); serde_impl_deser_fixed_bytes_ref!(SubnetworkId, SUBNETWORK_ID_SIZE); diff --git a/consensus/core/src/tx.rs b/consensus/core/src/tx.rs index 468cf0892..d23060078 100644 --- a/consensus/core/src/tx.rs +++ b/consensus/core/src/tx.rs @@ -1,9 +1,9 @@ mod script_public_key; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use kash_utils::hex::ToHex; use kash_utils::{serde_bytes, serde_bytes_fixed_ref}; pub use script_public_key::{scriptvec, ScriptPublicKey, ScriptPublicKeyVersion, ScriptPublicKeys, ScriptVec, SCRIPT_VECTOR_SIZE}; - use serde::{Deserialize, Serialize}; use std::{ fmt::Display, @@ -78,7 +78,7 @@ impl Display for TransactionOutpoint { } /// Represents a Kash transaction input -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, BorshSchema)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, BorshSchema)] #[serde(rename_all = "camelCase")] pub struct TransactionInput { pub previous_outpoint: TransactionOutpoint, @@ -94,6 +94,17 @@ impl TransactionInput { } } +impl std::fmt::Debug for TransactionInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransactionInput") + .field("previous_outpoint", &self.previous_outpoint) + .field("signature_script", &self.signature_script.to_hex()) + .field("sequence", &self.sequence) + .field("sig_op_count", &self.sig_op_count) + .finish() + } +} + /// Represents a Kashd transaction output #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, BorshSchema)] #[serde(rename_all = "camelCase")] @@ -112,7 +123,7 @@ impl TransactionOutput { /// Defines the kind of a transaction #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] #[serde(rename_all = "camelCase")] -#[wasm_bindgen(js_name = transactionAction)] +#[wasm_bindgen(js_name = TransactionAction)] #[derive(Default)] pub enum TransactionAction { /// Regular KSH transfer: KSH -> KSH @@ -227,7 +238,7 @@ impl TryFrom for TransactionAction { } /// Represents a Kash transaction -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct Transaction { pub version: u16, diff --git a/consensus/core/src/tx/script_public_key.rs b/consensus/core/src/tx/script_public_key.rs index a6c2383f3..f3d3c5be6 100644 --- a/consensus/core/src/tx/script_public_key.rs +++ b/consensus/core/src/tx/script_public_key.rs @@ -34,13 +34,19 @@ use wasm_bindgen::prelude::wasm_bindgen; pub type ScriptPublicKeys = HashSet; /// Represents a Kashd ScriptPublicKey -#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)] +#[derive(Default, PartialEq, Eq, Clone, Hash)] #[wasm_bindgen(inspectable)] pub struct ScriptPublicKey { pub version: ScriptPublicKeyVersion, pub(super) script: ScriptVec, // Kept private to preserve read-only semantics } +impl std::fmt::Debug for ScriptPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ScriptPublicKey").field("version", &self.version).field("script", &self.script.to_hex()).finish() + } +} + impl FromHex for ScriptPublicKey { type Error = faster_hex::Error; diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index 6b0d857f6..4d0f1aa69 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -566,6 +566,10 @@ impl ConsensusApi for Consensus { self.virtual_stores.read().state.get().unwrap().parents.iter().copied().collect() } + fn get_virtual_parents_len(&self) -> usize { + self.virtual_stores.read().state.get().unwrap().parents.len() + } + fn get_virtual_utxos( &self, from_outpoint: Option, @@ -581,6 +585,10 @@ impl ConsensusApi for Consensus { self.body_tips_store.read().get().unwrap().read().iter().copied().collect_vec() } + fn get_tips_len(&self) -> usize { + self.body_tips_store.read().get().unwrap().read().len() + } + fn get_pruning_point_utxos( &self, expected_pruning_point: Hash, diff --git a/consensus/src/processes/transaction_validator/transaction_validator_populated.rs b/consensus/src/processes/transaction_validator/transaction_validator_populated.rs index b84b04321..5858be052 100644 --- a/consensus/src/processes/transaction_validator/transaction_validator_populated.rs +++ b/consensus/src/processes/transaction_validator/transaction_validator_populated.rs @@ -120,7 +120,7 @@ impl TransactionValidator { Ok(()) } - fn check_scripts(&self, tx: &impl VerifiableTransaction) -> TxResult<()> { + pub fn check_scripts(&self, tx: &impl VerifiableTransaction) -> TxResult<()> { let mut reused_values = SigHashReusedValues::new(); for (i, (input, entry)) in tx.populated_inputs().enumerate() { let mut engine = TxScriptEngine::from_transaction_input(tx, input, i, entry, &mut reused_values, &self.sig_cache) diff --git a/consensus/wasm/src/keypair.rs b/consensus/wasm/src/keypair.rs index 15e98fa34..7eda84519 100644 --- a/consensus/wasm/src/keypair.rs +++ b/consensus/wasm/src/keypair.rs @@ -1,5 +1,5 @@ //! -//! [`keypair`](mod@keypair) module encapsulates [`Keypair`] and [`PrivateKey`]. +//! [`keypair`](mod@self) module encapsulates [`Keypair`] and [`PrivateKey`]. //! The [`Keypair`] provides access to the secret and public keys. //! //! ```javascript @@ -22,6 +22,8 @@ use crate::result::Result; use js_sys::{Array, Uint8Array}; use kash_addresses::{Address, Version as AddressVersion}; use kash_consensus_core::network::wasm::Network; +#[allow(unused_imports)] // needed for rust doc! +use kash_consensus_core::network::NetworkType; use secp256k1::{Secp256k1, XOnlyPublicKey}; use serde_wasm_bindgen::to_value; use std::str::FromStr; diff --git a/consensus/wasm/src/outpoint.rs b/consensus/wasm/src/outpoint.rs index 08dc9460d..7ea4c75dd 100644 --- a/consensus/wasm/src/outpoint.rs +++ b/consensus/wasm/src/outpoint.rs @@ -143,7 +143,7 @@ impl From for cctx::TransactionOutpoint { } impl TransactionOutpoint { - pub fn fake() -> Self { - Self::new(TransactionId::from_slice(&[0; kash_hashes::HASH_SIZE]), 0) + pub fn simulated() -> Self { + Self::new(TransactionId::from_slice(&rand::random::<[u8; kash_hashes::HASH_SIZE]>()), 0) } } diff --git a/consensus/wasm/src/signer.rs b/consensus/wasm/src/signer.rs index 8723026af..6130a9061 100644 --- a/consensus/wasm/src/signer.rs +++ b/consensus/wasm/src/signer.rs @@ -68,8 +68,10 @@ fn sign_transaction_impl( } /// Sign a transaction using schnorr, returns a new transaction with the signatures added. +/// The resulting transaction may be partially signed if the supplied keys are not sufficient +/// to sign all of its inputs. pub fn sign(mutable_tx: tx::SignableTransaction, privkeys: Vec<[u8; 32]>) -> Result { - Ok(sign_with_multiple_v2(mutable_tx, privkeys)) + Ok(sign_with_multiple_v2(mutable_tx, privkeys).unwrap()) } #[wasm_bindgen(js_name=signScriptHash)] diff --git a/consensus/wasm/src/transaction.rs b/consensus/wasm/src/transaction.rs index 4b4749389..f14c45a06 100644 --- a/consensus/wasm/src/transaction.rs +++ b/consensus/wasm/src/transaction.rs @@ -154,12 +154,12 @@ impl Transaction { #[wasm_bindgen(getter, js_name = gas)] pub fn get_gas(&self) -> u64 { - self.inner().lock_time + self.inner().gas } #[wasm_bindgen(setter, js_name = gas)] pub fn set_gas(&self, v: u64) { - self.inner().lock_time = v; + self.inner().gas = v; } #[wasm_bindgen(getter = subnetworkId)] diff --git a/consensus/wasm/src/txscript.rs b/consensus/wasm/src/txscript.rs index e9455a0db..cf23c867b 100644 --- a/consensus/wasm/src/txscript.rs +++ b/consensus/wasm/src/txscript.rs @@ -59,11 +59,11 @@ impl ScriptBuilder { /// chooses canonical opcodes depending on the length of the data. /// /// A zero length buffer will lead to a push of empty data onto the stack (Op0 = OpFalse) - /// and any push of data greater than [`MAX_SCRIPT_ELEMENT_SIZE`] will not modify + /// and any push of data greater than [`MAX_SCRIPT_ELEMENT_SIZE`](kash_txscript::MAX_SCRIPT_ELEMENT_SIZE) will not modify /// the script since that is not allowed by the script engine. /// /// Also, the script will not be modified if pushing the data would cause the script to - /// exceed the maximum allowed script engine size [`MAX_SCRIPTS_SIZE`]. + /// exceed the maximum allowed script engine size [`MAX_SCRIPTS_SIZE`](kash_txscript::MAX_SCRIPTS_SIZE). #[wasm_bindgen(js_name = "addData")] pub fn add_data(&self, data: JsValue) -> Result { let data = data.try_as_vec_u8()?; diff --git a/consensus/wasm/src/utxo.rs b/consensus/wasm/src/utxo.rs index a4635acf7..da237f179 100644 --- a/consensus/wasm/src/utxo.rs +++ b/consensus/wasm/src/utxo.rs @@ -102,6 +102,12 @@ impl UtxoEntryReference { } } +impl std::hash::Hash for UtxoEntryReference { + fn hash(&self, state: &mut H) { + self.id().hash(state); + } +} + impl AsRef for UtxoEntryReference { fn as_ref(&self) -> &UtxoEntry { &self.utxo @@ -255,7 +261,7 @@ impl TryFrom for UtxoEntries { type Error = Error; fn try_from(js_value: JsValue) -> std::result::Result { if !js_value.is_array() { - return Err("Data type spplied to UtxoEntries must be an Array".into()); + return Err("Data type supplied to UtxoEntries must be an Array".into()); } Ok(Self(Arc::new(js_value.try_into_utxo_entry_references()?))) @@ -302,15 +308,15 @@ impl TryFrom<&JsValue> for UtxoEntryReference { impl UtxoEntryReference { // Creates a fake UtxoEntryReference with specified amount. - pub fn fake(amount: u64) -> Self { + pub fn simulated(amount: u64) -> Self { use kash_addresses::{Prefix, Version}; - let address = Address::new(Prefix::Testnet, Version::PubKey, &[0; 32]); - Self::fake_with_address(amount, &address) + let address = Address::new(Prefix::Testnet, Version::PubKey, &rand::random::<[u8; 32]>()); + Self::simulated_with_address(amount, &address) } // Creates a fake UtxoEntryReference with specified amount and address. - pub fn fake_with_address(amount: u64, address: &Address) -> Self { - let outpoint = TransactionOutpoint::fake(); + pub fn simulated_with_address(amount: u64, address: &Address) -> Self { + let outpoint = TransactionOutpoint::simulated(); let script_public_key = kash_txscript::pay_to_address_script(address); let block_daa_score = 0; let is_coinbase = true; diff --git a/core/src/log/mod.rs b/core/src/log/mod.rs index ab35578ff..4857c44dd 100644 --- a/core/src/log/mod.rs +++ b/core/src/log/mod.rs @@ -4,7 +4,7 @@ //! crate log (ie. `log.workspace = true`) when target architecture is not wasm32. #[allow(unused_imports)] -use log::{Level, LevelFilter}; +pub use log::{Level, LevelFilter}; cfg_if::cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { diff --git a/crypto/addresses/src/lib.rs b/crypto/addresses/src/lib.rs index e2868700f..48445d0a5 100644 --- a/crypto/addresses/src/lib.rs +++ b/crypto/addresses/src/lib.rs @@ -11,25 +11,25 @@ mod bech32; #[derive(Error, PartialEq, Eq, Debug, Clone)] pub enum AddressError { - #[error("Invalid prefix {0}")] + #[error("The address has an invalid prefix {0}")] InvalidPrefix(String), - #[error("Prefix is missing")] + #[error("The address prefix is missing")] MissingPrefix, - #[error("Invalid version {0}")] + #[error("The address has an invalid version {0}")] InvalidVersion(u8), - #[error("Invalid version {0}")] + #[error("The address has an invalid version {0}")] InvalidVersionString(String), - #[error("Invalid character {0}")] + #[error("The address contains an invalid character {0}")] DecodingError(char), - #[error("Checksum is invalid")] + #[error("The address checksum is invalid")] BadChecksum, - #[error("Invalid address")] + #[error("The address is invalid")] InvalidAddress, #[error("{0}")] @@ -180,7 +180,7 @@ pub const PAYLOAD_VECTOR_SIZE: usize = 36; pub type PayloadVec = SmallVec<[u8; PAYLOAD_VECTOR_SIZE]>; /// Kash `Address` struct that serializes to and from an address format string: `kash:qz0s...t8cv`. -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Hash)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] #[wasm_bindgen(inspectable)] pub struct Address { #[wasm_bindgen(skip)] @@ -191,6 +191,16 @@ pub struct Address { pub payload: PayloadVec, } +impl std::fmt::Debug for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.version == Version::PubKey { + write!(f, "{}", String::from(self)) + } else { + write!(f, "{} ({})", String::from(self), self.version) + } + } +} + impl Address { pub fn new(prefix: Prefix, version: Version, payload: &[u8]) -> Self { if !prefix.is_test() { @@ -209,27 +219,27 @@ impl Address { /// Convert an address to a string. #[wasm_bindgen(js_name = toString)] - pub fn to_str(&self) -> String { + pub fn address_to_string(&self) -> String { self.into() } - #[wasm_bindgen(getter)] - pub fn version(&self) -> String { + #[wasm_bindgen(getter, js_name = "version")] + pub fn version_to_string(&self) -> String { self.version.to_string() } - #[wasm_bindgen(getter)] - pub fn prefix(&self) -> String { + #[wasm_bindgen(getter, js_name = "prefix")] + pub fn prefix_to_string(&self) -> String { self.prefix.to_string() } - #[wasm_bindgen(setter)] - pub fn set_prefix(&mut self, prefix: &str) { + #[wasm_bindgen(setter, js_name = "setPrefix")] + pub fn set_prefix_from_str(&mut self, prefix: &str) { self.prefix = Prefix::try_from(prefix).unwrap_or_else(|err| panic!("Address::prefix() - invalid prefix `{prefix}`: {err}")); } - #[wasm_bindgen(getter)] - pub fn payload(&self) -> String { + #[wasm_bindgen(getter, js_name = "payload")] + pub fn payload_to_string(&self) -> String { self.encode_payload() } @@ -242,7 +252,7 @@ impl Address { impl Display for Address { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.to_str()) + write!(f, "{}", String::from(self)) } } @@ -517,7 +527,7 @@ impl From for Vec
{ impl TryFrom for AddressList { type Error = AddressError; fn try_from(js_value: JsValue) -> Result { - js_value.try_into() + js_value.as_ref().try_into() } } @@ -632,7 +642,12 @@ mod tests { let expected = Address::constructor("kash:qpauqsvk7yf9unexwmxsnmg547mhyga37csh0kj53q6xxgl24ydxjh3y20yhf"); use web_sys::console; - console::log_4(&"address: ".into(), &expected.version().into(), &expected.prefix().into(), &expected.payload().into()); + console::log_4( + &"address: ".into(), + &expected.version_to_string().into(), + &expected.prefix_to_string().into(), + &expected.payload_to_string().into(), + ); let obj = Object::new(); obj.set("version", &JsValue::from_str("PubKey")).unwrap(); diff --git a/crypto/txscript/src/opcodes/mod.rs b/crypto/txscript/src/opcodes/mod.rs index b42e0530d..fe360fa9e 100644 --- a/crypto/txscript/src/opcodes/mod.rs +++ b/crypto/txscript/src/opcodes/mod.rs @@ -982,8 +982,8 @@ mod test { use kash_consensus_core::hashing::sighash::SigHashReusedValues; use kash_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kash_consensus_core::tx::{ - PopulatedTransaction, ScriptPublicKey, Transaction, TransactionInput, TransactionAction, TransactionOutpoint, TransactionOutput, - UtxoEntry, VerifiableTransaction, + PopulatedTransaction, ScriptPublicKey, Transaction, TransactionAction, TransactionInput, TransactionOutpoint, + TransactionOutput, UtxoEntry, VerifiableTransaction, }; struct TestCase<'a> { diff --git a/indexes/core/src/indexed_utxos.rs b/indexes/core/src/indexed_utxos.rs index 8566667ba..ea6176c79 100644 --- a/indexes/core/src/indexed_utxos.rs +++ b/indexes/core/src/indexed_utxos.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; // One possible implementation: u64 of transaction id xor'd with 4 bytes of transaction index. pub type CompactUtxoCollection = HashMap; -/// A collection of utxos indexed via; [`ScriptPublicKey`] => [`TransactionOutpoint`] => [`CompactUtxo`]. +/// A collection of utxos indexed via; [`ScriptPublicKey`] => [`TransactionOutpoint`] => [`CompactUtxoEntry`]. pub type UtxoSetByScriptPublicKey = HashMap; /// A map of balance by script public key diff --git a/kashd/src/args.rs b/kashd/src/args.rs index 55c7c2271..98ee8aa92 100644 --- a/kashd/src/args.rs +++ b/kashd/src/args.rs @@ -8,6 +8,7 @@ use kash_addresses::Address; use kash_consensus_core::tx::{TransactionOutpoint, UtxoEntry}; #[cfg(feature = "devnet-prealloc")] use kash_txscript::pay_to_address_script; +use std::ffi::OsString; #[cfg(feature = "devnet-prealloc")] use std::sync::Arc; @@ -333,7 +334,8 @@ pub fn cli() -> Command { .help("Interval in seconds for performance metrics collection."), ) .arg(arg!(--"disable-upnp" "Disable upnp")) - .arg(arg!(--"nodnsseed" "Disable DNS seeding for peers")); + .arg(arg!(--"nodnsseed" "Disable DNS seeding for peers")) + ; #[cfg(feature = "devnet-prealloc")] let cmd = cmd @@ -345,55 +347,72 @@ pub fn cli() -> Command { } pub fn parse_args() -> Args { - let m: clap::ArgMatches = cli().get_matches(); - let defaults: Args = Default::default(); + match Args::parse(std::env::args_os()) { + Ok(args) => args, + Err(err) => { + println!("{err}"); + std::process::exit(1); + } + } +} - Args { - appdir: m.get_one::("appdir").cloned(), - logdir: m.get_one::("logdir").cloned(), - no_log_files: m.get_one::("nologfiles").cloned().unwrap_or(defaults.no_log_files), - rpclisten: m.get_one::("rpclisten").cloned(), - rpclisten_borsh: m.get_one::("rpclisten-borsh").cloned(), - rpclisten_json: m.get_one::("rpclisten-json").cloned(), - unsafe_rpc: m.get_one::("unsaferpc").cloned().unwrap_or(defaults.unsafe_rpc), - wrpc_verbose: false, - log_level: m.get_one::("log_level").cloned().unwrap(), - async_threads: m.get_one::("async_threads").cloned().unwrap_or(defaults.async_threads), - connect_peers: m.get_many::("connect-peers").unwrap_or_default().copied().collect(), - add_peers: m.get_many::("add-peers").unwrap_or_default().copied().collect(), - listen: m.get_one::("listen").cloned(), - outbound_target: m.get_one::("outpeers").cloned().unwrap_or(defaults.outbound_target), - inbound_limit: m.get_one::("maxinpeers").cloned().unwrap_or(defaults.inbound_limit), - rpc_max_clients: m.get_one::("rpcmaxclients").cloned().unwrap_or(defaults.rpc_max_clients), - reset_db: m.get_one::("reset-db").cloned().unwrap_or(defaults.reset_db), - enable_unsynced_mining: m.get_one::("enable-unsynced-mining").cloned().unwrap_or(defaults.enable_unsynced_mining), - enable_mainnet_mining: m.get_one::("enable-mainnet-mining").cloned().unwrap_or(defaults.enable_mainnet_mining), - utxoindex: m.get_one::("utxoindex").cloned().unwrap_or(defaults.utxoindex), - testnet: m.get_one::("testnet").cloned().unwrap_or(defaults.testnet), - testnet_suffix: m.get_one::("netsuffix").cloned().unwrap_or(defaults.testnet_suffix), - devnet: m.get_one::("devnet").cloned().unwrap_or(defaults.devnet), - simnet: m.get_one::("simnet").cloned().unwrap_or(defaults.simnet), - archival: m.get_one::("archival").cloned().unwrap_or(defaults.archival), - sanity: m.get_one::("sanity").cloned().unwrap_or(defaults.sanity), - yes: m.get_one::("yes").cloned().unwrap_or(defaults.yes), - user_agent_comments: m.get_many::("user_agent_comments").unwrap_or_default().cloned().collect(), - externalip: m.get_one::("externalip").cloned(), - perf_metrics: m.get_one::("perf-metrics").cloned().unwrap_or(defaults.perf_metrics), - perf_metrics_interval_sec: m - .get_one::("perf-metrics-interval-sec") - .cloned() - .unwrap_or(defaults.perf_metrics_interval_sec), - // Note: currently used programmatically by benchmarks and not exposed to CLI users - block_template_cache_lifetime: defaults.block_template_cache_lifetime, +impl Args { + pub fn parse(itr: I) -> Result + where + I: IntoIterator, + T: Into + Clone, + { + let m: clap::ArgMatches = cli().try_get_matches_from(itr)?; + let defaults: Args = Default::default(); - #[cfg(feature = "devnet-prealloc")] - num_prealloc_utxos: m.get_one::("num-prealloc-utxos").cloned(), - #[cfg(feature = "devnet-prealloc")] - prealloc_address: m.get_one::("prealloc-address").cloned(), - #[cfg(feature = "devnet-prealloc")] - prealloc_amount: m.get_one::("prealloc-amount").cloned().unwrap_or(defaults.prealloc_amount), - disable_upnp: m.get_one::("disable-upnp").cloned().unwrap_or(defaults.disable_upnp), - disable_dns_seeding: m.get_one::("nodnsseed").cloned().unwrap_or(defaults.disable_dns_seeding), + let args = Args { + appdir: m.get_one::("appdir").cloned(), + logdir: m.get_one::("logdir").cloned(), + no_log_files: m.get_one::("nologfiles").cloned().unwrap_or(defaults.no_log_files), + rpclisten: m.get_one::("rpclisten").cloned(), + rpclisten_borsh: m.get_one::("rpclisten-borsh").cloned(), + rpclisten_json: m.get_one::("rpclisten-json").cloned(), + unsafe_rpc: m.get_one::("unsaferpc").cloned().unwrap_or(defaults.unsafe_rpc), + wrpc_verbose: false, + log_level: m.get_one::("log_level").cloned().unwrap(), + async_threads: m.get_one::("async_threads").cloned().unwrap_or(defaults.async_threads), + connect_peers: m.get_many::("connect-peers").unwrap_or_default().copied().collect(), + add_peers: m.get_many::("add-peers").unwrap_or_default().copied().collect(), + listen: m.get_one::("listen").cloned(), + outbound_target: m.get_one::("outpeers").cloned().unwrap_or(defaults.outbound_target), + inbound_limit: m.get_one::("maxinpeers").cloned().unwrap_or(defaults.inbound_limit), + rpc_max_clients: m.get_one::("rpcmaxclients").cloned().unwrap_or(defaults.rpc_max_clients), + reset_db: m.get_one::("reset-db").cloned().unwrap_or(defaults.reset_db), + enable_unsynced_mining: m.get_one::("enable-unsynced-mining").cloned().unwrap_or(defaults.enable_unsynced_mining), + enable_mainnet_mining: m.get_one::("enable-mainnet-mining").cloned().unwrap_or(defaults.enable_mainnet_mining), + utxoindex: m.get_one::("utxoindex").cloned().unwrap_or(defaults.utxoindex), + testnet: m.get_one::("testnet").cloned().unwrap_or(defaults.testnet), + testnet_suffix: m.get_one::("netsuffix").cloned().unwrap_or(defaults.testnet_suffix), + devnet: m.get_one::("devnet").cloned().unwrap_or(defaults.devnet), + simnet: m.get_one::("simnet").cloned().unwrap_or(defaults.simnet), + archival: m.get_one::("archival").cloned().unwrap_or(defaults.archival), + sanity: m.get_one::("sanity").cloned().unwrap_or(defaults.sanity), + yes: m.get_one::("yes").cloned().unwrap_or(defaults.yes), + user_agent_comments: m.get_many::("user_agent_comments").unwrap_or_default().cloned().collect(), + externalip: m.get_one::("externalip").cloned(), + perf_metrics: m.get_one::("perf-metrics").cloned().unwrap_or(defaults.perf_metrics), + perf_metrics_interval_sec: m + .get_one::("perf-metrics-interval-sec") + .cloned() + .unwrap_or(defaults.perf_metrics_interval_sec), + // Note: currently used programmatically by benchmarks and not exposed to CLI users + block_template_cache_lifetime: defaults.block_template_cache_lifetime, + disable_upnp: m.get_one::("disable-upnp").cloned().unwrap_or(defaults.disable_upnp), + disable_dns_seeding: m.get_one::("nodnsseed").cloned().unwrap_or(defaults.disable_dns_seeding), + + #[cfg(feature = "devnet-prealloc")] + num_prealloc_utxos: m.get_one::("num-prealloc-utxos").cloned(), + #[cfg(feature = "devnet-prealloc")] + prealloc_address: m.get_one::("prealloc-address").cloned(), + #[cfg(feature = "devnet-prealloc")] + prealloc_amount: m.get_one::("prealloc-amount").cloned().unwrap_or(defaults.prealloc_amount), + }; + Ok(args) } } diff --git a/kashd/src/daemon.rs b/kashd/src/daemon.rs index bc05e55de..8b21d5144 100644 --- a/kashd/src/daemon.rs +++ b/kashd/src/daemon.rs @@ -31,7 +31,7 @@ use kash_p2p_flows::{flow_context::FlowContext, service::P2pService}; use kash_perf_monitor::builder::Builder as PerfMonitorBuilder; use kash_utxoindex::{api::UtxoIndexProxy, UtxoIndex}; -use kash_wrpc_server::service::{Options as WrpcServerOptions, ServerCounters as WrpcServerCounters, WrpcEncoding, WrpcService}; +use kash_wrpc_server::service::{Options as WrpcServerOptions, WebSocketCounters as WrpcServerCounters, WrpcEncoding, WrpcService}; /// Desired soft FD limit that needs to be configured /// for the kashd process. diff --git a/kos/Cargo.toml b/kos/Cargo.toml index cbf19c724..29d46bc97 100644 --- a/kos/Cargo.toml +++ b/kos/Cargo.toml @@ -1,4 +1,3 @@ - [package] name = "kash-os" description = "Kash Node & Wallet Manager" @@ -35,6 +34,7 @@ kash-cli.workspace = true kash-consensus-core.workspace = true kash-core.workspace = true kash-daemon.workspace = true +kash-metrics-core.workspace = true kash-rpc-core.workspace = true kash-wallet-core.workspace = true nw-sys.workspace = true diff --git a/kos/src/imports.rs b/kos/src/imports.rs index 3b72e7f19..b5f24df2d 100644 --- a/kos/src/imports.rs +++ b/kos/src/imports.rs @@ -15,7 +15,7 @@ pub use kash_daemon::{ CpuMiner, CpuMinerConfig, CpuMinerCtl, DaemonEvent, DaemonKind, DaemonStatus, Daemons, Kashd, KashdConfig, KashdCtl, Result as DaemonResult, }; -pub use kash_wallet_core::{DefaultSettings, SettingsStore, SettingsStoreT}; +pub use kash_wallet_core::settings::{DefaultSettings, SettingsStore, SettingsStoreT}; pub use nw_sys::prelude::*; pub use regex::Regex; pub use serde::{Deserialize, Serialize}; diff --git a/kos/src/metrics/ipc.rs b/kos/src/metrics/ipc.rs index 00baf9f1e..d76e98008 100644 --- a/kos/src/metrics/ipc.rs +++ b/kos/src/metrics/ipc.rs @@ -1,5 +1,5 @@ use crate::imports::*; -use kash_cli_lib::metrics::MetricsSnapshot; +use kash_metrics_core::MetricsSnapshot; #[derive(Debug, Clone, PartialEq, Eq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub enum MetricsOps { diff --git a/kos/src/metrics/metrics.rs b/kos/src/metrics/metrics.rs index 7b746157f..6edda9870 100644 --- a/kos/src/metrics/metrics.rs +++ b/kos/src/metrics/metrics.rs @@ -1,6 +1,6 @@ use super::toolbar::*; use crate::imports::*; -use kash_cli_lib::metrics::{Metric, MetricsSnapshot}; +use kash_metrics_core::{Metric, MetricsSnapshot}; use std::collections::HashMap; use workflow_core::time::{HOURS, MINUTES}; use workflow_d3::container::*; @@ -26,7 +26,7 @@ impl Metrics { pub async fn try_new() -> Result> { workflow_d3::load().await?; - let core_ipc_target = get_ipc_target(Modules::Core).await?.expect("Unable to aquire background window"); + let core_ipc_target = get_ipc_target(Modules::Core).await?.expect("Unable to acquire background window"); let core = Arc::new(CoreIpc::new(core_ipc_target)); let settings = Arc::new(SettingsStore::::try_new("metrics")?); @@ -149,7 +149,7 @@ impl Metrics { let si = true; for metric in Metric::list() { let value = data.get(&metric); - self.graph(&metric).ingest(data.unixtime, value, &data.format(&metric, si)).await?; + self.graph(&metric).ingest(data.unixtime_millis, value, &data.format(&metric, si, false)).await?; } yield_executor().await; @@ -165,7 +165,7 @@ impl Metrics { self.init_graphs().await?; // this call reflects from core to terminal - // initiating metrica data relay + // initiating metrics data relay self.core.metrics_ready().await?; Ok(()) diff --git a/kos/src/metrics/toolbar.rs b/kos/src/metrics/toolbar.rs index 91844f3bf..596137fb6 100644 --- a/kos/src/metrics/toolbar.rs +++ b/kos/src/metrics/toolbar.rs @@ -4,7 +4,7 @@ use crate::imports::*; use crate::result::Result; use std::{collections::HashMap, sync::MutexGuard}; -use kash_cli_lib::metrics::Metric; +use kash_metrics_core::Metric; use web_sys::{Document, Element, MouseEvent}; use workflow_d3::graph::GraphDuration; #[allow(unused_imports)] diff --git a/kos/src/modules/metrics.rs b/kos/src/modules/metrics.rs index 97a26f624..2a9da4bb1 100644 --- a/kos/src/modules/metrics.rs +++ b/kos/src/modules/metrics.rs @@ -1,5 +1,6 @@ use crate::imports::*; -use kash_cli_lib::metrics::{metrics::MetricsSinkFn, Metrics as Inner}; +use kash_cli_lib::modules::metrics::Metrics as Inner; +use kash_metrics_core::MetricsSinkFn; pub struct Metrics { inner: Arc, diff --git a/kos/src/terminal/terminal.rs b/kos/src/terminal/terminal.rs index fe4c2fbbe..4e684abc5 100644 --- a/kos/src/terminal/terminal.rs +++ b/kos/src/terminal/terminal.rs @@ -1,5 +1,5 @@ use crate::imports::*; -use kash_cli_lib::metrics::MetricsSnapshot; +use kash_metrics_core::MetricsSnapshot; static mut TERMINAL: Option> = None; static mut SHUTDOWN_ATTEMPTS: usize = 0; @@ -20,7 +20,7 @@ pub struct Terminal { impl Terminal { pub async fn try_new() -> Result> { log_info!("-> core ipc binding"); - let core_ipc_target = get_ipc_target(Modules::Core).await?.expect("Unable to aquire background window"); + let core_ipc_target = get_ipc_target(Modules::Core).await?.expect("Unable to acquire background window"); let core = Arc::new(CoreIpc::new(core_ipc_target)); log_info!("-> creating daemon interface"); let daemons = Arc::new(Daemons::new().with_kashd(core.clone()).with_cpu_miner(core.clone())); @@ -135,21 +135,21 @@ impl Terminal { MetricsSinkCtl::Activate => { let ipc = get_ipc_target(Modules::Metrics) .await - .expect("Error actuiring ipc for the metrics window") + .expect("Error acquiring ipc for the metrics window") .expect("Unable to locate ipc for the metrics window"); this.metrics.lock().unwrap().replace(Arc::new(ipc)); metrics.register_sink(Arc::new(Box::new(move |data: MetricsSnapshot| { let this = this.clone(); - Box::pin(async move { + Some(Box::pin(async move { let ipc = this.metrics.lock().unwrap().as_ref().unwrap().clone(); ipc.notify(MetricsOps::MetricsSnapshot, data).await.unwrap_or_else(|err| { log_error!("error posting metrics data to metrics window: {:?}", err); }); Ok(()) - }) + })) }))) } MetricsSinkCtl::Deactivate => { diff --git a/metrics/core/Cargo.toml b/metrics/core/Cargo.toml new file mode 100644 index 000000000..5115af975 --- /dev/null +++ b/metrics/core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "kash-metrics-core" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +include.workspace = true + +[dependencies] +async-trait.workspace = true +borsh.workspace = true +futures.workspace = true +kash-core.workspace = true +kash-rpc-core.workspace = true +separator.workspace = true +serde.workspace = true +thiserror.workspace = true +workflow-core.workspace = true +workflow-log.workspace = true diff --git a/metrics/core/src/data.rs b/metrics/core/src/data.rs new file mode 100644 index 000000000..50dc2d275 --- /dev/null +++ b/metrics/core/src/data.rs @@ -0,0 +1,875 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use separator::{separated_float, separated_int, separated_uint_with_output, Separatable}; +use serde::{Deserialize, Serialize}; +use workflow_core::enums::Describe; + +#[derive(Describe, Debug, Clone, Copy, Eq, PartialEq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum MetricGroup { + System, + Storage, + Bandwidth, + Connections, + Network, +} + +impl std::fmt::Display for MetricGroup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MetricGroup::System => write!(f, "system"), + MetricGroup::Storage => write!(f, "storage"), + MetricGroup::Bandwidth => write!(f, "bandwidth"), + MetricGroup::Connections => write!(f, "connections"), + MetricGroup::Network => write!(f, "network"), + } + } +} + +impl MetricGroup { + pub fn title(&self) -> &str { + match self { + MetricGroup::System => "System", + MetricGroup::Storage => "Storage", + MetricGroup::Bandwidth => "Bandwidth", + MetricGroup::Connections => "Connections", + MetricGroup::Network => "Network", + } + } +} + +impl MetricGroup { + pub fn iter() -> impl Iterator { + [MetricGroup::System, MetricGroup::Storage, MetricGroup::Connections, MetricGroup::Network].into_iter() + } + + pub fn metrics(&self) -> impl Iterator { + match self { + MetricGroup::System => [ + Metric::NodeCpuUsage, + Metric::NodeResidentSetSizeBytes, + Metric::NodeVirtualMemorySizeBytes, + Metric::NodeFileHandlesCount, + ] + .as_slice() + .iter(), + MetricGroup::Storage => [ + Metric::NodeDiskIoReadBytes, + Metric::NodeDiskIoReadPerSec, + Metric::NodeDiskIoWriteBytes, + Metric::NodeDiskIoWritePerSec, + ] + .as_slice() + .iter(), + MetricGroup::Bandwidth => [ + Metric::NodeTotalBytesTx, + Metric::NodeTotalBytesTxPerSecond, + Metric::NodeTotalBytesRx, + Metric::NodeTotalBytesRxPerSecond, + Metric::NodeBorshBytesTx, + Metric::NodeBorshBytesTxPerSecond, + Metric::NodeBorshBytesRx, + Metric::NodeBorshBytesRxPerSecond, + Metric::NodeP2pBytesTx, + Metric::NodeP2pBytesTxPerSecond, + Metric::NodeP2pBytesRx, + Metric::NodeP2pBytesRxPerSecond, + Metric::NodeGrpcUserBytesTx, + Metric::NodeGrpcUserBytesTxPerSecond, + Metric::NodeGrpcUserBytesRx, + Metric::NodeGrpcUserBytesRxPerSecond, + Metric::NodeJsonBytesTx, + Metric::NodeJsonBytesTxPerSecond, + Metric::NodeJsonBytesRx, + Metric::NodeJsonBytesRxPerSecond, + ] + .as_slice() + .iter(), + MetricGroup::Connections => [ + Metric::NodeActivePeers, + Metric::NodeBorshLiveConnections, + Metric::NodeBorshConnectionAttempts, + Metric::NodeBorshHandshakeFailures, + Metric::NodeJsonLiveConnections, + Metric::NodeJsonConnectionAttempts, + Metric::NodeJsonHandshakeFailures, + ] + .as_slice() + .iter(), + MetricGroup::Network => [ + Metric::NodeBlocksSubmittedCount, + Metric::NodeHeadersProcessedCount, + Metric::NodeDependenciesProcessedCount, + Metric::NodeBodiesProcessedCount, + Metric::NodeTransactionsProcessedCount, + Metric::NodeChainBlocksProcessedCount, + Metric::NodeMassProcessedCount, + Metric::NodeDatabaseBlocksCount, + Metric::NodeDatabaseHeadersCount, + Metric::NetworkMempoolSize, + Metric::NetworkTransactionsPerSecond, + Metric::NetworkTipHashesCount, + Metric::NetworkDifficulty, + Metric::NetworkPastMedianTime, + Metric::NetworkVirtualParentHashesCount, + Metric::NetworkVirtualDaaScore, + ] + .as_slice() + .iter(), + } + } +} + +impl From for MetricGroup { + fn from(value: Metric) -> Self { + match value { + Metric::NodeCpuUsage | Metric::NodeResidentSetSizeBytes | Metric::NodeVirtualMemorySizeBytes => MetricGroup::System, + // -- + Metric::NodeFileHandlesCount + | Metric::NodeDiskIoReadBytes + | Metric::NodeDiskIoWriteBytes + | Metric::NodeDiskIoReadPerSec + | Metric::NodeDiskIoWritePerSec => MetricGroup::Storage, + // -- + Metric::NodeBorshLiveConnections + | Metric::NodeBorshConnectionAttempts + | Metric::NodeBorshHandshakeFailures + | Metric::NodeJsonLiveConnections + | Metric::NodeJsonConnectionAttempts + | Metric::NodeJsonHandshakeFailures + | Metric::NodeActivePeers => MetricGroup::Connections, + // -- + Metric::NodeBorshBytesRx + | Metric::NodeBorshBytesTx + | Metric::NodeJsonBytesTx + | Metric::NodeJsonBytesRx + | Metric::NodeP2pBytesTx + | Metric::NodeP2pBytesRx + | Metric::NodeGrpcUserBytesTx + | Metric::NodeGrpcUserBytesRx + | Metric::NodeTotalBytesRx + | Metric::NodeTotalBytesTx + | Metric::NodeBorshBytesRxPerSecond + | Metric::NodeBorshBytesTxPerSecond + | Metric::NodeJsonBytesTxPerSecond + | Metric::NodeJsonBytesRxPerSecond + | Metric::NodeP2pBytesTxPerSecond + | Metric::NodeP2pBytesRxPerSecond + | Metric::NodeGrpcUserBytesTxPerSecond + | Metric::NodeGrpcUserBytesRxPerSecond + | Metric::NodeTotalBytesRxPerSecond + | Metric::NodeTotalBytesTxPerSecond => MetricGroup::Bandwidth, + // -- + Metric::NodeBlocksSubmittedCount + | Metric::NodeHeadersProcessedCount + | Metric::NodeDependenciesProcessedCount + | Metric::NodeBodiesProcessedCount + | Metric::NodeTransactionsProcessedCount + | Metric::NodeChainBlocksProcessedCount + | Metric::NodeMassProcessedCount + // -- + | Metric::NodeDatabaseBlocksCount + | Metric::NodeDatabaseHeadersCount + // -- + | Metric::NetworkMempoolSize + | Metric::NetworkTransactionsPerSecond + | Metric::NetworkTipHashesCount + | Metric::NetworkDifficulty + | Metric::NetworkPastMedianTime + | Metric::NetworkVirtualParentHashesCount + | Metric::NetworkVirtualDaaScore => MetricGroup::Network, + } + } +} + +#[derive(Describe, Debug, Clone, Copy, Eq, PartialEq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Metric { + // NodeCpuCores is used to normalize NodeCpuUsage metric + // NodeCpuCores + NodeCpuUsage, + NodeResidentSetSizeBytes, + NodeVirtualMemorySizeBytes, + // --- + NodeFileHandlesCount, + NodeDiskIoReadBytes, + NodeDiskIoWriteBytes, + NodeDiskIoReadPerSec, + NodeDiskIoWritePerSec, + // --- + NodeActivePeers, + NodeBorshLiveConnections, + NodeBorshConnectionAttempts, + NodeBorshHandshakeFailures, + NodeJsonLiveConnections, + NodeJsonConnectionAttempts, + NodeJsonHandshakeFailures, + // --- + NodeTotalBytesTx, + NodeTotalBytesRx, + NodeTotalBytesTxPerSecond, + NodeTotalBytesRxPerSecond, + + NodeP2pBytesTx, + NodeP2pBytesRx, + NodeP2pBytesTxPerSecond, + NodeP2pBytesRxPerSecond, + + NodeBorshBytesTx, + NodeBorshBytesRx, + NodeBorshBytesTxPerSecond, + NodeBorshBytesRxPerSecond, + + NodeGrpcUserBytesTx, + NodeGrpcUserBytesRx, + NodeGrpcUserBytesTxPerSecond, + NodeGrpcUserBytesRxPerSecond, + + NodeJsonBytesTx, + NodeJsonBytesRx, + NodeJsonBytesTxPerSecond, + NodeJsonBytesRxPerSecond, + + // --- + NodeBlocksSubmittedCount, + NodeHeadersProcessedCount, + NodeDependenciesProcessedCount, + NodeBodiesProcessedCount, + NodeTransactionsProcessedCount, + NodeChainBlocksProcessedCount, + NodeMassProcessedCount, + // -- + NodeDatabaseBlocksCount, + NodeDatabaseHeadersCount, + // -- + NetworkMempoolSize, + NetworkTransactionsPerSecond, + NetworkTipHashesCount, + NetworkDifficulty, + NetworkPastMedianTime, + NetworkVirtualParentHashesCount, + NetworkVirtualDaaScore, +} + +impl Metric { + // TODO - this will be refactored at a later date + // as this requires changes and testing in /kos + pub fn group(&self) -> &'static str { + match self { + Metric::NodeCpuUsage + | Metric::NodeResidentSetSizeBytes + | Metric::NodeVirtualMemorySizeBytes + | Metric::NodeFileHandlesCount + | Metric::NodeDiskIoReadBytes + | Metric::NodeDiskIoWriteBytes + | Metric::NodeDiskIoReadPerSec + | Metric::NodeDiskIoWritePerSec + | Metric::NodeBorshLiveConnections + | Metric::NodeBorshConnectionAttempts + | Metric::NodeBorshHandshakeFailures + | Metric::NodeJsonLiveConnections + | Metric::NodeJsonConnectionAttempts + | Metric::NodeJsonHandshakeFailures + | Metric::NodeBorshBytesTx + | Metric::NodeBorshBytesRx + | Metric::NodeJsonBytesTx + | Metric::NodeJsonBytesRx + | Metric::NodeP2pBytesTx + | Metric::NodeP2pBytesRx + | Metric::NodeGrpcUserBytesTx + | Metric::NodeGrpcUserBytesRx + | Metric::NodeTotalBytesTx + | Metric::NodeTotalBytesRx + | Metric::NodeBorshBytesTxPerSecond + | Metric::NodeBorshBytesRxPerSecond + | Metric::NodeJsonBytesTxPerSecond + | Metric::NodeJsonBytesRxPerSecond + | Metric::NodeP2pBytesTxPerSecond + | Metric::NodeP2pBytesRxPerSecond + | Metric::NodeGrpcUserBytesTxPerSecond + | Metric::NodeGrpcUserBytesRxPerSecond + | Metric::NodeTotalBytesTxPerSecond + | Metric::NodeTotalBytesRxPerSecond + | Metric::NodeActivePeers => "system", + // -- + Metric::NodeBlocksSubmittedCount + | Metric::NodeHeadersProcessedCount + | Metric::NodeDependenciesProcessedCount + | Metric::NodeBodiesProcessedCount + | Metric::NodeTransactionsProcessedCount + | Metric::NodeChainBlocksProcessedCount + | Metric::NodeMassProcessedCount + | Metric::NodeDatabaseBlocksCount + | Metric::NodeDatabaseHeadersCount + | Metric::NetworkMempoolSize + | Metric::NetworkTransactionsPerSecond + | Metric::NetworkTipHashesCount + | Metric::NetworkDifficulty + | Metric::NetworkPastMedianTime + | Metric::NetworkVirtualParentHashesCount + | Metric::NetworkVirtualDaaScore => "kash", + } + } + + pub fn is_key_performance_metric(&self) -> bool { + matches!( + self, + Metric::NodeCpuUsage + | Metric::NodeResidentSetSizeBytes + | Metric::NodeFileHandlesCount + | Metric::NodeDiskIoReadBytes + | Metric::NodeDiskIoWriteBytes + | Metric::NodeDiskIoReadPerSec + | Metric::NodeDiskIoWritePerSec + | Metric::NodeBorshBytesTx + | Metric::NodeBorshBytesRx + | Metric::NodeP2pBytesTx + | Metric::NodeP2pBytesRx + | Metric::NodeGrpcUserBytesTx + | Metric::NodeGrpcUserBytesRx + | Metric::NodeTotalBytesTx + | Metric::NodeTotalBytesRx + | Metric::NodeBorshBytesTxPerSecond + | Metric::NodeBorshBytesRxPerSecond + | Metric::NodeP2pBytesTxPerSecond + | Metric::NodeP2pBytesRxPerSecond + | Metric::NodeGrpcUserBytesTxPerSecond + | Metric::NodeGrpcUserBytesRxPerSecond + | Metric::NodeTotalBytesTxPerSecond + | Metric::NodeTotalBytesRxPerSecond + | Metric::NodeActivePeers + | Metric::NetworkMempoolSize + | Metric::NetworkTipHashesCount + | Metric::NetworkTransactionsPerSecond + | Metric::NodeTransactionsProcessedCount + | Metric::NodeDatabaseBlocksCount + | Metric::NodeDatabaseHeadersCount + ) + } + + pub fn format(&self, f: f64, si: bool, short: bool) -> String { + match self { + Metric::NodeCpuUsage => { + if f.is_nan() { + "---".to_string() + } else { + format!("{:1.2}%", f) + } + } + Metric::NodeResidentSetSizeBytes => as_mb(f, si, short), + Metric::NodeVirtualMemorySizeBytes => as_mb(f, si, short), + Metric::NodeFileHandlesCount => f.separated_string(), + // -- + Metric::NodeDiskIoReadBytes => as_mb(f, si, short), + Metric::NodeDiskIoWriteBytes => as_mb(f, si, short), + Metric::NodeDiskIoReadPerSec => format!("{}/s", as_data_size(f, si)), + Metric::NodeDiskIoWritePerSec => format!("{}/s", as_data_size(f, si)), + // -- + Metric::NodeBorshLiveConnections => f.trunc().separated_string(), + Metric::NodeBorshConnectionAttempts => f.trunc().separated_string(), + Metric::NodeBorshHandshakeFailures => f.trunc().separated_string(), + Metric::NodeJsonLiveConnections => f.trunc().separated_string(), + Metric::NodeJsonConnectionAttempts => f.trunc().separated_string(), + Metric::NodeJsonHandshakeFailures => f.trunc().separated_string(), + Metric::NodeActivePeers => f.trunc().separated_string(), + // -- + Metric::NodeBorshBytesTx => as_data_size(f, si), + Metric::NodeBorshBytesRx => as_data_size(f, si), + Metric::NodeJsonBytesTx => as_data_size(f, si), + Metric::NodeJsonBytesRx => as_data_size(f, si), + Metric::NodeP2pBytesTx => as_data_size(f, si), + Metric::NodeP2pBytesRx => as_data_size(f, si), + Metric::NodeGrpcUserBytesTx => as_data_size(f, si), + Metric::NodeGrpcUserBytesRx => as_data_size(f, si), + Metric::NodeTotalBytesTx => as_data_size(f, si), + Metric::NodeTotalBytesRx => as_data_size(f, si), + // -- + Metric::NodeBorshBytesTxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeBorshBytesRxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeJsonBytesTxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeJsonBytesRxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeP2pBytesTxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeP2pBytesRxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeGrpcUserBytesTxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeGrpcUserBytesRxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeTotalBytesTxPerSecond => format!("{}/s", as_kb(f, si, short)), + Metric::NodeTotalBytesRxPerSecond => format!("{}/s", as_kb(f, si, short)), + // -- + Metric::NodeBlocksSubmittedCount => format_as_float(f, short), + Metric::NodeHeadersProcessedCount => format_as_float(f, short), + Metric::NodeDependenciesProcessedCount => format_as_float(f, short), + Metric::NodeBodiesProcessedCount => format_as_float(f, short), + Metric::NodeTransactionsProcessedCount => format_as_float(f, short), + Metric::NodeChainBlocksProcessedCount => format_as_float(f, short), + Metric::NodeMassProcessedCount => format_as_float(f, short), + // -- + Metric::NodeDatabaseHeadersCount => format_as_float(f, short), + Metric::NodeDatabaseBlocksCount => format_as_float(f, short), + // -- + Metric::NetworkMempoolSize => format_as_float(f.trunc(), short), + Metric::NetworkTransactionsPerSecond => format_as_float(f.trunc(), short), + Metric::NetworkTipHashesCount => format_as_float(f, short), + Metric::NetworkDifficulty => format_as_float(f, short), + Metric::NetworkPastMedianTime => format_as_float(f, false), + Metric::NetworkVirtualParentHashesCount => format_as_float(f, short), + Metric::NetworkVirtualDaaScore => format_as_float(f, false), + } + } + + pub fn title(&self) -> (&str, &str) { + match self { + Metric::NodeCpuUsage => ("CPU", "CPU"), + Metric::NodeResidentSetSizeBytes => ("Resident Memory", "Memory"), + Metric::NodeVirtualMemorySizeBytes => ("Virtual Memory", "Virtual"), + // -- + Metric::NodeFileHandlesCount => ("File Handles", "Handles"), + Metric::NodeDiskIoReadBytes => ("Storage Read", "Stor Read"), + Metric::NodeDiskIoWriteBytes => ("Storage Write", "Stor Write"), + Metric::NodeDiskIoReadPerSec => ("Storage Read/s", "Stor Read"), + Metric::NodeDiskIoWritePerSec => ("Storage Write/s", "Stor Write"), + // -- + Metric::NodeActivePeers => ("Active p2p Peers", "Peers"), + Metric::NodeBorshLiveConnections => ("Borsh Active Connections", "Borsh Conn"), + Metric::NodeBorshConnectionAttempts => ("Borsh Connection Attempts", "Borsh Conn Att"), + Metric::NodeBorshHandshakeFailures => ("Borsh Handshake Failures", "Borsh Failures"), + Metric::NodeJsonLiveConnections => ("Json Active Connections", "Json Conn"), + Metric::NodeJsonConnectionAttempts => ("Json Connection Attempts", "Json Conn Att"), + Metric::NodeJsonHandshakeFailures => ("Json Handshake Failures", "Json Failures"), + // -- + Metric::NodeBorshBytesTx => ("wRPC Borsh Tx", "Borsh Tx"), + Metric::NodeBorshBytesRx => ("wRPC Borsh Rx", "Borsh Rx"), + Metric::NodeJsonBytesTx => ("wRPC JSON Tx", "Json Tx"), + Metric::NodeJsonBytesRx => ("wRPC JSON Rx", "Json Rx"), + Metric::NodeP2pBytesTx => ("p2p Tx", "p2p Tx"), + Metric::NodeP2pBytesRx => ("p2p Rx", "p2p Rx"), + Metric::NodeGrpcUserBytesTx => ("gRPC Tx", "gRPC Tx"), + Metric::NodeGrpcUserBytesRx => ("gRPC Rx", "gRPC Rx"), + Metric::NodeTotalBytesTx => ("Total Tx", "Total Tx"), + Metric::NodeTotalBytesRx => ("Total Rx", "Total Rx"), + // -- + Metric::NodeBorshBytesTxPerSecond => ("wRPC Borsh Tx/s", "Borsh Tx/s"), + Metric::NodeBorshBytesRxPerSecond => ("wRPC Borsh Rx/s", "Borsh Rx/s"), + Metric::NodeJsonBytesTxPerSecond => ("wRPC JSON Tx/s", "JSON Tx/s"), + Metric::NodeJsonBytesRxPerSecond => ("wRPC JSON Rx/s", "JSON Rx/s"), + Metric::NodeP2pBytesTxPerSecond => ("p2p Tx/s", "p2p Tx/s"), + Metric::NodeP2pBytesRxPerSecond => ("p2p Rx/s", "p2p Rx/s"), + Metric::NodeGrpcUserBytesTxPerSecond => ("gRPC Tx/s", "gRPC Tx/s"), + Metric::NodeGrpcUserBytesRxPerSecond => ("gRPC Rx/s", "gRPC Rx/s"), + Metric::NodeTotalBytesTxPerSecond => ("Total Tx/s", "Total Tx/s"), + Metric::NodeTotalBytesRxPerSecond => ("Total Rx/s", "Total Rx/s"), + // -- + Metric::NodeBlocksSubmittedCount => ("Submitted Blocks", "Blocks"), + Metric::NodeHeadersProcessedCount => ("Processed Headers", "Headers"), + Metric::NodeDependenciesProcessedCount => ("Processed Dependencies", "Dependencies"), + Metric::NodeBodiesProcessedCount => ("Processed Bodies", "Bodies"), + Metric::NodeTransactionsProcessedCount => ("Processed Transactions", "Transactions"), + Metric::NodeChainBlocksProcessedCount => ("Chain Blocks", "Chain Blocks"), + Metric::NodeMassProcessedCount => ("Processed Mass Counts", "Mass Processed"), + // -- + Metric::NodeDatabaseBlocksCount => ("Database Blocks", "DB Blocks"), + Metric::NodeDatabaseHeadersCount => ("Database Headers", "DB Headers"), + // -- + Metric::NetworkMempoolSize => ("Mempool Size", "Mempool"), + Metric::NetworkTransactionsPerSecond => ("TPS", "TPS"), + Metric::NetworkTipHashesCount => ("Tip Hashes", "Tip Hashes"), + Metric::NetworkDifficulty => ("Network Difficulty", "Difficulty"), + Metric::NetworkPastMedianTime => ("Past Median Time", "MT"), + Metric::NetworkVirtualParentHashesCount => ("Virtual Parent Hashes", "Virt Parents"), + Metric::NetworkVirtualDaaScore => ("Virtual DAA Score", "DAA"), + } + } +} + +#[derive(Default, Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct MetricsData { + pub unixtime_millis: f64, + + // --- + pub node_resident_set_size_bytes: u64, + pub node_virtual_memory_size_bytes: u64, + pub node_cpu_cores: u32, + pub node_cpu_usage: f32, + pub node_file_handles: u32, + // --- + pub node_disk_io_read_bytes: u64, + pub node_disk_io_write_bytes: u64, + pub node_disk_io_read_per_sec: f32, + pub node_disk_io_write_per_sec: f32, + // --- + pub node_borsh_live_connections: u32, + pub node_borsh_connection_attempts: u64, + pub node_borsh_handshake_failures: u64, + pub node_json_live_connections: u32, + pub node_json_connection_attempts: u64, + pub node_json_handshake_failures: u64, + pub node_active_peers: u32, + // --- + pub node_borsh_bytes_tx: u64, + pub node_borsh_bytes_rx: u64, + pub node_json_bytes_tx: u64, + pub node_json_bytes_rx: u64, + pub node_p2p_bytes_tx: u64, + pub node_p2p_bytes_rx: u64, + pub node_grpc_user_bytes_tx: u64, + pub node_grpc_user_bytes_rx: u64, + pub node_total_bytes_tx: u64, + pub node_total_bytes_rx: u64, + + pub node_borsh_bytes_tx_per_second: u64, + pub node_borsh_bytes_rx_per_second: u64, + pub node_json_bytes_tx_per_second: u64, + pub node_json_bytes_rx_per_second: u64, + pub node_p2p_bytes_tx_per_second: u64, + pub node_p2p_bytes_rx_per_second: u64, + pub node_grpc_user_bytes_tx_per_second: u64, + pub node_grpc_user_bytes_rx_per_second: u64, + pub node_total_bytes_tx_per_second: u64, + pub node_total_bytes_rx_per_second: u64, + // --- + pub node_blocks_submitted_count: u64, + pub node_headers_processed_count: u64, + pub node_dependencies_processed_count: u64, + pub node_bodies_processed_count: u64, + pub node_transactions_processed_count: u64, + pub node_chain_blocks_processed_count: u64, + pub node_mass_processed_count: u64, + // --- + pub node_database_blocks_count: u64, + pub node_database_headers_count: u64, + // -- + pub network_mempool_size: u64, + pub network_tip_hashes_count: u32, + pub network_difficulty: f64, + pub network_past_median_time: u64, + pub network_virtual_parent_hashes_count: u32, + pub network_virtual_daa_score: u64, +} + +impl MetricsData { + pub fn new(unixtime: f64) -> Self { + Self { unixtime_millis: unixtime, ..Default::default() } + } +} + +#[derive(Default, Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct MetricsSnapshot { + pub data: MetricsData, + + pub unixtime_millis: f64, + pub duration_millis: f64, + // --- + pub node_resident_set_size_bytes: f64, + pub node_virtual_memory_size_bytes: f64, + pub node_cpu_cores: f64, + pub node_cpu_usage: f64, + // --- + pub node_file_handles: f64, + pub node_disk_io_read_bytes: f64, + pub node_disk_io_write_bytes: f64, + pub node_disk_io_read_per_sec: f64, + pub node_disk_io_write_per_sec: f64, + // --- + pub node_borsh_active_connections: f64, + pub node_borsh_connection_attempts: f64, + pub node_borsh_handshake_failures: f64, + pub node_json_active_connections: f64, + pub node_json_connection_attempts: f64, + pub node_json_handshake_failures: f64, + pub node_active_peers: f64, + // --- + pub node_borsh_bytes_tx: f64, + pub node_borsh_bytes_rx: f64, + pub node_json_bytes_tx: f64, + pub node_json_bytes_rx: f64, + pub node_p2p_bytes_tx: f64, + pub node_p2p_bytes_rx: f64, + pub node_grpc_user_bytes_tx: f64, + pub node_grpc_user_bytes_rx: f64, + pub node_total_bytes_tx: f64, + pub node_total_bytes_rx: f64, + + pub node_borsh_bytes_tx_per_second: f64, + pub node_borsh_bytes_rx_per_second: f64, + pub node_json_bytes_tx_per_second: f64, + pub node_json_bytes_rx_per_second: f64, + pub node_p2p_bytes_tx_per_second: f64, + pub node_p2p_bytes_rx_per_second: f64, + pub node_grpc_user_bytes_tx_per_second: f64, + pub node_grpc_user_bytes_rx_per_second: f64, + pub node_total_bytes_tx_per_second: f64, + pub node_total_bytes_rx_per_second: f64, + + // --- + pub node_blocks_submitted_count: f64, + pub node_headers_processed_count: f64, + pub node_dependencies_processed_count: f64, + pub node_bodies_processed_count: f64, + pub node_transactions_processed_count: f64, + pub node_chain_blocks_processed_count: f64, + pub node_mass_processed_count: f64, + // --- + pub network_mempool_size: f64, + pub network_transactions_per_second: f64, + pub node_database_blocks_count: f64, + pub node_database_headers_count: f64, + pub network_tip_hashes_count: f64, + pub network_difficulty: f64, + pub network_past_median_time: f64, + pub network_virtual_parent_hashes_count: f64, + pub network_virtual_daa_score: f64, +} + +impl MetricsSnapshot { + pub fn get(&self, metric: &Metric) -> f64 { + match metric { + // CpuCores + Metric::NodeCpuUsage => self.node_cpu_usage, // / self.cpu_cores, + Metric::NodeResidentSetSizeBytes => self.node_resident_set_size_bytes, + Metric::NodeVirtualMemorySizeBytes => self.node_virtual_memory_size_bytes, + Metric::NodeFileHandlesCount => self.node_file_handles, + Metric::NodeDiskIoReadBytes => self.node_disk_io_read_bytes, + Metric::NodeDiskIoWriteBytes => self.node_disk_io_write_bytes, + Metric::NodeDiskIoReadPerSec => self.node_disk_io_read_per_sec, + Metric::NodeDiskIoWritePerSec => self.node_disk_io_write_per_sec, + // --- + Metric::NodeActivePeers => self.node_active_peers, + Metric::NodeBorshLiveConnections => self.node_borsh_active_connections, + Metric::NodeBorshConnectionAttempts => self.node_borsh_connection_attempts, + Metric::NodeBorshHandshakeFailures => self.node_borsh_handshake_failures, + Metric::NodeJsonLiveConnections => self.node_json_active_connections, + Metric::NodeJsonConnectionAttempts => self.node_json_connection_attempts, + Metric::NodeJsonHandshakeFailures => self.node_json_handshake_failures, + // --- + Metric::NodeBorshBytesTx => self.node_borsh_bytes_tx, + Metric::NodeBorshBytesRx => self.node_borsh_bytes_rx, + Metric::NodeJsonBytesTx => self.node_json_bytes_tx, + Metric::NodeJsonBytesRx => self.node_json_bytes_rx, + Metric::NodeP2pBytesTx => self.node_p2p_bytes_tx, + Metric::NodeP2pBytesRx => self.node_p2p_bytes_rx, + Metric::NodeGrpcUserBytesTx => self.node_grpc_user_bytes_tx, + Metric::NodeGrpcUserBytesRx => self.node_grpc_user_bytes_rx, + Metric::NodeTotalBytesTx => self.node_total_bytes_tx, + Metric::NodeTotalBytesRx => self.node_total_bytes_rx, + + Metric::NodeBorshBytesTxPerSecond => self.node_borsh_bytes_tx_per_second, + Metric::NodeBorshBytesRxPerSecond => self.node_borsh_bytes_rx_per_second, + Metric::NodeJsonBytesTxPerSecond => self.node_json_bytes_tx_per_second, + Metric::NodeJsonBytesRxPerSecond => self.node_json_bytes_rx_per_second, + Metric::NodeP2pBytesTxPerSecond => self.node_p2p_bytes_tx_per_second, + Metric::NodeP2pBytesRxPerSecond => self.node_p2p_bytes_rx_per_second, + Metric::NodeGrpcUserBytesTxPerSecond => self.node_grpc_user_bytes_tx_per_second, + Metric::NodeGrpcUserBytesRxPerSecond => self.node_grpc_user_bytes_rx_per_second, + Metric::NodeTotalBytesTxPerSecond => self.node_total_bytes_tx_per_second, + Metric::NodeTotalBytesRxPerSecond => self.node_total_bytes_rx_per_second, + // --- + Metric::NodeBlocksSubmittedCount => self.node_blocks_submitted_count, + Metric::NodeHeadersProcessedCount => self.node_headers_processed_count, + Metric::NodeDependenciesProcessedCount => self.node_dependencies_processed_count, + Metric::NodeBodiesProcessedCount => self.node_bodies_processed_count, + Metric::NodeTransactionsProcessedCount => self.node_transactions_processed_count, + Metric::NodeChainBlocksProcessedCount => self.node_chain_blocks_processed_count, + Metric::NodeMassProcessedCount => self.node_mass_processed_count, + // -- + Metric::NodeDatabaseBlocksCount => self.node_database_blocks_count, + Metric::NodeDatabaseHeadersCount => self.node_database_headers_count, + // -- + Metric::NetworkMempoolSize => self.network_mempool_size, + Metric::NetworkTransactionsPerSecond => self.network_transactions_per_second, + Metric::NetworkTipHashesCount => self.network_tip_hashes_count, + Metric::NetworkDifficulty => self.network_difficulty, + Metric::NetworkPastMedianTime => self.network_past_median_time, + Metric::NetworkVirtualParentHashesCount => self.network_virtual_parent_hashes_count, + Metric::NetworkVirtualDaaScore => self.network_virtual_daa_score, + } + } + + pub fn format(&self, metric: &Metric, si: bool, short: bool) -> String { + if short { + format!("{}: {}", metric.title().1, metric.format(self.get(metric), si, short)) + } else { + format!("{}: {}", metric.title().0, metric.format(self.get(metric), si, short)) + } + } +} + +#[inline(always)] +fn per_sec(a: u64, b: u64, duration_millis: f64) -> f64 { + b.checked_sub(a).unwrap_or_default() as f64 * 1000. / duration_millis +} + +impl From<(&MetricsData, &MetricsData)> for MetricsSnapshot { + fn from((a, b): (&MetricsData, &MetricsData)) -> Self { + let duration_millis = b.unixtime_millis - a.unixtime_millis; + + let network_transactions_per_second = + per_sec(a.node_transactions_processed_count, b.node_transactions_processed_count, duration_millis); + let node_borsh_bytes_tx_per_second = per_sec(a.node_borsh_bytes_tx, b.node_borsh_bytes_tx, duration_millis); + let node_borsh_bytes_rx_per_second = per_sec(a.node_borsh_bytes_rx, b.node_borsh_bytes_rx, duration_millis); + let node_json_bytes_tx_per_second = per_sec(a.node_json_bytes_tx, b.node_json_bytes_tx, duration_millis); + let node_json_bytes_rx_per_second = per_sec(a.node_json_bytes_rx, b.node_json_bytes_rx, duration_millis); + let node_p2p_bytes_tx_per_second = per_sec(a.node_p2p_bytes_tx, b.node_p2p_bytes_tx, duration_millis); + let node_p2p_bytes_rx_per_second = per_sec(a.node_p2p_bytes_rx, b.node_p2p_bytes_rx, duration_millis); + let node_grpc_user_bytes_tx_per_second = per_sec(a.node_grpc_user_bytes_tx, b.node_grpc_user_bytes_tx, duration_millis); + let node_grpc_user_bytes_rx_per_second = per_sec(a.node_grpc_user_bytes_rx, b.node_grpc_user_bytes_rx, duration_millis); + let node_total_bytes_tx_per_second = per_sec(a.node_total_bytes_tx, b.node_total_bytes_tx, duration_millis); + let node_total_bytes_rx_per_second = per_sec(a.node_total_bytes_rx, b.node_total_bytes_rx, duration_millis); + + Self { + unixtime_millis: b.unixtime_millis, + duration_millis, + // --- + node_cpu_usage: b.node_cpu_usage as f64 / b.node_cpu_cores as f64 * 100.0, + node_cpu_cores: b.node_cpu_cores as f64, + node_resident_set_size_bytes: b.node_resident_set_size_bytes as f64, + node_virtual_memory_size_bytes: b.node_virtual_memory_size_bytes as f64, + node_file_handles: b.node_file_handles as f64, + node_disk_io_read_bytes: b.node_disk_io_read_bytes as f64, + node_disk_io_write_bytes: b.node_disk_io_write_bytes as f64, + node_disk_io_read_per_sec: b.node_disk_io_read_per_sec as f64, + node_disk_io_write_per_sec: b.node_disk_io_write_per_sec as f64, + // --- + node_borsh_active_connections: b.node_borsh_live_connections as f64, + node_borsh_connection_attempts: b.node_borsh_connection_attempts as f64, + node_borsh_handshake_failures: b.node_borsh_handshake_failures as f64, + node_json_active_connections: b.node_json_live_connections as f64, + node_json_connection_attempts: b.node_json_connection_attempts as f64, + node_json_handshake_failures: b.node_json_handshake_failures as f64, + node_active_peers: b.node_active_peers as f64, + // --- + node_borsh_bytes_tx: b.node_borsh_bytes_tx as f64, + node_borsh_bytes_rx: b.node_borsh_bytes_rx as f64, + node_json_bytes_tx: b.node_json_bytes_tx as f64, + node_json_bytes_rx: b.node_json_bytes_rx as f64, + node_p2p_bytes_tx: b.node_p2p_bytes_tx as f64, + node_p2p_bytes_rx: b.node_p2p_bytes_rx as f64, + node_grpc_user_bytes_tx: b.node_grpc_user_bytes_tx as f64, + node_grpc_user_bytes_rx: b.node_grpc_user_bytes_rx as f64, + node_total_bytes_tx: b.node_total_bytes_tx as f64, + node_total_bytes_rx: b.node_total_bytes_rx as f64, + + node_borsh_bytes_tx_per_second, + node_borsh_bytes_rx_per_second, + node_json_bytes_tx_per_second, + node_json_bytes_rx_per_second, + node_p2p_bytes_tx_per_second, + node_p2p_bytes_rx_per_second, + node_grpc_user_bytes_tx_per_second, + node_grpc_user_bytes_rx_per_second, + node_total_bytes_tx_per_second, + node_total_bytes_rx_per_second, + // --- + node_blocks_submitted_count: b.node_blocks_submitted_count as f64, + node_headers_processed_count: b.node_headers_processed_count as f64, + node_dependencies_processed_count: b.node_dependencies_processed_count as f64, + node_bodies_processed_count: b.node_bodies_processed_count as f64, + node_transactions_processed_count: b.node_transactions_processed_count as f64, + node_chain_blocks_processed_count: b.node_chain_blocks_processed_count as f64, + node_mass_processed_count: b.node_mass_processed_count as f64, + // --- + node_database_blocks_count: b.node_database_blocks_count as f64, + node_database_headers_count: b.node_database_headers_count as f64, + // -- + network_mempool_size: b.network_mempool_size as f64, + network_transactions_per_second, + network_tip_hashes_count: b.network_tip_hashes_count as f64, + network_difficulty: b.network_difficulty, + network_past_median_time: b.network_past_median_time as f64, + network_virtual_parent_hashes_count: b.network_virtual_parent_hashes_count as f64, + network_virtual_daa_score: b.network_virtual_daa_score as f64, + + data: b.clone(), + } + } +} + +/// Display KB or KiB if `short` is false, otherwise if `short` is true +/// and the value is greater than 1MB or 1MiB, display units using [`as_data_size()`]. +pub fn as_kb(bytes: f64, si: bool, short: bool) -> String { + let unit = if si { 1000_f64 } else { 1024_f64 }; + if short && bytes > unit.powi(2) { + as_data_size(bytes, si) + } else { + let suffix = if si { " KB" } else { " KiB" }; + let kb = bytes / unit; //(( * 100.) as u64) as f64 / 100.; + format_with_precision(kb) + suffix + } +} + +/// Display MB or MiB if `short` is false, otherwise if `short` is true +/// and the value is greater than 1GB or 1GiB, display units using [`as_data_size()`]. +pub fn as_mb(bytes: f64, si: bool, short: bool) -> String { + let unit = if si { 1000_f64 } else { 1024_f64 }; + if short && bytes > unit.powi(3) { + as_data_size(bytes, si) + } else { + let suffix = if si { " MB" } else { " MiB" }; + let mb = bytes / unit.powi(2); //(( * 100.) as u64) as f64 / 100.; + format_with_precision(mb) + suffix + } +} + +/// Display GB or GiB if `short` is false, otherwise if `short` is true +/// and the value is greater than 1TB or 1TiB, display units using [`as_data_size()`]. +pub fn as_gb(bytes: f64, si: bool, short: bool) -> String { + let unit = if si { 1000_f64 } else { 1024_f64 }; + if short && bytes > unit.powi(4) { + as_data_size(bytes, si) + } else { + let suffix = if si { " GB" } else { " GiB" }; + let gb = bytes / unit.powi(3); //(( * 100.) as u64) as f64 / 100.; + format_with_precision(gb) + suffix + } +} + +/// Display units dynamically formatted based on the size of the value. +pub fn as_data_size(bytes: f64, si: bool) -> String { + let unit = if si { 1000_f64 } else { 1024_f64 }; + let mut size = bytes; + let mut unit_str = " B"; + + if size >= unit.powi(4) { + size /= unit.powi(4); + unit_str = " TB"; + } else if size >= unit.powi(3) { + size /= unit.powi(3); + unit_str = " GB"; + } else if size >= unit.powi(2) { + size /= unit.powi(2); + unit_str = " MB"; + } else if size >= unit { + size /= unit; + unit_str = " KB"; + } + + format_with_precision(size) + unit_str +} + +/// Format supplied value as a float with 2 decimal places. +fn format_as_float(f: f64, short: bool) -> String { + if short { + if f < 1000.0 { + format_with_precision(f) + } else if f < 1000000.0 { + format_with_precision(f / 1000.0) + " K" + } else if f < 1000000000.0 { + format_with_precision(f / 1000000.0) + " M" + } else if f < 1000000000000.0 { + format_with_precision(f / 1000000000.0) + " G" + } else if f < 1000000000000000.0 { + format_with_precision(f / 1000000000000.0) + " T" + } else if f < 1000000000000000000.0 { + format_with_precision(f / 1000000000000000.0) + " P" + } else { + format_with_precision(f / 1000000000000000000.0) + " E" + } + } else { + f.separated_string() + } +} + +/// Format supplied value as a float with 2 decimal places. +fn format_with_precision(f: f64) -> String { + if f.fract() < 0.01 { + separated_float!(format!("{}", f.trunc())) + } else { + separated_float!(format!("{:.2}", f)) + } +} diff --git a/metrics/core/src/error.rs b/metrics/core/src/error.rs new file mode 100644 index 000000000..2d3bfddc6 --- /dev/null +++ b/metrics/core/src/error.rs @@ -0,0 +1,29 @@ +use kash_rpc_core::RpcError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Custom(String), + + #[error(transparent)] + RpcError(#[from] RpcError), +} + +impl Error { + pub fn custom>(msg: T) -> Self { + Error::Custom(msg.into()) + } +} + +impl From for Error { + fn from(err: String) -> Self { + Self::Custom(err) + } +} + +impl From<&str> for Error { + fn from(err: &str) -> Self { + Self::Custom(err.to_string()) + } +} diff --git a/metrics/core/src/lib.rs b/metrics/core/src/lib.rs new file mode 100644 index 000000000..d64b6da5d --- /dev/null +++ b/metrics/core/src/lib.rs @@ -0,0 +1,185 @@ +pub mod data; +pub mod error; +pub mod result; + +pub use data::{Metric, MetricGroup, MetricsData, MetricsSnapshot}; + +use crate::result::Result; +use futures::{pin_mut, select, FutureExt, StreamExt}; +use kash_rpc_core::{api::rpc::RpcApi, GetMetricsResponse}; +use std::{ + future::Future, + pin::Pin, + sync::{Arc, Mutex}, + time::Duration, +}; +use workflow_core::channel::DuplexChannel; +use workflow_core::task::interval; +use workflow_core::task::spawn; +use workflow_core::time::unixtime_as_millis_f64; +use workflow_log::*; + +pub type MetricsSinkFn = + Arc Option>)>>> + 'static>>; + +pub struct Metrics { + task_ctl: DuplexChannel, + rpc: Arc>>>, + sink: Arc>>, + data: Arc>>, +} + +impl Default for Metrics { + fn default() -> Self { + Metrics { + task_ctl: DuplexChannel::oneshot(), + rpc: Arc::new(Mutex::new(None)), + sink: Arc::new(Mutex::new(None)), + data: Arc::new(Mutex::new(None)), + } + } +} + +impl Metrics { + pub fn set_rpc(&self, rpc: Option>) { + *self.rpc.lock().unwrap() = rpc; + } + + fn rpc(&self) -> Option> { + self.rpc.lock().unwrap().clone() + } + + pub fn register_sink(&self, target: MetricsSinkFn) { + self.sink.lock().unwrap().replace(target); + } + + pub fn unregister_sink(&self) { + self.sink.lock().unwrap().take(); + } + + pub fn sink(&self) -> Option { + self.sink.lock().unwrap().clone() + } + + pub async fn start_task(self: &Arc) -> Result<()> { + let this = self.clone(); + + let task_ctl_receiver = self.task_ctl.request.receiver.clone(); + let task_ctl_sender = self.task_ctl.response.sender.clone(); + + let mut current_metrics_data = MetricsData::new(unixtime_as_millis_f64()); + *this.data.lock().unwrap() = Some(current_metrics_data.clone()); + + spawn(async move { + let interval = interval(Duration::from_secs(1)); + pin_mut!(interval); + + loop { + select! { + _ = task_ctl_receiver.recv().fuse() => { + break; + }, + _ = interval.next().fuse() => { + + let last_metrics_data = current_metrics_data; + current_metrics_data = MetricsData::new(unixtime_as_millis_f64()); + + if let Some(rpc) = this.rpc() { + if let Err(err) = this.sample_metrics(rpc.clone(), &mut current_metrics_data).await { + log_trace!("Metrics::sample_metrics() error: {}", err); + } + } + + this.data.lock().unwrap().replace(current_metrics_data.clone()); + + if let Some(sink) = this.sink() { + let snapshot = MetricsSnapshot::from((&last_metrics_data, ¤t_metrics_data)); + if let Some(future) = sink(snapshot) { + future.await.ok(); + } + } + } + } + } + + task_ctl_sender.send(()).await.unwrap(); + }); + Ok(()) + } + + pub async fn stop_task(&self) -> Result<()> { + self.task_ctl.signal(()).await.expect("Metrics::stop_task() signal error"); + Ok(()) + } + + // --- samplers + + async fn sample_metrics(self: &Arc, rpc: Arc, data: &mut MetricsData) -> Result<()> { + let GetMetricsResponse { server_time: _, consensus_metrics, connection_metrics, bandwidth_metrics, process_metrics } = + rpc.get_metrics(true, true, true, true).await?; + + if let Some(consensus_metrics) = consensus_metrics { + data.node_blocks_submitted_count = consensus_metrics.node_blocks_submitted_count; + data.node_headers_processed_count = consensus_metrics.node_headers_processed_count; + data.node_dependencies_processed_count = consensus_metrics.node_dependencies_processed_count; + data.node_bodies_processed_count = consensus_metrics.node_bodies_processed_count; + data.node_transactions_processed_count = consensus_metrics.node_transactions_processed_count; + data.node_chain_blocks_processed_count = consensus_metrics.node_chain_blocks_processed_count; + data.node_mass_processed_count = consensus_metrics.node_mass_processed_count; + // -- + data.node_database_blocks_count = consensus_metrics.node_database_blocks_count; + data.node_database_headers_count = consensus_metrics.node_database_headers_count; + data.network_mempool_size = consensus_metrics.network_mempool_size; + data.network_tip_hashes_count = consensus_metrics.network_tip_hashes_count; + data.network_difficulty = consensus_metrics.network_difficulty; + data.network_past_median_time = consensus_metrics.network_past_median_time; + data.network_virtual_parent_hashes_count = consensus_metrics.network_virtual_parent_hashes_count; + data.network_virtual_daa_score = consensus_metrics.network_virtual_daa_score; + } + + if let Some(connection_metrics) = connection_metrics { + data.node_borsh_live_connections = connection_metrics.borsh_live_connections; + data.node_borsh_connection_attempts = connection_metrics.borsh_connection_attempts; + data.node_borsh_handshake_failures = connection_metrics.borsh_handshake_failures; + data.node_json_live_connections = connection_metrics.json_live_connections; + data.node_json_connection_attempts = connection_metrics.json_connection_attempts; + data.node_json_handshake_failures = connection_metrics.json_handshake_failures; + data.node_active_peers = connection_metrics.active_peers; + } + + if let Some(bandwidth_metrics) = bandwidth_metrics { + data.node_borsh_bytes_tx = bandwidth_metrics.borsh_bytes_tx; + data.node_borsh_bytes_rx = bandwidth_metrics.borsh_bytes_rx; + data.node_json_bytes_tx = bandwidth_metrics.json_bytes_tx; + data.node_json_bytes_rx = bandwidth_metrics.json_bytes_rx; + data.node_p2p_bytes_tx = bandwidth_metrics.p2p_bytes_tx; + data.node_p2p_bytes_rx = bandwidth_metrics.p2p_bytes_rx; + data.node_grpc_user_bytes_tx = bandwidth_metrics.grpc_bytes_tx; + data.node_grpc_user_bytes_rx = bandwidth_metrics.grpc_bytes_rx; + + data.node_total_bytes_tx = bandwidth_metrics.borsh_bytes_tx + + bandwidth_metrics.json_bytes_tx + + bandwidth_metrics.p2p_bytes_tx + + bandwidth_metrics.grpc_bytes_tx; + + data.node_total_bytes_rx = bandwidth_metrics.borsh_bytes_rx + + bandwidth_metrics.json_bytes_rx + + bandwidth_metrics.p2p_bytes_rx + + bandwidth_metrics.grpc_bytes_rx; + } + + if let Some(process_metrics) = process_metrics { + data.node_resident_set_size_bytes = process_metrics.resident_set_size; + data.node_virtual_memory_size_bytes = process_metrics.virtual_memory_size; + data.node_cpu_cores = process_metrics.core_num; + data.node_cpu_usage = process_metrics.cpu_usage; + data.node_file_handles = process_metrics.fd_num; + data.node_disk_io_read_bytes = process_metrics.disk_io_read_bytes; + data.node_disk_io_write_bytes = process_metrics.disk_io_write_bytes; + data.node_disk_io_read_per_sec = process_metrics.disk_io_read_per_sec; + data.node_disk_io_write_per_sec = process_metrics.disk_io_write_per_sec; + } + + Ok(()) + } +} diff --git a/metrics/core/src/result.rs b/metrics/core/src/result.rs new file mode 100644 index 000000000..605dc25cf --- /dev/null +++ b/metrics/core/src/result.rs @@ -0,0 +1 @@ +pub type Result = std::result::Result; diff --git a/notify/src/collector.rs b/notify/src/collector.rs index 0a48d86bb..351cbdb1b 100644 --- a/notify/src/collector.rs +++ b/notify/src/collector.rs @@ -14,7 +14,7 @@ pub type CollectorNotificationChannel = Channel; pub type CollectorNotificationSender = Sender; pub type CollectorNotificationReceiver = Receiver; -/// A notification collector, relaying notifications to a [`Notifier`]. +/// A notification collector, relaying notifications to a [`Notifier`](notifier::Notifier). /// /// A [`Collector`] is responsible for collecting notifications of /// a specific form from a specific source, convert them if necessary diff --git a/protocol/p2p/src/core/hub.rs b/protocol/p2p/src/core/hub.rs index 49b5f5685..19b087dab 100644 --- a/protocol/p2p/src/core/hub.rs +++ b/protocol/p2p/src/core/hub.rs @@ -138,6 +138,11 @@ impl Hub { self.peers.read().values().map(|r| r.as_ref().into()).collect() } + /// Returns the number of currently active peers + pub fn active_peers_len(&self) -> usize { + self.peers.read().len() + } + /// Returns whether there are currently active peers pub fn has_peers(&self) -> bool { !self.peers.read().is_empty() diff --git a/rothschild/src/main.rs b/rothschild/src/main.rs index a41a9e148..e05c238ba 100644 --- a/rothschild/src/main.rs +++ b/rothschild/src/main.rs @@ -328,7 +328,8 @@ fn generate_tx( let outputs = (0..num_outs) .map(|_| TransactionOutput { value: send_amount / num_outs, script_public_key: script_public_key.clone(), asset_type: KSH }) .collect_vec(); - let unsigned_tx = Transaction::new(TX_VERSION, inputs, outputs, TransactionAction::TransferKSH, 0, SUBNETWORK_ID_NATIVE, 0, vec![]); + let unsigned_tx = + Transaction::new(TX_VERSION, inputs, outputs, TransactionAction::TransferKSH, 0, SUBNETWORK_ID_NATIVE, 0, vec![]); let signed_tx = sign(MutableTransaction::with_entries(unsigned_tx, utxos.iter().map(|(_, entry)| entry.clone()).collect_vec()), schnorr_key); signed_tx.tx diff --git a/rpc/core/src/api/ctl.rs b/rpc/core/src/api/ctl.rs index 96e935924..0cef8ecf4 100644 --- a/rpc/core/src/api/ctl.rs +++ b/rpc/core/src/api/ctl.rs @@ -23,7 +23,7 @@ struct Inner { } /// RPC channel control helper. This is a companion -/// struct to [`RpcApi`](crate::api::RpcApi) that +/// struct to [`RpcApi`](crate::api::rpc::RpcApi) that /// provides signaling for RPC open/close events as /// well as an optional connection descriptor (URL). #[derive(Default, Clone)] @@ -40,7 +40,7 @@ impl RpcCtl { Self { inner: Arc::new(Inner { descriptor: Mutex::new(Some(descriptor.to_string())), ..Inner::default() }) } } - /// Obtain internal multiplexer (MPMC channel for [`RpcCtlOp`] operations) + /// Obtain internal multiplexer (MPMC channel for [`RpcState`] operations) pub fn multiplexer(&self) -> &Multiplexer { &self.inner.multiplexer } diff --git a/rpc/core/src/api/rpc.rs b/rpc/core/src/api/rpc.rs index 825e26a23..e84de17fc 100644 --- a/rpc/core/src/api/rpc.rs +++ b/rpc/core/src/api/rpc.rs @@ -28,8 +28,14 @@ pub trait RpcApi: Sync + Send + AnySync { // --- - async fn get_metrics(&self, process_metrics: bool, consensus_metrics: bool) -> RpcResult { - self.get_metrics_call(GetMetricsRequest { process_metrics, consensus_metrics }).await + async fn get_metrics( + &self, + process_metrics: bool, + connection_metrics: bool, + bandwidth_metrics: bool, + consensus_metrics: bool, + ) -> RpcResult { + self.get_metrics_call(GetMetricsRequest { process_metrics, connection_metrics, bandwidth_metrics, consensus_metrics }).await } async fn get_metrics_call(&self, request: GetMetricsRequest) -> RpcResult; @@ -290,8 +296,8 @@ pub trait RpcApi: Sync + Send + AnySync { } async fn get_coin_supply_call(&self, request: GetCoinSupplyRequest) -> RpcResult; - async fn get_daa_score_timestamp_estimate(&self, daa_scores: Vec) -> RpcResult { - self.get_daa_score_timestamp_estimate_call(GetDaaScoreTimestampEstimateRequest { daa_scores }).await + async fn get_daa_score_timestamp_estimate(&self, daa_scores: Vec) -> RpcResult> { + Ok(self.get_daa_score_timestamp_estimate_call(GetDaaScoreTimestampEstimateRequest { daa_scores }).await?.timestamps) } async fn get_daa_score_timestamp_estimate_call( &self, diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index ad149babd..610a7f8dd 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -685,6 +685,8 @@ pub struct PingResponse {} #[serde(rename_all = "camelCase")] pub struct GetMetricsRequest { pub process_metrics: bool, + pub connection_metrics: bool, + pub bandwidth_metrics: bool, pub consensus_metrics: bool, } @@ -693,32 +695,61 @@ pub struct GetMetricsRequest { pub struct ProcessMetrics { pub resident_set_size: u64, pub virtual_memory_size: u64, - pub core_num: u64, - pub cpu_usage: f64, - pub fd_num: u64, + pub core_num: u32, + pub cpu_usage: f32, + pub fd_num: u32, pub disk_io_read_bytes: u64, pub disk_io_write_bytes: u64, - pub disk_io_read_per_sec: f64, - pub disk_io_write_per_sec: f64, + pub disk_io_read_per_sec: f32, + pub disk_io_write_per_sec: f32, +} - pub borsh_live_connections: u64, +#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] +#[serde(rename_all = "camelCase")] +pub struct ConnectionMetrics { + pub borsh_live_connections: u32, pub borsh_connection_attempts: u64, pub borsh_handshake_failures: u64, - pub json_live_connections: u64, + pub json_live_connections: u32, pub json_connection_attempts: u64, pub json_handshake_failures: u64, + + pub active_peers: u32, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] +#[serde(rename_all = "camelCase")] +pub struct BandwidthMetrics { + pub borsh_bytes_tx: u64, + pub borsh_bytes_rx: u64, + pub json_bytes_tx: u64, + pub json_bytes_rx: u64, + pub p2p_bytes_tx: u64, + pub p2p_bytes_rx: u64, + pub grpc_bytes_tx: u64, + pub grpc_bytes_rx: u64, } #[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] #[serde(rename_all = "camelCase")] pub struct ConsensusMetrics { - pub blocks_submitted: u64, - pub header_counts: u64, - pub dep_counts: u64, - pub body_counts: u64, - pub txs_counts: u64, - pub chain_block_counts: u64, - pub mass_counts: u64, + pub node_blocks_submitted_count: u64, + pub node_headers_processed_count: u64, + pub node_dependencies_processed_count: u64, + pub node_bodies_processed_count: u64, + pub node_transactions_processed_count: u64, + pub node_chain_blocks_processed_count: u64, + pub node_mass_processed_count: u64, + + pub node_database_blocks_count: u64, + pub node_database_headers_count: u64, + + pub network_mempool_size: u64, + pub network_tip_hashes_count: u32, + pub network_difficulty: f64, + pub network_past_median_time: u64, + pub network_virtual_parent_hashes_count: u32, + pub network_virtual_daa_score: u64, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] @@ -726,12 +757,20 @@ pub struct ConsensusMetrics { pub struct GetMetricsResponse { pub server_time: u64, pub process_metrics: Option, + pub connection_metrics: Option, + pub bandwidth_metrics: Option, pub consensus_metrics: Option, } impl GetMetricsResponse { - pub fn new(server_time: u64, process_metrics: Option, consensus_metrics: Option) -> Self { - Self { process_metrics, consensus_metrics, server_time } + pub fn new( + server_time: u64, + process_metrics: Option, + connection_metrics: Option, + bandwidth_metrics: Option, + consensus_metrics: Option, + ) -> Self { + Self { process_metrics, connection_metrics, bandwidth_metrics, consensus_metrics, server_time } } } diff --git a/rpc/core/src/model/tx.rs b/rpc/core/src/model/tx.rs index 62b260cfb..9d6a1e133 100644 --- a/rpc/core/src/model/tx.rs +++ b/rpc/core/src/model/tx.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use kash_addresses::Address; use kash_consensus_core::asset_type::AssetType; use kash_consensus_core::tx::{ - ScriptPublicKey, ScriptVec, TransactionId, TransactionInput, TransactionAction, TransactionOutpoint, TransactionOutput, UtxoEntry, + ScriptPublicKey, ScriptVec, TransactionAction, TransactionId, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, }; use serde::{Deserialize, Serialize}; diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index d61a4745c..7b22079c4 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -757,21 +757,36 @@ message PingResponseMessage{ message ProcessMetrics{ uint64 residentSetSize = 1; uint64 virtualMemorySize = 2; - uint64 coreNum = 3; - double cpuUsage = 4; - uint64 fdNum = 5; + uint32 coreNum = 3; + float cpuUsage = 4; + uint32 fdNum = 5; uint64 diskIoReadBytes = 6; uint64 diskIoWriteBytes = 7; - double diskIoReadPerSec = 8; - double diskIoWritePerSec = 9; + float diskIoReadPerSec = 8; + float diskIoWritePerSec = 9; +} - uint64 borshLiveConnections = 31; +message ConnectionMetrics { + uint32 borshLiveConnections = 31; uint64 borshConnectionAttempts = 32; uint64 borshHandshakeFailures = 33; - uint64 jsonLiveConnections = 41; + uint32 jsonLiveConnections = 41; uint64 jsonConnectionAttempts = 42; uint64 jsonHandshakeFailures = 43; + + uint32 activePeers = 51; +} + +message BandwidthMetrics { + uint64 borshBytesTx = 61; + uint64 borshBytesRx = 62; + uint64 jsonBytesTx = 63; + uint64 jsonBytesRx = 64; + uint64 grpcP2pBytesTx = 65; + uint64 grpcP2pBytesRx = 66; + uint64 grpcUserBytesTx = 67; + uint64 grpcUserBytesRx = 68; } message ConsensusMetrics{ @@ -782,17 +797,30 @@ message ConsensusMetrics{ uint64 txsCounts = 5; uint64 chainBlockCounts = 6; uint64 massCounts = 7; + + uint64 blockCount = 11; + uint64 headerCount = 12; + uint64 mempoolSize = 13; + uint32 tipHashesCount = 14; + double difficulty = 15; + uint64 pastMedianTime = 16; + uint32 virtualParentHashesCount = 17; + uint64 virtualDaaScore = 18; } message GetMetricsRequestMessage{ bool processMetrics = 1; - bool consensusMetrics = 2; + bool connectionMetrics = 2; + bool bandwidthMetrics = 3; + bool consensusMetrics = 4; } message GetMetricsResponseMessage{ uint64 serverTime = 1; ProcessMetrics processMetrics = 11; - ConsensusMetrics consensusMetrics = 12; + ConnectionMetrics connectionMetrics = 12; + BandwidthMetrics bandwidthMetrics = 13; + ConsensusMetrics consensusMetrics = 14; RPCError error = 1000; } diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index 5de0c127a..e0b8df5d4 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -400,13 +400,17 @@ from!(RpcResult<&kash_rpc_core::PingResponse>, protowire::PingResponseMessage); from!(item: &kash_rpc_core::GetMetricsRequest, protowire::GetMetricsRequestMessage, { Self { process_metrics: item.process_metrics, - consensus_metrics: item.consensus_metrics + connection_metrics: item.connection_metrics, + bandwidth_metrics: item.bandwidth_metrics, + consensus_metrics: item.consensus_metrics, } }); from!(item: RpcResult<&kash_rpc_core::GetMetricsResponse>, protowire::GetMetricsResponseMessage, { Self { server_time: item.server_time, process_metrics: item.process_metrics.as_ref().map(|x| x.into()), + connection_metrics: item.connection_metrics.as_ref().map(|x| x.into()), + bandwidth_metrics: item.bandwidth_metrics.as_ref().map(|x| x.into()), consensus_metrics: item.consensus_metrics.as_ref().map(|x| x.into()), error: None, } @@ -791,12 +795,14 @@ try_from!(&protowire::PingRequestMessage, kash_rpc_core::PingRequest); try_from!(&protowire::PingResponseMessage, RpcResult); try_from!(item: &protowire::GetMetricsRequestMessage, kash_rpc_core::GetMetricsRequest, { - Self { process_metrics: item.process_metrics, consensus_metrics: item.consensus_metrics } + Self { process_metrics: item.process_metrics, connection_metrics: item.connection_metrics, bandwidth_metrics:item.bandwidth_metrics, consensus_metrics: item.consensus_metrics } }); try_from!(item: &protowire::GetMetricsResponseMessage, RpcResult, { Self { server_time: item.server_time, process_metrics: item.process_metrics.as_ref().map(|x| x.try_into()).transpose()?, + connection_metrics: item.connection_metrics.as_ref().map(|x| x.try_into()).transpose()?, + bandwidth_metrics: item.bandwidth_metrics.as_ref().map(|x| x.try_into()).transpose()?, consensus_metrics: item.consensus_metrics.as_ref().map(|x| x.try_into()).transpose()?, } }); diff --git a/rpc/grpc/core/src/convert/metrics.rs b/rpc/grpc/core/src/convert/metrics.rs index 6c7f44ac8..8eb3ef03a 100644 --- a/rpc/grpc/core/src/convert/metrics.rs +++ b/rpc/grpc/core/src/convert/metrics.rs @@ -17,24 +17,52 @@ from!(item: &kash_rpc_core::ProcessMetrics, protowire::ProcessMetrics, { disk_io_write_bytes: item.disk_io_write_bytes, disk_io_read_per_sec: item.disk_io_read_per_sec, disk_io_write_per_sec: item.disk_io_write_per_sec, + } +}); + +from!(item: &kash_rpc_core::ConnectionMetrics, protowire::ConnectionMetrics, { + Self { borsh_live_connections: item.borsh_live_connections, borsh_connection_attempts: item.borsh_connection_attempts, borsh_handshake_failures: item.borsh_handshake_failures, json_live_connections: item.json_live_connections, json_connection_attempts: item.json_connection_attempts, json_handshake_failures: item.json_handshake_failures, + active_peers: item.active_peers, + } +}); + +from!(item: &kash_rpc_core::BandwidthMetrics, protowire::BandwidthMetrics, { + Self { + borsh_bytes_tx: item.borsh_bytes_tx, + borsh_bytes_rx: item.borsh_bytes_rx, + json_bytes_tx: item.json_bytes_tx, + json_bytes_rx: item.json_bytes_rx, + grpc_p2p_bytes_tx: item.p2p_bytes_tx, + grpc_p2p_bytes_rx: item.p2p_bytes_rx, + grpc_user_bytes_tx: item.grpc_bytes_tx, + grpc_user_bytes_rx: item.grpc_bytes_rx, } }); from!(item: &kash_rpc_core::ConsensusMetrics, protowire::ConsensusMetrics, { Self { - blocks_submitted: item.blocks_submitted, - header_counts: item.header_counts, - dep_counts: item.dep_counts, - body_counts: item.body_counts, - txs_counts: item.txs_counts, - chain_block_counts: item.chain_block_counts, - mass_counts: item.mass_counts, + blocks_submitted: item.node_blocks_submitted_count, + header_counts: item.node_headers_processed_count, + dep_counts: item.node_dependencies_processed_count, + body_counts: item.node_bodies_processed_count, + txs_counts: item.node_transactions_processed_count, + chain_block_counts: item.node_chain_blocks_processed_count, + mass_counts: item.node_mass_processed_count, + + block_count: item.node_database_blocks_count, + header_count: item.node_database_headers_count, + mempool_size: item.network_mempool_size, + tip_hashes_count: item.network_tip_hashes_count, + difficulty: item.network_difficulty, + past_median_time: item.network_past_median_time, + virtual_parent_hashes_count: item.network_virtual_parent_hashes_count, + virtual_daa_score: item.network_virtual_daa_score, } }); @@ -53,23 +81,51 @@ try_from!(item: &protowire::ProcessMetrics, kash_rpc_core::ProcessMetrics, { disk_io_write_bytes: item.disk_io_write_bytes, disk_io_read_per_sec: item.disk_io_read_per_sec, disk_io_write_per_sec: item.disk_io_write_per_sec, + } +}); + +try_from!(item: &protowire::ConnectionMetrics, kash_rpc_core::ConnectionMetrics, { + Self { borsh_live_connections: item.borsh_live_connections, borsh_connection_attempts: item.borsh_connection_attempts, borsh_handshake_failures: item.borsh_handshake_failures, json_live_connections: item.json_live_connections, json_connection_attempts: item.json_connection_attempts, json_handshake_failures: item.json_handshake_failures, + active_peers: item.active_peers, + } +}); + +try_from!(item: &protowire::BandwidthMetrics, kash_rpc_core::BandwidthMetrics, { + Self { + borsh_bytes_tx: item.borsh_bytes_tx, + borsh_bytes_rx: item.borsh_bytes_rx, + json_bytes_tx: item.json_bytes_tx, + json_bytes_rx: item.json_bytes_rx, + p2p_bytes_tx: item.grpc_p2p_bytes_tx, + p2p_bytes_rx: item.grpc_p2p_bytes_rx, + grpc_bytes_tx: item.grpc_user_bytes_tx, + grpc_bytes_rx: item.grpc_user_bytes_rx, } }); try_from!(item: &protowire::ConsensusMetrics, kash_rpc_core::ConsensusMetrics, { Self { - blocks_submitted: item.blocks_submitted, - header_counts: item.header_counts, - dep_counts: item.dep_counts, - body_counts: item.body_counts, - txs_counts: item.txs_counts, - chain_block_counts: item.chain_block_counts, - mass_counts: item.mass_counts, + node_blocks_submitted_count: item.blocks_submitted, + node_headers_processed_count: item.header_counts, + node_dependencies_processed_count: item.dep_counts, + node_bodies_processed_count: item.body_counts, + node_transactions_processed_count: item.txs_counts, + node_chain_blocks_processed_count: item.chain_block_counts, + node_mass_processed_count: item.mass_counts, + + node_database_blocks_count: item.block_count, + node_database_headers_count: item.header_count, + network_mempool_size: item.mempool_size, + network_tip_hashes_count: item.tip_hashes_count, + network_difficulty: item.difficulty, + network_past_median_time: item.past_median_time, + network_virtual_parent_hashes_count: item.virtual_parent_hashes_count, + network_virtual_daa_score: item.virtual_daa_score, } }); diff --git a/rpc/service/Cargo.toml b/rpc/service/Cargo.toml index d1fd8f9e9..57b20423a 100644 --- a/rpc/service/Cargo.toml +++ b/rpc/service/Cargo.toml @@ -27,8 +27,8 @@ kash-txscript.workspace = true kash-utils.workspace = true kash-utils-tower.workspace = true kash-utxoindex.workspace = true -kash-wrpc-core.workspace = true async-trait.workspace = true log.workspace = true tokio.workspace = true +workflow-rpc.workspace = true \ No newline at end of file diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index 5558e68d5..9fc4a6d18 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -59,13 +59,13 @@ use kash_txscript::{extract_script_pub_key_address, pay_to_address_script}; use kash_utils::{channel::Channel, triggers::SingleTrigger}; use kash_utils_tower::counters::TowerConnectionCounters; use kash_utxoindex::api::UtxoIndexProxy; -use kash_wrpc_core::ServerCounters as WrpcServerCounters; use std::{ collections::HashMap, iter::once, sync::{atomic::Ordering, Arc}, vec, }; +use workflow_rpc::server::WebSocketCounters as WrpcServerCounters; /// A service implementing the Rpc API at kash_rpc_core level. /// @@ -100,13 +100,7 @@ pub struct RpcCoreService { wrpc_json_counters: Arc, shutdown: SingleTrigger, perf_monitor: Arc>>, - - // parking here for now - // will be integrated into - // metrics in the upcoming PR - #[allow(dead_code)] p2p_tower_counters: Arc, - #[allow(dead_code)] grpc_tower_counters: Arc, } @@ -760,39 +754,72 @@ impl RpcApi for RpcCoreService { disk_io_write_bytes, disk_io_read_per_sec, disk_io_write_per_sec, - .. } = self.perf_monitor.snapshot(); + let process_metrics = req.process_metrics.then_some(ProcessMetrics { resident_set_size, virtual_memory_size, - core_num: core_num as u64, - cpu_usage, - fd_num: fd_num as u64, + core_num: core_num as u32, + cpu_usage: cpu_usage as f32, + fd_num: fd_num as u32, disk_io_read_bytes, disk_io_write_bytes, - disk_io_read_per_sec, - disk_io_write_per_sec, - borsh_live_connections: self.wrpc_borsh_counters.live_connections.load(Ordering::Relaxed), - borsh_connection_attempts: self.wrpc_borsh_counters.connection_attempts.load(Ordering::Relaxed), - borsh_handshake_failures: self.wrpc_borsh_counters.handshake_failures.load(Ordering::Relaxed), - json_live_connections: self.wrpc_json_counters.live_connections.load(Ordering::Relaxed), - json_connection_attempts: self.wrpc_json_counters.connection_attempts.load(Ordering::Relaxed), - json_handshake_failures: self.wrpc_json_counters.handshake_failures.load(Ordering::Relaxed), + disk_io_read_per_sec: disk_io_read_per_sec as f32, + disk_io_write_per_sec: disk_io_write_per_sec as f32, + }); + + let connection_metrics = req.connection_metrics.then_some(ConnectionMetrics { + borsh_live_connections: self.wrpc_borsh_counters.active_connections.load(Ordering::Relaxed) as u32, + borsh_connection_attempts: self.wrpc_borsh_counters.total_connections.load(Ordering::Relaxed) as u64, + borsh_handshake_failures: self.wrpc_borsh_counters.handshake_failures.load(Ordering::Relaxed) as u64, + json_live_connections: self.wrpc_json_counters.active_connections.load(Ordering::Relaxed) as u32, + json_connection_attempts: self.wrpc_json_counters.total_connections.load(Ordering::Relaxed) as u64, + json_handshake_failures: self.wrpc_json_counters.handshake_failures.load(Ordering::Relaxed) as u64, + + active_peers: self.flow_context.hub().active_peers_len() as u32, }); - let consensus_metrics = req.consensus_metrics.then_some(ConsensusMetrics { - blocks_submitted: self.processing_counters.blocks_submitted.load(Ordering::SeqCst), - header_counts: self.processing_counters.header_counts.load(Ordering::SeqCst), - dep_counts: self.processing_counters.dep_counts.load(Ordering::SeqCst), - body_counts: self.processing_counters.body_counts.load(Ordering::SeqCst), - txs_counts: self.processing_counters.txs_counts.load(Ordering::SeqCst), - chain_block_counts: self.processing_counters.chain_block_counts.load(Ordering::SeqCst), - mass_counts: self.processing_counters.mass_counts.load(Ordering::SeqCst), + let bandwidth_metrics = req.bandwidth_metrics.then_some(BandwidthMetrics { + borsh_bytes_tx: self.wrpc_borsh_counters.tx_bytes.load(Ordering::Relaxed) as u64, + borsh_bytes_rx: self.wrpc_borsh_counters.rx_bytes.load(Ordering::Relaxed) as u64, + json_bytes_tx: self.wrpc_json_counters.tx_bytes.load(Ordering::Relaxed) as u64, + json_bytes_rx: self.wrpc_json_counters.rx_bytes.load(Ordering::Relaxed) as u64, + p2p_bytes_tx: self.p2p_tower_counters.bytes_tx.load(Ordering::Relaxed) as u64, + p2p_bytes_rx: self.p2p_tower_counters.bytes_rx.load(Ordering::Relaxed) as u64, + grpc_bytes_tx: self.grpc_tower_counters.bytes_tx.load(Ordering::Relaxed) as u64, + grpc_bytes_rx: self.grpc_tower_counters.bytes_rx.load(Ordering::Relaxed) as u64, }); + let consensus_metrics = if req.consensus_metrics { + let session = self.consensus_manager.consensus().unguarded_session(); + let block_count = session.async_estimate_block_count().await; + + Some(ConsensusMetrics { + node_blocks_submitted_count: self.processing_counters.blocks_submitted.load(Ordering::SeqCst), + node_headers_processed_count: self.processing_counters.header_counts.load(Ordering::SeqCst), + node_dependencies_processed_count: self.processing_counters.dep_counts.load(Ordering::SeqCst), + node_bodies_processed_count: self.processing_counters.body_counts.load(Ordering::SeqCst), + node_transactions_processed_count: self.processing_counters.txs_counts.load(Ordering::SeqCst), + node_chain_blocks_processed_count: self.processing_counters.chain_block_counts.load(Ordering::SeqCst), + node_mass_processed_count: self.processing_counters.mass_counts.load(Ordering::SeqCst), + // --- + node_database_blocks_count: block_count.block_count, + node_database_headers_count: block_count.header_count, + // --- + network_mempool_size: self.mining_manager.clone().transaction_count(TransactionQuery::TransactionsOnly).await as u64, + network_tip_hashes_count: session.async_get_tips_len().await as u32, + network_difficulty: self.consensus_converter.get_difficulty_ratio(session.async_get_virtual_bits().await), + network_past_median_time: session.async_get_virtual_past_median_time().await, + network_virtual_parent_hashes_count: session.async_get_virtual_parents_len().await as u32, + network_virtual_daa_score: session.async_get_virtual_daa_score().await, + }) + } else { + None + }; + let server_time = unix_now(); - let response = GetMetricsResponse { server_time, process_metrics, consensus_metrics }; + let response = GetMetricsResponse { server_time, process_metrics, connection_metrics, bandwidth_metrics, consensus_metrics }; Ok(response) } diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index 05d13207b..6dce75001 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -140,13 +140,13 @@ impl Debug for Inner { #[async_trait] impl SubscriptionManager for Inner { async fn start_notify(&self, _: ListenerId, scope: Scope) -> NotifyResult<()> { - log_trace!("[WrpcClient] start_notify: {:?}", scope); + // log_trace!("[WrpcClient] start_notify: {:?}", scope); self.start_notify_to_client(scope).await.map_err(|err| NotifyError::General(err.to_string()))?; Ok(()) } async fn stop_notify(&self, _: ListenerId, scope: Scope) -> NotifyResult<()> { - log_trace!("[WrpcClient] stop_notify: {:?}", scope); + // log_trace!("[WrpcClient] stop_notify: {:?}", scope); self.stop_notify_to_client(scope).await.map_err(|err| NotifyError::General(err.to_string()))?; Ok(()) } @@ -299,23 +299,15 @@ impl KashRpcClient { self.notification_mode } - // pub fn ctl_multiplexer(&self) -> &Multiplexer { - // &self.inner.ctl_multiplexer - // } - pub fn ctl(&self) -> &RpcCtl { &self.inner.rpc_ctl } - pub fn parse_url_with_network_type(&self, url: Option, network_type: NetworkType) -> Result> { + pub fn parse_url_with_network_type(&self, url: String, network_type: NetworkType) -> Result { Self::parse_url(url, self.inner.encoding, network_type) } - pub fn parse_url(url: Option, encoding: Encoding, network_type: NetworkType) -> Result> { - let Some(url) = url else { - return Ok(None); - }; - + pub fn parse_url(url: String, encoding: Encoding, network_type: NetworkType) -> Result { let parse_output = parse_host(&url).map_err(|err| Error::Custom(err.to_string()))?; let scheme = parse_output.scheme.map(Ok).unwrap_or_else(|| { if !application_runtime::is_web() { @@ -338,7 +330,7 @@ impl KashRpcClient { }); let path_str = parse_output.path; - Ok(Some(format!("{}://{}:{}{}", scheme, parse_output.host.to_string(), port, path_str))) + Ok(format!("{}://{}:{}{}", scheme, parse_output.host.to_string(), port, path_str)) } async fn start_rpc_ctl_service(&self) -> Result<()> { @@ -354,12 +346,10 @@ impl KashRpcClient { if let Ok(msg) = msg { match msg { WrpcCtl::Open => { - // inner.rpc_ctl.set_descriptor(Some(inner.rpc.url())); inner.rpc_ctl.signal_open().await.expect("(KashRpcClient) rpc_ctl.signal_open() error"); } WrpcCtl::Close => { inner.rpc_ctl.signal_close().await.expect("(KashRpcClient) rpc_ctl.signal_close() error"); - // inner.rpc_ctl.set_descriptor(None); } } } else { diff --git a/rpc/wrpc/client/src/wasm.rs b/rpc/wrpc/client/src/wasm.rs index 0bbdf8285..33e923e80 100644 --- a/rpc/wrpc/client/src/wasm.rs +++ b/rpc/wrpc/client/src/wasm.rs @@ -64,7 +64,8 @@ impl RpcClient { /// Connect to the Kash RPC server. This function starts a background /// task that connects and reconnects to the server if the connection - /// is terminated. Use [`disconnect()`] to terminate the connection. + /// is terminated. Use [`disconnect()`](Self::disconnect()) to + /// terminate the connection. pub async fn connect(&self, args: JsValue) -> Result<()> { let options: ConnectOptions = args.try_into()?; @@ -188,8 +189,7 @@ impl RpcClient { /// #[wasm_bindgen(js_name = parseUrl)] pub fn parse_url(url: &str, encoding: Encoding, network: Network) -> Result { - let url_ = KashRpcClient::parse_url(Some(url.to_string()), encoding, network.try_into()?)?; - let url_ = url_.ok_or(Error::custom(format!("received a malformed URL: {url}")))?; + let url_ = KashRpcClient::parse_url(url.to_string(), encoding, network.try_into()?)?; Ok(url_) } } diff --git a/rpc/wrpc/core/Cargo.toml b/rpc/wrpc/core/Cargo.toml index bf2641d8f..e69de29bb 100644 --- a/rpc/wrpc/core/Cargo.toml +++ b/rpc/wrpc/core/Cargo.toml @@ -1,13 +0,0 @@ -[package] -name = "kash-wrpc-core" -description = "Kash wRPC core" -version.workspace = true -edition.workspace = true -authors.workspace = true -include.workspace = true -license.workspace = true - -[lib] -crate-type = ["cdylib", "lib"] - -[dependencies] diff --git a/rpc/wrpc/core/src/lib.rs b/rpc/wrpc/core/src/lib.rs deleted file mode 100644 index b8d533e20..000000000 --- a/rpc/wrpc/core/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -use std::sync::atomic::AtomicU64; - -#[derive(Debug, Default)] -pub struct ServerCounters { - pub live_connections: AtomicU64, - pub connection_attempts: AtomicU64, - pub handshake_failures: AtomicU64, -} diff --git a/rpc/wrpc/proxy/Cargo.toml b/rpc/wrpc/proxy/Cargo.toml index 4810129ca..483a393bd 100644 --- a/rpc/wrpc/proxy/Cargo.toml +++ b/rpc/wrpc/proxy/Cargo.toml @@ -14,7 +14,6 @@ kash-consensus-core.workspace = true kash-grpc-client.workspace = true kash-rpc-core.workspace = true kash-rpc-macros.workspace = true -kash-wrpc-core.workspace = true kash-wrpc-server.workspace = true num_cpus.workspace = true thiserror.workspace = true diff --git a/rpc/wrpc/proxy/src/main.rs b/rpc/wrpc/proxy/src/main.rs index 2e9b5c09a..5379a9b5f 100644 --- a/rpc/wrpc/proxy/src/main.rs +++ b/rpc/wrpc/proxy/src/main.rs @@ -4,7 +4,6 @@ mod result; use clap::Parser; use kash_consensus_core::network::NetworkType; use kash_rpc_core::api::ops::RpcApiOps; -use kash_wrpc_core::ServerCounters as WrpcServerCounters; use kash_wrpc_server::{ connection::Connection, router::Router, @@ -15,6 +14,7 @@ use result::Result; use std::sync::Arc; use workflow_log::*; use workflow_rpc::server::prelude::*; +use workflow_rpc::server::WebSocketCounters; #[derive(Debug, Parser)] #[clap(name = "proxy")] @@ -80,13 +80,17 @@ async fn main() -> Result<()> { log_info!(""); log_info!("Proxy routing to `{}` on {}", network_type, options.grpc_proxy_address.as_ref().unwrap()); - let counters = Arc::new(WrpcServerCounters::default()); + let counters = Arc::new(WebSocketCounters::default()); let tasks = threads.unwrap_or_else(num_cpus::get); - let rpc_handler = Arc::new(KashRpcHandler::new(tasks, encoding, None, options.clone(), counters)); + let rpc_handler = Arc::new(KashRpcHandler::new(tasks, encoding, None, options.clone())); let router = Arc::new(Router::new(rpc_handler.server.clone())); - let server = - RpcServer::new_with_encoding::(encoding, rpc_handler.clone(), router.interface.clone()); + let server = RpcServer::new_with_encoding::( + encoding, + rpc_handler.clone(), + router.interface.clone(), + Some(counters), + ); log_info!("Kash wRPC server is listening on {}", options.listen_address); log_info!("Using `{encoding}` protocol encoding"); diff --git a/rpc/wrpc/server/Cargo.toml b/rpc/wrpc/server/Cargo.toml index 4fc331302..0a1a9b6eb 100644 --- a/rpc/wrpc/server/Cargo.toml +++ b/rpc/wrpc/server/Cargo.toml @@ -22,7 +22,6 @@ kash-rpc-core.workspace = true kash-rpc-macros.workspace = true kash-rpc-service.workspace = true kash-utils.workspace = true -kash-wrpc-core.workspace = true log.workspace = true num_cpus.workspace = true paste.workspace = true diff --git a/rpc/wrpc/server/src/server.rs b/rpc/wrpc/server/src/server.rs index 3423bd2b1..6fc0f7367 100644 --- a/rpc/wrpc/server/src/server.rs +++ b/rpc/wrpc/server/src/server.rs @@ -125,11 +125,11 @@ impl Server { } pub async fn disconnect(&self, connection: Connection) { - log_info!("WebSocket disconnected: {}", connection.peer()); + // log_info!("WebSocket disconnected: {}", connection.peer()); if let Some(rpc_core) = &self.inner.rpc_core { if let Some(listener_id) = connection.listener_id() { rpc_core.wrpc_notifier.unregister_listener(listener_id).unwrap_or_else(|err| { - format!("WebSocket {} (disconnected) error unregistering the notification listener: {err}", connection.peer()); + log_error!("WebSocket {} (disconnected) error unregistering the notification listener: {err}", connection.peer()); }); } } else { diff --git a/rpc/wrpc/server/src/service.rs b/rpc/wrpc/server/src/service.rs index f8c28dd9b..77fe49262 100644 --- a/rpc/wrpc/server/src/service.rs +++ b/rpc/wrpc/server/src/service.rs @@ -8,12 +8,10 @@ use kash_core::{ use kash_rpc_core::api::ops::RpcApiOps; use kash_rpc_service::service::RpcCoreService; use kash_utils::triggers::SingleTrigger; -pub use kash_wrpc_core::ServerCounters; -use std::sync::atomic::Ordering; use std::sync::Arc; use tokio::sync::oneshot::{channel as oneshot_channel, Sender as OneshotSender}; use workflow_rpc::server::prelude::*; -pub use workflow_rpc::server::{Encoding as WrpcEncoding, WebSocketConfig}; +pub use workflow_rpc::server::{Encoding as WrpcEncoding, WebSocketConfig, WebSocketCounters}; static MAX_WRPC_MESSAGE_SIZE: usize = 1024 * 1024 * 128; // 128MB @@ -48,7 +46,6 @@ impl Default for Options { pub struct KashRpcHandler { pub server: Server, pub options: Arc, - pub counters: Arc, } impl KashRpcHandler { @@ -57,9 +54,8 @@ impl KashRpcHandler { encoding: WrpcEncoding, core_service: Option>, options: Arc, - counters: Arc, ) -> KashRpcHandler { - KashRpcHandler { server: Server::new(tasks, encoding, core_service, options.clone()), options, counters } + KashRpcHandler { server: Server::new(tasks, encoding, core_service, options.clone()), options } } } @@ -67,11 +63,6 @@ impl KashRpcHandler { impl RpcHandler for KashRpcHandler { type Context = Connection; - async fn connect(self: Arc, _peer: &SocketAddr) -> WebSocketResult<()> { - self.counters.connection_attempts.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - async fn handshake( self: Arc, peer: &SocketAddr, @@ -89,7 +80,6 @@ impl RpcHandler for KashRpcHandler { // .await let connection = self.server.connect(peer, messenger).await.map_err(|err| err.to_string())?; - self.counters.live_connections.fetch_add(1, Ordering::SeqCst); Ok(connection) } @@ -97,7 +87,6 @@ impl RpcHandler for KashRpcHandler { /// before dropping it. This is the last chance to cleanup and resources owned by /// this connection. Delegate to Server. async fn disconnect(self: Arc, ctx: Self::Context, _result: WebSocketResult<()>) { - self.counters.live_connections.fetch_sub(1, Ordering::SeqCst); self.server.disconnect(ctx).await; } } @@ -111,7 +100,6 @@ pub struct WrpcService { server: RpcServer, rpc_handler: Arc, shutdown: SingleTrigger, - // counters: Arc, } impl WrpcService { @@ -120,12 +108,12 @@ impl WrpcService { tasks: usize, core_service: Option>, encoding: &Encoding, - counters: Arc, + counters: Arc, options: Options, ) -> Self { let options = Arc::new(options); // Create handle to manage connections - let rpc_handler = Arc::new(KashRpcHandler::new(tasks, *encoding, core_service, options.clone(), counters)); + let rpc_handler = Arc::new(KashRpcHandler::new(tasks, *encoding, core_service, options.clone())); // Create router (initializes Interface registering RPC method and notification handlers) let router = Arc::new(Router::new(rpc_handler.server.clone())); @@ -134,6 +122,7 @@ impl WrpcService { *encoding, rpc_handler.clone(), router.interface.clone(), + Some(counters), ); WrpcService { options, server, rpc_handler, shutdown: SingleTrigger::default() } diff --git a/simpa/src/simulator/miner.rs b/simpa/src/simulator/miner.rs index b39be0398..156b70142 100644 --- a/simpa/src/simulator/miner.rs +++ b/simpa/src/simulator/miner.rs @@ -10,7 +10,7 @@ use kash_consensus_core::coinbase::MinerData; use kash_consensus_core::sign::sign; use kash_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kash_consensus_core::tx::{ - MutableTransaction, ScriptPublicKey, ScriptVec, Transaction, TransactionInput, TransactionAction, TransactionOutpoint, + MutableTransaction, ScriptPublicKey, ScriptVec, Transaction, TransactionAction, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, }; use kash_consensus_core::utxo::utxo_view::UtxoView; diff --git a/testing/integration/src/consensus_integration_tests.rs b/testing/integration/src/consensus_integration_tests.rs index 577eb24ed..892cc5041 100644 --- a/testing/integration/src/consensus_integration_tests.rs +++ b/testing/integration/src/consensus_integration_tests.rs @@ -31,7 +31,7 @@ use kash_consensus_core::network::{NetworkId, NetworkType::Mainnet}; use kash_consensus_core::subnets::SubnetworkId; use kash_consensus_core::trusted::{ExternalGhostdagData, TrustedBlock}; use kash_consensus_core::tx::{ - ScriptPublicKey, Transaction, TransactionInput, TransactionAction, TransactionOutpoint, TransactionOutput, UtxoEntry, + ScriptPublicKey, Transaction, TransactionAction, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, }; use kash_consensus_core::{blockhash, hashing, BlockHashMap, BlueWorkType}; use kash_consensus_notify::root::ConsensusNotificationRoot; diff --git a/testing/integration/src/rpc_tests.rs b/testing/integration/src/rpc_tests.rs index 2298f2100..8c47f0e3c 100644 --- a/testing/integration/src/rpc_tests.rs +++ b/testing/integration/src/rpc_tests.rs @@ -456,28 +456,48 @@ async fn sanity_test() { let rpc_client = client.clone(); tst!(op, { let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { consensus_metrics: true, process_metrics: true }) + .get_metrics_call(GetMetricsRequest { + consensus_metrics: true, + connection_metrics: true, + bandwidth_metrics: true, + process_metrics: true, + }) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_some()); assert!(get_metrics_call_response.consensus_metrics.is_some()); let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { consensus_metrics: false, process_metrics: true }) + .get_metrics_call(GetMetricsRequest { + consensus_metrics: false, + connection_metrics: true, + bandwidth_metrics: true, + process_metrics: true, + }) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_some()); assert!(get_metrics_call_response.consensus_metrics.is_none()); let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { consensus_metrics: true, process_metrics: false }) + .get_metrics_call(GetMetricsRequest { + consensus_metrics: true, + connection_metrics: true, + bandwidth_metrics: false, + process_metrics: false, + }) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_none()); assert!(get_metrics_call_response.consensus_metrics.is_some()); let get_metrics_call_response = rpc_client - .get_metrics_call(GetMetricsRequest { consensus_metrics: false, process_metrics: false }) + .get_metrics_call(GetMetricsRequest { + consensus_metrics: false, + connection_metrics: true, + bandwidth_metrics: false, + process_metrics: false, + }) .await .unwrap(); assert!(get_metrics_call_response.process_metrics.is_none()); diff --git a/wallet/bip32/Cargo.toml b/wallet/bip32/Cargo.toml index edc055b3c..43e40d379 100644 --- a/wallet/bip32/Cargo.toml +++ b/wallet/bip32/Cargo.toml @@ -10,6 +10,7 @@ include = ["src/**/*.rs", "Cargo.toml", "src/**/*.txt"] # include.workspace = true [dependencies] +borsh.workspace = true bs58.workspace = true getrandom = { workspace = true, features = ["js"] } hmac.workspace = true @@ -21,6 +22,7 @@ rand_core.workspace = true rand.workspace = true ripemd.workspace = true secp256k1.workspace = true +serde.workspace = true sha2.workspace = true subtle.workspace = true thiserror.workspace = true diff --git a/wallet/bip32/src/attrs.rs b/wallet/bip32/src/attrs.rs index e6c019489..7960a7d4a 100644 --- a/wallet/bip32/src/attrs.rs +++ b/wallet/bip32/src/attrs.rs @@ -1,8 +1,9 @@ use crate::{ChainCode, ChildNumber, Depth, KeyFingerprint}; +use borsh::{BorshDeserialize, BorshSerialize}; /// Extended key attributes: fields common to extended keys including depth, /// fingerprints, child numbers, and chain codes. -#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] pub struct ExtendedKeyAttrs { /// Depth in the key derivation hierarchy. pub depth: Depth, diff --git a/wallet/bip32/src/child_number.rs b/wallet/bip32/src/child_number.rs index a9d53f1b5..7be22fbf7 100644 --- a/wallet/bip32/src/child_number.rs +++ b/wallet/bip32/src/child_number.rs @@ -1,13 +1,14 @@ //! Child numbers use crate::{Error, Result}; +use borsh::{BorshDeserialize, BorshSerialize}; use core::{ fmt::{self, Display}, str::FromStr, }; /// Index of a particular child key for a given (extended) private key. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] pub struct ChildNumber(pub u32); impl ChildNumber { diff --git a/wallet/bip32/src/error.rs b/wallet/bip32/src/error.rs index 497309af4..2ef733bbc 100644 --- a/wallet/bip32/src/error.rs +++ b/wallet/bip32/src/error.rs @@ -92,6 +92,9 @@ pub enum Error { #[error(transparent)] WorkflowWasm(#[from] workflow_wasm::error::Error), + + #[error("Mnemonic word count is not supported ({0})")] + WordCount(usize), } impl From for Error { diff --git a/wallet/bip32/src/lib.rs b/wallet/bip32/src/lib.rs index fbc07d3fa..e883f56f0 100644 --- a/wallet/bip32/src/lib.rs +++ b/wallet/bip32/src/lib.rs @@ -24,7 +24,7 @@ pub use address_type::AddressType; pub use attrs::ExtendedKeyAttrs; pub use child_number::ChildNumber; pub use derivation_path::DerivationPath; -pub use mnemonic::{Language, Mnemonic}; +pub use mnemonic::{Language, Mnemonic, WordCount}; pub use prefix::Prefix; pub use private_key::PrivateKey; pub use public_key::PublicKey; diff --git a/wallet/bip32/src/mnemonic/mod.rs b/wallet/bip32/src/mnemonic/mod.rs index 6d28f591d..ef793b5a7 100644 --- a/wallet/bip32/src/mnemonic/mod.rs +++ b/wallet/bip32/src/mnemonic/mod.rs @@ -10,7 +10,7 @@ mod phrase; //#[cfg(feature = "bip39")] pub(crate) mod seed; -pub use self::{language::Language, phrase::Mnemonic}; +pub use self::{language::Language, phrase::Mnemonic, phrase::WordCount}; //#[cfg(feature = "bip39")] pub use self::seed::Seed; diff --git a/wallet/bip32/src/mnemonic/phrase.rs b/wallet/bip32/src/mnemonic/phrase.rs index 4bbab2c16..499b7972e 100644 --- a/wallet/bip32/src/mnemonic/phrase.rs +++ b/wallet/bip32/src/mnemonic/phrase.rs @@ -4,26 +4,43 @@ use super::{ bits::{BitWriter, IterExt}, language::Language, }; +use crate::Result; use crate::{Error, KEY_SIZE}; -//use alloc::{format, string::String}; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use kash_utils::hex::*; use rand_core::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use zeroize::{Zeroize, Zeroizing}; - -//#[cfg(feature = "bip39")] use wasm_bindgen::prelude::*; +use zeroize::{Zeroize, Zeroizing}; use {super::seed::Seed, hmac::Hmac, sha2::Sha512}; -use kash_utils::hex::*; -//use workflow_wasm::jsvalue::*; -use crate::Result; /// Number of PBKDF2 rounds to perform when deriving the seed -//#[cfg(feature = "bip39")] const PBKDF2_ROUNDS: u32 = 2048; /// Source entropy for a BIP39 mnemonic phrase -pub type Entropy = [u8; KEY_SIZE]; -//use std::convert::TryInto; +pub type Entropy32 = [u8; KEY_SIZE]; +pub type Entropy16 = [u8; 16]; + +#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] +#[serde(rename_all = "kebab-case")] +pub enum WordCount { + #[default] + Words12, + Words24, +} + +impl TryFrom for WordCount { + type Error = Error; + fn try_from(word_count: usize) -> Result { + match word_count { + 12 => Ok(WordCount::Words12), + 24 => Ok(WordCount::Words24), + _ => Err(Error::WordCount(word_count)), + } + } +} + /// BIP39 mnemonic phrases: sequences of words representing cryptographic keys. #[derive(Clone)] #[wasm_bindgen(inspectable)] @@ -42,11 +59,6 @@ pub struct Mnemonic { impl Mnemonic { #[wasm_bindgen(constructor)] pub fn constructor(phrase: String, language: Option) -> Result { - //let vec: Vec = entropy.try_as_vec_u8().unwrap_or_else(|err| panic!("invalid entropy {err}")); - //let entropy = as TryInto>::try_into(vec).unwrap_or_else(|vec| panic!("invalid mnemonic: {vec:?}")); - - //Mnemonic { language, entropy, phrase } - Mnemonic::new(phrase, language.unwrap_or(Language::English)) } @@ -66,8 +78,9 @@ impl Mnemonic { } #[wasm_bindgen(js_name = random)] - pub fn create_random() -> Result { - Mnemonic::random(rand::thread_rng(), Default::default()) + pub fn create_random_js(word_count: JsValue) -> Result { + let word_count = word_count.as_f64().unwrap_or(24.0) as usize; + Mnemonic::random(word_count.try_into()?, Default::default()) } #[wasm_bindgen(getter, js_name = phrase)] @@ -88,11 +101,24 @@ impl Mnemonic { } impl Mnemonic { + pub fn random(word_count: WordCount, language: Language) -> Result { + Mnemonic::random_impl(word_count, rand::thread_rng(), language) + } + /// Create a random BIP39 mnemonic phrase. - pub fn random(mut rng: impl RngCore + CryptoRng, language: Language) -> Result { - let mut entropy = Entropy::default(); - rng.fill_bytes(&mut entropy); - Self::from_entropy(entropy.to_vec(), language) + pub fn random_impl(word_count: WordCount, mut rng: impl RngCore + CryptoRng, language: Language) -> Result { + match word_count { + WordCount::Words24 => { + let mut entropy = Entropy32::default(); + rng.fill_bytes(&mut entropy); + Self::from_entropy(entropy.to_vec(), language) + } + WordCount::Words12 => { + let mut entropy = Entropy16::default(); + rng.fill_bytes(&mut entropy); + Self::from_entropy(entropy.to_vec(), language) + } + } } /// Create a new BIP39 mnemonic phrase from the given entropy diff --git a/wallet/bip32/src/prefix.rs b/wallet/bip32/src/prefix.rs index a4cfc21f0..989c75c1e 100644 --- a/wallet/bip32/src/prefix.rs +++ b/wallet/bip32/src/prefix.rs @@ -1,6 +1,7 @@ //! Extended key prefixes. use crate::{Error, ExtendedKey, Result, Version}; +use borsh::{BorshDeserialize, BorshSerialize}; use core::{ fmt::{self, Debug, Display}, str, @@ -11,7 +12,7 @@ use core::{ /// The BIP32 spec describes these as "versions" and gives examples for /// `xprv`/`xpub` (mainnet) and `tprv`/`tpub` (testnet), however in practice /// there are many more used (e.g. `ypub`, `zpub`). -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] #[non_exhaustive] pub struct Prefix { /// ASCII characters representing the prefix. diff --git a/wallet/bip32/src/xkey.rs b/wallet/bip32/src/xkey.rs index bee7ca8b5..c08caf906 100644 --- a/wallet/bip32/src/xkey.rs +++ b/wallet/bip32/src/xkey.rs @@ -121,7 +121,7 @@ mod tests { fn bip32_test_vector_1_xprv() { //let xprv_base58 = "kprv5y2qurMHCsXYrNfU3GCihuwG3vMqFji7PZXajMEqyBkNh9UZUJgoHYBLTKu1eM4MvUtomcXPQ3Sw9HZ5ebbM4byoUciHo1zrPJBQfqpLorQ"; let xprv_base58 = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPP\ - qjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; + qjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; let xprv = xprv_base58.parse::(); assert!(xprv.is_ok(), "Could not parse key"); diff --git a/wallet/bip32/src/xprivate_key.rs b/wallet/bip32/src/xprivate_key.rs index 97a5bce4c..eec196f56 100644 --- a/wallet/bip32/src/xprivate_key.rs +++ b/wallet/bip32/src/xprivate_key.rs @@ -14,9 +14,7 @@ const BIP39_DOMAIN_SEPARATOR: [u8; 12] = [0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x /// Extended private keys derived using BIP32. /// -/// Generic around a [`PrivateKey`] type. When the `secp256k1` feature of this -/// crate is enabled, the [`XPrv`] type provides a convenient alias for -/// extended ECDSA/secp256k1 private keys. +/// Generic around a [`PrivateKey`] type. #[derive(Clone)] pub struct ExtendedPrivateKey { /// Derived private key diff --git a/wallet/bip32/src/xpublic_key.rs b/wallet/bip32/src/xpublic_key.rs index 21ca98eff..c971f6a32 100644 --- a/wallet/bip32/src/xpublic_key.rs +++ b/wallet/bip32/src/xpublic_key.rs @@ -4,8 +4,11 @@ use crate::{ types::*, ChildNumber, DerivationPath, Error, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, KeyFingerprint, Prefix, PrivateKey, PublicKey, PublicKeyBytes, Result, KEY_SIZE, }; +use borsh::{BorshDeserialize, BorshSerialize}; use core::str::FromStr; use hmac::Mac; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; /// Extended public secp256k1 ECDSA verification key. //#[cfg(feature = "secp256k1")] @@ -14,9 +17,7 @@ use hmac::Mac; /// Extended public keys derived using BIP32. /// -/// Generic around a [`PublicKey`] type. When the `secp256k1` feature of this -/// crate is enabled, the [`XPub`] type provides a convenient alias for -/// extended ECDSA/secp256k1 public keys. +/// Generic around a [`PublicKey`] type. #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub struct ExtendedPublicKey { /// Derived public key @@ -133,3 +134,111 @@ where } } } + +impl fmt::Display for ExtendedPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.to_string(None).fmt(f) + } +} + +impl ExtendedPublicKey +where + K: PublicKey, +{ + // a unique number used for binary + // serialization data alignment check + const STORAGE_MAGIC: u16 = 0x4b58; + // binary serialization version + const STORAGE_VERSION: u16 = 0; +} + +#[derive(BorshSerialize, BorshDeserialize)] +struct Header { + magic: u16, + version: u16, +} + +impl BorshSerialize for ExtendedPublicKey +where + K: PublicKey, +{ + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + Header { version: Self::STORAGE_VERSION, magic: Self::STORAGE_MAGIC }.serialize(writer)?; + writer.write_all(self.public_key.to_bytes().as_slice())?; + BorshSerialize::serialize(&self.attrs, writer)?; + Ok(()) + } +} + +impl BorshDeserialize for ExtendedPublicKey +where + K: PublicKey, +{ + fn deserialize(buf: &mut &[u8]) -> std::io::Result { + let Header { version, magic } = Header::deserialize(buf)?; + if magic != Self::STORAGE_MAGIC { + return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key magic value")); + } + if version != Self::STORAGE_VERSION { + return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key version")); + } + + let public_key_bytes: [u8; KEY_SIZE + 1] = buf[..KEY_SIZE + 1] + .try_into() + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key"))?; + let public_key = K::from_bytes(public_key_bytes) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Invalid extended public key"))?; + *buf = &buf[KEY_SIZE + 1..]; + let attrs = ExtendedKeyAttrs::deserialize(buf)?; + Ok(Self { public_key, attrs }) + } +} + +impl Serialize for ExtendedPublicKey +where + K: Serialize + PublicKey, +{ + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string(None)) + } +} + +struct ExtendedPublicKeyVisitor<'de, K> +where + K: Deserialize<'de> + PublicKey, +{ + phantom: std::marker::PhantomData<&'de K>, +} + +impl<'de, K> de::Visitor<'de> for ExtendedPublicKeyVisitor<'de, K> +where + K: Deserialize<'de> + PublicKey, +{ + type Value = ExtendedPublicKey; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string containing network_type and optional suffix separated by a '-'") + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: de::Error, + { + ExtendedPublicKey::::from_str(value).map_err(|err| de::Error::custom(err.to_string())) + } +} + +impl<'de, K> Deserialize<'de> for ExtendedPublicKey +where + K: Deserialize<'de> + PublicKey + 'de, +{ + fn deserialize(deserializer: D) -> std::result::Result, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(ExtendedPublicKeyVisitor::<'de, K> { phantom: std::marker::PhantomData::<&'de K> }) + } +} diff --git a/wallet/core/Cargo.toml b/wallet/core/Cargo.toml index a249a1ed1..3b4b6da05 100644 --- a/wallet/core/Cargo.toml +++ b/wallet/core/Cargo.toml @@ -18,7 +18,9 @@ crate-type = ["cdylib", "lib"] [dependencies] aes.workspace = true +ahash.workspace = true argon2.workspace = true +async-channel.workspace = true async-std.workspace = true async-trait.workspace = true base64.workspace = true @@ -31,6 +33,7 @@ derivative.workspace = true downcast.workspace = true evpkdf.workspace = true faster-hex.workspace = true +fixedstr.workspace = true futures.workspace = true heapless.workspace = true hmac.workspace = true @@ -47,6 +50,7 @@ kash-rpc-core.workspace = true kash-txscript-errors.workspace = true kash-txscript.workspace = true kash-utils.workspace = true +kash-wallet-macros.workspace = true kash-wrpc-client.workspace = true md-5.workspace = true pad.workspace = true diff --git a/wallet/core/src/account/descriptor.rs b/wallet/core/src/account/descriptor.rs new file mode 100644 index 000000000..ea6ce22f9 --- /dev/null +++ b/wallet/core/src/account/descriptor.rs @@ -0,0 +1,181 @@ +//! +//! Account descriptors (client-side account information representation). +//! + +use crate::derivation::AddressDerivationMeta; +use crate::imports::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use kash_addresses::Address; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct AccountDescriptor { + pub kind: AccountKind, + pub account_id: AccountId, + pub account_name: Option, + pub prv_key_data_ids: AssocPrvKeyDataIds, + pub receive_address: Option
, + pub change_address: Option
, + + pub properties: BTreeMap, +} + +impl AccountDescriptor { + pub fn new( + kind: AccountKind, + account_id: AccountId, + account_name: Option, + prv_key_data_ids: AssocPrvKeyDataIds, + receive_address: Option
, + change_address: Option
, + ) -> Self { + Self { kind, account_id, account_name, prv_key_data_ids, receive_address, change_address, properties: BTreeMap::default() } + } + + pub fn with_property(mut self, property: AccountDescriptorProperty, value: AccountDescriptorValue) -> Self { + self.properties.insert(property, value); + self + } +} + +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] + +pub enum AccountDescriptorProperty { + AccountIndex, + XpubKeys, + Ecdsa, + DerivationMeta, + Other(String), +} + +impl std::fmt::Display for AccountDescriptorProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AccountDescriptorProperty::AccountIndex => write!(f, "Account Index"), + AccountDescriptorProperty::XpubKeys => write!(f, "Xpub Keys"), + AccountDescriptorProperty::Ecdsa => write!(f, "ECDSA"), + AccountDescriptorProperty::DerivationMeta => write!(f, "Derivation Indexes"), + AccountDescriptorProperty::Other(other) => write!(f, "{}", other), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(tag = "type", content = "value")] +#[serde(rename_all = "kebab-case")] +pub enum AccountDescriptorValue { + U64(u64), + String(String), + Bool(bool), + AddressDerivationMeta(AddressDerivationMeta), + XPubKeys(ExtendedPublicKeys), + Json(String), +} + +impl std::fmt::Display for AccountDescriptorValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AccountDescriptorValue::U64(value) => write!(f, "{}", value), + AccountDescriptorValue::String(value) => write!(f, "{}", value), + AccountDescriptorValue::Bool(value) => write!(f, "{}", value), + AccountDescriptorValue::AddressDerivationMeta(value) => write!(f, "{}", value), + AccountDescriptorValue::XPubKeys(value) => { + let mut s = String::new(); + for xpub in value.iter() { + s.push_str(&format!("{}\n", xpub)); + } + write!(f, "{}", s) + } + AccountDescriptorValue::Json(value) => write!(f, "{}", value), + } + } +} + +impl From for AccountDescriptorValue { + fn from(value: u64) -> Self { + Self::U64(value) + } +} + +impl From for AccountDescriptorValue { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From for AccountDescriptorValue { + fn from(value: bool) -> Self { + Self::Bool(value) + } +} + +impl From for AccountDescriptorValue { + fn from(value: AddressDerivationMeta) -> Self { + Self::AddressDerivationMeta(value) + } +} + +impl From<&str> for AccountDescriptorValue { + fn from(value: &str) -> Self { + Self::String(value.to_string()) + } +} + +impl From for AccountDescriptorValue { + fn from(value: ExtendedPublicKeys) -> Self { + Self::XPubKeys(value) + } +} + +impl From for AccountDescriptorValue { + fn from(value: serde_json::Value) -> Self { + Self::Json(value.to_string()) + } +} + +impl AccountDescriptor { + pub fn name(&self) -> &Option { + &self.account_name + } + + pub fn prv_key_data_ids(&self) -> &AssocPrvKeyDataIds { + &self.prv_key_data_ids + } + + pub fn account_id(&self) -> &AccountId { + &self.account_id + } + + pub fn name_or_id(&self) -> String { + if let Some(name) = self.name() { + if name.is_empty() { + self.account_id().short() + } else { + name.clone() + } + } else { + self.account_id().short() + } + } + + pub fn name_with_id(&self) -> String { + if let Some(name) = self.name() { + if name.is_empty() { + self.account_id().short() + } else { + format!("{name} {}", self.account_id().short()) + } + } else { + self.account_id().short() + } + } + + pub fn account_kind(&self) -> &AccountKind { + &self.kind + } + + pub fn receive_address(&self) -> &Option
{ + &self.receive_address + } +} diff --git a/wallet/core/src/account/kind.rs b/wallet/core/src/account/kind.rs new file mode 100644 index 000000000..768f26e7f --- /dev/null +++ b/wallet/core/src/account/kind.rs @@ -0,0 +1,115 @@ +//! +//! [`AccountKind`] is a unique type identifier of an [`Account`]. +//! + +use crate::imports::*; +use fixedstr::*; +use std::hash::Hash; +use std::str::FromStr; + +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)] +#[wasm_bindgen] +pub struct AccountKind(str64); + +impl AccountKind { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl AsRef for AccountKind { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl std::fmt::Display for AccountKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<&str> for AccountKind { + fn from(kind: &str) -> Self { + Self(kind.into()) + } +} + +impl PartialEq<&str> for AccountKind { + fn eq(&self, other: &&str) -> bool { + self.0.as_str() == *other + } +} + +impl FromStr for AccountKind { + type Err = Error; + fn from_str(s: &str) -> Result { + if factories().contains_key(&s.into()) { + Ok(s.into()) + } else { + match s.to_lowercase().as_str() { + "legacy" => Ok(LEGACY_ACCOUNT_KIND.into()), + "bip32" => Ok(BIP32_ACCOUNT_KIND.into()), + "multisig" => Ok(MULTISIG_ACCOUNT_KIND.into()), + "keypair" => Ok(KEYPAIR_ACCOUNT_KIND.into()), + _ => Err(Error::InvalidAccountKind), + } + } + } +} + +impl TryFrom for AccountKind { + type Error = Error; + fn try_from(kind: JsValue) -> Result { + if let Some(kind) = kind.as_string() { + Ok(AccountKind::from_str(kind.as_str())?) + } else { + Err(Error::InvalidAccountKind) + } + } +} + +impl BorshSerialize for AccountKind { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + let len = self.0.len() as u8; + writer.write_all(&[len])?; + writer.write_all(self.0.as_bytes())?; + Ok(()) + } +} + +impl BorshDeserialize for AccountKind { + fn deserialize(buf: &mut &[u8]) -> IoResult { + if buf.is_empty() { + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid AccountKind length")) + } else { + let len = buf[0]; + if buf.len() < (len as usize + 1) { + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid AccountKind length")) + } else { + let s = str64::make( + std::str::from_utf8(&buf[1..(len as usize + 1)]) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid UTF-8 sequence"))?, + ); + *buf = &buf[(len as usize + 1)..]; + Ok(Self(s)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_account_kind() -> Result<()> { + let storable_in = AccountKind::from("hello world"); + let guard = StorageGuard::new(&storable_in); + let storable_out = guard.validate()?; + assert_eq!(storable_in, storable_out); + + Ok(()) + } +} diff --git a/wallet/core/src/runtime/account/mod.rs b/wallet/core/src/account/mod.rs similarity index 81% rename from wallet/core/src/runtime/account/mod.rs rename to wallet/core/src/account/mod.rs index b2b657e4a..fde26edea 100644 --- a/wallet/core/src/runtime/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -1,80 +1,77 @@ -pub mod id; -pub mod kind; -pub mod variants; +//! +//! Generic wallet [`Account`] trait implementation used +//! by different types of accounts. +//! pub use id::*; -use kash_bip32::ExtendedPrivateKey; -use kash_bip32::{ChildNumber, PrivateKeyBytes}; +pub mod descriptor; +pub mod kind; +pub mod variants; pub use kind::*; pub use variants::*; use crate::derivation::build_derivate_paths; use crate::derivation::gen0; use crate::derivation::AddressDerivationManagerTrait; -#[allow(unused_imports)] -use crate::derivation::{gen0::*, gen1::*, AddressDerivationMeta, PubkeyDerivationManagerTrait, WalletDerivationManagerTrait}; use crate::imports::*; -use crate::result::Result; -use crate::runtime::{Balance, BalanceStrings, Wallet}; -use crate::secret::Secret; -use crate::storage::interface::AccessContext; -use crate::storage::Metadata; -use crate::storage::{self, AccessContextT, AccountData, PrvKeyData, PrvKeyDataId}; +use crate::storage::account::AccountSettings; +use crate::storage::AccountMetadata; +use crate::storage::{PrvKeyData, PrvKeyDataId}; +use crate::tx::PaymentOutput; use crate::tx::{Fees, Generator, GeneratorSettings, GeneratorSummary, PaymentDestination, PendingTransaction, Signer}; -use crate::utxo::{UtxoContext, UtxoContextBinding}; -use kash_bip32::PrivateKey; +use crate::utxo::balance::{AtomicBalance, BalanceStrings}; +use crate::utxo::UtxoContextBinding; +use kash_bip32::{ChildNumber, ExtendedPrivateKey, PrivateKey, PrivateKeyBytes}; use kash_consensus_core::asset_type::AssetType; use kash_consensus_core::tx::TransactionAction; use kash_consensus_wasm::UtxoEntryReference; -use kash_notify::listener::ListenerId; -use separator::Separatable; use workflow_core::abortable::Abortable; -use super::AtomicBalance; - pub const DEFAULT_AMOUNT_PADDING: usize = 19; pub type GenerationNotifier = Arc; -pub type ScanNotifier = Arc) + Send + Sync>; +pub type ScanNotifier = Arc) + Send + Sync>; pub struct Context { - pub settings: Option, - pub listener_id: Option, + pub settings: AccountSettings, +} + +impl Context { + pub fn new(settings: AccountSettings) -> Self { + Self { settings } + } + + pub fn settings(&self) -> &AccountSettings { + &self.settings + } } pub struct Inner { context: Mutex, id: AccountId, + storage_key: AccountStorageKey, wallet: Arc, utxo_context: UtxoContext, } impl Inner { - pub fn new(wallet: &Arc, id: AccountId, settings: Option) -> Self { + pub fn new(wallet: &Arc, id: AccountId, storage_key: AccountStorageKey, settings: AccountSettings) -> Self { let utxo_context = UtxoContext::new(wallet.utxo_processor(), UtxoContextBinding::AccountId(id)); - let context = Context { listener_id: None, settings }; - Inner { context: Mutex::new(context), id, wallet: wallet.clone(), utxo_context: utxo_context.clone() } + let context = Context { settings }; + Inner { context: Mutex::new(context), id, storage_key, wallet: wallet.clone(), utxo_context: utxo_context.clone() } } -} -pub async fn try_from_storage( - wallet: &Arc, - stored_account: Arc, - meta: Option>, -) -> Result> { - let storage::Account { prv_key_data_id, data, settings, .. } = (*stored_account).clone(); - - match data { - AccountData::Bip32(bip32) => Ok(Arc::new(Bip32::try_new(wallet, prv_key_data_id.unwrap(), settings, bip32, meta).await?)), - AccountData::Legacy(legacy) => Ok(Arc::new(Legacy::try_new(wallet, prv_key_data_id.unwrap(), settings, legacy, meta).await?)), - AccountData::MultiSig(multisig) => Ok(Arc::new(MultiSig::try_new(wallet, settings, multisig, meta).await?)), - AccountData::Keypair(keypair) => { - Ok(Arc::new(Keypair::try_new(wallet, prv_key_data_id.unwrap(), settings, keypair, meta).await?)) - } - AccountData::Hardware(_hardware) => { - todo!() - } + pub fn from_storage(wallet: &Arc, storage: &AccountStorage) -> Self { + Self::new(wallet, storage.id, storage.storage_key, storage.settings.clone()) + } + + pub fn context(&self) -> MutexGuard { + self.context.lock().unwrap() + } + + pub fn store(&self) -> &Arc { + self.wallet.store() } } @@ -90,6 +87,10 @@ pub trait Account: AnySync + Send + Sync + 'static { &self.inner().id } + fn storage_key(&self) -> &AccountStorageKey { + &self.inner().storage_key + } + fn account_kind(&self) -> AccountKind; fn wallet(&self) -> &Arc { @@ -109,11 +110,7 @@ pub trait Account: AnySync + Send + Sync + 'static { } fn name(&self) -> Option { - self.context().settings.as_ref().and_then(|settings| settings.name.clone()) - } - - fn title(&self) -> Option { - self.context().settings.as_ref().and_then(|settings| settings.title.clone()) + self.context().settings.name.clone() } fn name_or_id(&self) -> String { @@ -140,21 +137,16 @@ pub trait Account: AnySync + Send + Sync + 'static { } } - async fn rename(&self, secret: Secret, name: Option<&str>) -> Result<()> { + async fn rename(&self, wallet_secret: &Secret, name: Option<&str>) -> Result<()> { { let mut context = self.context(); - if let Some(settings) = &mut context.settings { - settings.name = name.map(String::from); - } else { - context.settings = Some(storage::Settings { name: name.map(String::from), title: None, ..Default::default() }); - } + context.settings.name = name.map(String::from); } - let account = self.as_storable()?; + let account = self.to_storage()?; self.wallet().store().as_account_store()?.store_single(&account, None).await?; - let ctx: Arc = Arc::new(AccessContext::new(secret)); - self.wallet().store().commit(&ctx).await?; + self.wallet().store().commit(wallet_secret).await?; Ok(()) } @@ -179,26 +171,26 @@ pub trait Account: AnySync + Send + Sync + 'static { } fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { + // TODO - change to AssocPrvKeyDataIds Err(Error::ResidentAccount) - // panic!("account type does not have a private key in storage") } async fn prv_key_data(&self, wallet_secret: Secret) -> Result { let prv_key_data_id = self.prv_key_data_id()?; - let access_ctx: Arc = Arc::new(AccessContext::new(wallet_secret)); let keydata = self .wallet() .store() .as_prv_key_data_store()? - .load_key_data(&access_ctx, prv_key_data_id) + .load_key_data(&wallet_secret, prv_key_data_id) .await? - .ok_or(Error::PrivateKeyNotFound(prv_key_data_id.to_hex()))?; + .ok_or(Error::PrivateKeyNotFound(*prv_key_data_id))?; Ok(keydata) } - fn as_storable(&self) -> Result; - fn metadata(&self) -> Result>; + fn to_storage(&self) -> Result; + fn metadata(&self) -> Result>; + fn descriptor(&self) -> Result; async fn scan(self: Arc, window_size: Option, extent: Option) -> Result<()> { self.utxo_context().clear().await?; @@ -257,13 +249,9 @@ pub trait Account: AnySync + Send + Sync + 'static { Ok(()) } - fn sig_op_count(&self) -> u8 { - 1 - } + fn sig_op_count(&self) -> u8; - fn minimum_signatures(&self) -> u16 { - 1 - } + fn minimum_signatures(&self) -> u16; fn receive_address(&self) -> Result
; @@ -299,6 +287,8 @@ pub trait Account: AnySync + Send + Sync + 'static { fn as_dyn_arc(self: Arc) -> Arc; + /// Aggregate all account UTXOs into the change address. + /// Also known as "compounding". async fn sweep( self: Arc, wallet_secret: Secret, @@ -335,14 +325,12 @@ pub trait Account: AnySync + Send + Sync + 'static { let mut ids = Vec::new(); while let Some(transaction) = stream.try_next().await? { + transaction.try_sign()?; + ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); if let Some(notifier) = notifier.as_ref() { notifier(&transaction); } - transaction.try_sign()?; - transaction.log().await?; - let id = transaction.try_submit(&self.wallet().rpc_api()).await?; - ids.push(id); yield_executor().await; } @@ -353,6 +341,8 @@ pub trait Account: AnySync + Send + Sync + 'static { Ok((all_summaries, all_ids)) } + /// Send funds to a [`PaymentDestination`] comprised of one or multiple [`PaymentOutputs`](crate::tx::PaymentOutputs) + /// or [`PaymentDestination::Change`] variant that will forward funds to the change address. async fn send( self: Arc, asset_type: AssetType, @@ -381,14 +371,70 @@ pub trait Account: AnySync + Send + Sync + 'static { let mut stream = generator.stream(); let mut ids = vec![]; while let Some(transaction) = stream.try_next().await? { + transaction.try_sign()?; + ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + if let Some(notifier) = notifier.as_ref() { notifier(&transaction); } + yield_executor().await; + } + + Ok((generator.summary(), ids)) + } + + /// Execute a transfer to another wallet account. + async fn transfer( + self: Arc, + asset_type: AssetType, + destination_account_id: AccountId, + transfer_amount_sompi: u64, + priority_fee_sompi: Fees, + wallet_secret: Secret, + payment_secret: Option, + abortable: &Abortable, + notifier: Option, + ) -> Result<(GeneratorSummary, Vec)> { + let keydata = self.prv_key_data(wallet_secret).await?; + let signer = Arc::new(Signer::new(self.clone().as_dyn_arc(), keydata, payment_secret)); + + let destination_account = self + .wallet() + .get_account_by_id(&destination_account_id) + .await? + .ok_or_else(|| Error::AccountNotFound(destination_account_id))?; + + let destination_address = destination_account.receive_address()?; + let final_transaction_destination = + PaymentDestination::from(PaymentOutput::new(destination_address, transfer_amount_sompi, asset_type)); + let final_transaction_payload = None; + + let tx_kind = match asset_type { + AssetType::KSH => TransactionAction::TransferKSH, + AssetType::KUSD => TransactionAction::TransferKUSD, + AssetType::KRV => TransactionAction::TransferKRV, + }; + let settings = GeneratorSettings::try_new_with_account( + self.clone().as_dyn_arc(), + tx_kind, + final_transaction_destination, + priority_fee_sompi, + final_transaction_payload, + )? + .utxo_context_transfer(destination_account.utxo_context()); + + let generator = Generator::try_new(settings, Some(signer), Some(abortable))?; + + let mut stream = generator.stream(); + let mut ids = vec![]; + while let Some(transaction) = stream.try_next().await? { transaction.try_sign()?; - transaction.log().await?; - let id = transaction.try_submit(&self.wallet().rpc_api()).await?; - ids.push(id); + ids.push(transaction.try_submit(&self.wallet().rpc_api()).await?); + + if let Some(notifier) = notifier.as_ref() { + notifier(&transaction); + } yield_executor().await; } @@ -410,7 +456,6 @@ pub trait Account: AnySync + Send + Sync + 'static { let mut stream = generator.stream(); while let Some(_transaction) = stream.try_next().await? { - _transaction.log().await?; yield_executor().await; } @@ -421,29 +466,30 @@ pub trait Account: AnySync + Send + Sync + 'static { Err(Error::AccountAddressDerivationCaps) } - async fn initialize_private_data( - self: Arc, - _secret: Secret, - _payment_secret: Option<&Secret>, - _index: Option, - ) -> Result<()> { - Ok(()) - } - - async fn clear_private_data(self: Arc) -> Result<()> { - Ok(()) + fn as_legacy_account(self: Arc) -> Result> { + Err(Error::InvalidAccountKind) } } downcast_sync!(dyn Account); +#[async_trait] +pub trait AsLegacyAccount: Account { + async fn create_private_context( + &self, + _wallet_secret: &Secret, + _payment_secret: Option<&Secret>, + _index: Option, + ) -> Result<()>; + + async fn clear_private_context(&self) -> Result<()>; +} + #[async_trait] pub trait DerivationCapableAccount: Account { fn derivation(&self) -> Arc; - fn account_index(&self) -> u64 { - 0 - } + fn account_index(&self) -> u64; async fn derivation_scan( self: Arc, @@ -456,7 +502,9 @@ pub trait DerivationCapableAccount: Account { abortable: &Abortable, notifier: Option, ) -> Result<()> { - self.clone().initialize_private_data(wallet_secret.clone(), payment_secret.as_ref(), None).await?; + if let Ok(legacy_account) = self.clone().as_legacy_account() { + legacy_account.create_private_context(&wallet_secret, payment_secret.as_ref(), None).await?; + } let derivation = self.derivation(); @@ -477,10 +525,11 @@ pub trait DerivationCapableAccount: Account { let mut index: usize = start; let mut last_notification = 0; let mut aggregate_balance = 0; + let mut aggregate_utxo_count = 0; let change_address = change_address_keypair[0].0.clone(); - while index < extent { + while index < extent && !abortable.is_aborted() { let first = index as u32; let last = (index + window) as u32; index = last as usize; @@ -508,6 +557,8 @@ pub trait DerivationCapableAccount: Account { let utxos = rpc.get_utxos_by_addresses(addresses.clone()).await?; let balance = utxos.iter().map(|utxo| utxo.utxo_entry.amount).sum::(); + aggregate_utxo_count += utxos.len(); + if balance > 0 { aggregate_balance += balance; @@ -531,12 +582,13 @@ pub trait DerivationCapableAccount: Account { notifier, index, balance, + aggregate_utxo_count, ) .await?; } } else { if let Some(notifier) = notifier { - notifier(index, aggregate_balance, None); + notifier(index, aggregate_utxo_count, aggregate_balance, None); } yield_executor().await; } @@ -545,7 +597,7 @@ pub trait DerivationCapableAccount: Account { if index > last_notification + 1_000 { last_notification = index; if let Some(notifier) = notifier { - notifier(index, aggregate_balance, None); + notifier(index, aggregate_utxo_count, aggregate_balance, None); } yield_executor().await; } @@ -553,11 +605,13 @@ pub trait DerivationCapableAccount: Account { if index > last_notification { if let Some(notifier) = notifier { - notifier(index, aggregate_balance, None); + notifier(index, aggregate_utxo_count, aggregate_balance, None); } } - self.clone().clear_private_data().await?; + if let Ok(legacy_account) = self.as_legacy_account() { + legacy_account.clear_private_context().await?; + } Ok(()) } @@ -568,7 +622,9 @@ pub trait DerivationCapableAccount: Account { let metadata = self.metadata()?.expect("derivation accounts must provide metadata"); let store = self.wallet().store().as_account_store()?; - store.update_metadata(&[&metadata]).await?; + store.update_metadata(vec![metadata]).await?; + + self.wallet().notify(Events::AccountUpdate { account_descriptor: self.descriptor()? }).await?; Ok(address) } @@ -579,7 +635,9 @@ pub trait DerivationCapableAccount: Account { let metadata = self.metadata()?.expect("derivation accounts must provide metadata"); let store = self.wallet().store().as_account_store()?; - store.update_metadata(&[&metadata]).await?; + store.update_metadata(vec![metadata]).await?; + + self.wallet().notify(Events::AccountUpdate { account_descriptor: self.descriptor()? }).await?; Ok(address) } @@ -597,14 +655,14 @@ pub trait DerivationCapableAccount: Account { ) -> Result> { let payload = key_data.payload.decrypt(payment_secret.as_ref())?; let xkey = payload.get_xprv(payment_secret.as_ref())?; - create_private_keys(self.account_kind(), self.cosigner_index(), self.account_index(), &xkey, receive, change) + create_private_keys(&self.account_kind(), self.cosigner_index(), self.account_index(), &xkey, receive, change) } } downcast_sync!(dyn DerivationCapableAccount); pub fn create_private_keys<'l>( - account_kind: AccountKind, + account_kind: &AccountKind, cosigner_index: u32, account_index: u64, xkey: &ExtendedPrivateKey, @@ -613,7 +671,7 @@ pub fn create_private_keys<'l>( ) -> Result> { let paths = build_derivate_paths(account_kind, account_index, cosigner_index)?; let mut private_keys = vec![]; - if matches!(account_kind, AccountKind::Legacy) { + if matches!(account_kind.as_ref(), LEGACY_ACCOUNT_KIND) { let (private_key, attrs) = gen0::WalletDerivationManagerV0::derive_key_by_path(xkey, paths.0)?; for (address, index) in receive.iter() { let (private_key, _) = @@ -667,6 +725,7 @@ async fn process_asset_utxos( notifier: Option<&ScanNotifier>, index: usize, balance: u64, + aggregate_utxo_count: usize, ) -> Result<()> { // Verify that all UTXOs are of the correct asset type if let Some(mismatched_utxo_ref) = utxos.iter().find(|utxo_ref| utxo_ref.utxo.entry.asset_type != asset_type) { @@ -700,7 +759,7 @@ async fn process_asset_utxos( transaction.try_sign_with_keys(keys.to_vec())?; let id = transaction.try_submit(rpc).await?; if let Some(notifier) = notifier { - notifier(index, balance, Some(id)); + notifier(index, aggregate_utxo_count, balance, Some(id)); } yield_executor().await; } @@ -712,8 +771,9 @@ async fn process_asset_utxos( #[cfg(test)] mod tests { use super::create_private_keys; - use super::{AccountKind, ExtendedPrivateKey}; - use crate::runtime::account::PubkeyDerivationManagerV0; + use super::ExtendedPrivateKey; + use crate::derivation::gen0::PubkeyDerivationManagerV0; + use crate::imports::LEGACY_ACCOUNT_KIND; use kash_addresses::Address; use kash_addresses::Prefix; use kash_bip32::secp256k1::SecretKey; @@ -850,14 +910,14 @@ mod tests { let receive_keys = gen0_receive_keys(); let change_keys = gen0_change_keys(); - let keys = create_private_keys(AccountKind::Legacy, 0, 0, &xkey, &receive_addresses, &[]).unwrap(); + let keys = create_private_keys(&LEGACY_ACCOUNT_KIND.into(), 0, 0, &xkey, &receive_addresses, &[]).unwrap(); for (index, (a, key)) in keys.iter().enumerate() { let address = PubkeyDerivationManagerV0::create_address(&key.get_public_key(), Prefix::Testnet, false).unwrap(); assert_eq!(*a, &address, "receive address at {index} failed"); assert_eq!(bytes_str(&key.to_bytes()), receive_keys[index], "receive key at {index} failed"); } - let keys = create_private_keys(AccountKind::Legacy, 0, 0, &xkey, &[], &change_addresses).unwrap(); + let keys = create_private_keys(&LEGACY_ACCOUNT_KIND.into(), 0, 0, &xkey, &[], &change_addresses).unwrap(); for (index, (a, key)) in keys.iter().enumerate() { let address = PubkeyDerivationManagerV0::create_address(&key.get_public_key(), Prefix::Testnet, false).unwrap(); assert_eq!(*a, &address, "change address at {index} failed"); diff --git a/wallet/core/src/account/variants/bip32.rs b/wallet/core/src/account/variants/bip32.rs new file mode 100644 index 000000000..3c64a14b5 --- /dev/null +++ b/wallet/core/src/account/variants/bip32.rs @@ -0,0 +1,259 @@ +//! +//! BIP32 & BIP44 account implementation +//! + +use crate::account::Inner; +use crate::derivation::{AddressDerivationManager, AddressDerivationManagerTrait}; +use crate::imports::*; + +pub const BIP32_ACCOUNT_KIND: &str = "kash-bip32-standard"; + +pub struct Ctor {} + +#[async_trait] +impl Factory for Ctor { + fn name(&self) -> String { + "bip32/bip44".to_string() + } + + fn description(&self) -> String { + "Kash Core HD Wallet Account".to_string() + } + + async fn try_load( + &self, + wallet: &Arc, + storage: &AccountStorage, + meta: Option>, + ) -> Result> { + Ok(Arc::new(bip32::Bip32::try_load(wallet, storage, meta).await?)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub struct Payload { + pub xpub_keys: Arc>, + pub account_index: u64, + pub ecdsa: bool, +} + +impl Payload { + pub fn new(account_index: u64, xpub_keys: Arc>, ecdsa: bool) -> Self { + Self { account_index, xpub_keys, ecdsa } + } + + pub fn try_load(storage: &AccountStorage) -> Result { + Ok(Self::try_from_slice(storage.serialized.as_slice())?) + } +} + +impl Storable for Payload { + // a unique number used for binary + // serialization data alignment check + const STORAGE_MAGIC: u32 = 0x32335042; + // binary serialization version + const STORAGE_VERSION: u32 = 0; +} + +impl AccountStorable for Payload {} + +impl BorshSerialize for Payload { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.xpub_keys, writer)?; + BorshSerialize::serialize(&self.account_index, writer)?; + BorshSerialize::serialize(&self.ecdsa, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for Payload { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let xpub_keys = BorshDeserialize::deserialize(buf)?; + let account_index = BorshDeserialize::deserialize(buf)?; + let ecdsa = BorshDeserialize::deserialize(buf)?; + + Ok(Self { xpub_keys, account_index, ecdsa }) + } +} + +pub struct Bip32 { + inner: Arc, + prv_key_data_id: PrvKeyDataId, + account_index: u64, + xpub_keys: ExtendedPublicKeys, + ecdsa: bool, + derivation: Arc, +} + +impl Bip32 { + pub async fn try_new( + wallet: &Arc, + name: Option, + prv_key_data_id: PrvKeyDataId, + account_index: u64, + xpub_keys: ExtendedPublicKeys, + ecdsa: bool, + ) -> Result { + let storable = Payload::new(account_index, xpub_keys.clone(), ecdsa); + let settings = AccountSettings { name, ..Default::default() }; + let (id, storage_key) = make_account_hashes(from_bip32(&prv_key_data_id, &storable)); + let inner = Arc::new(Inner::new(wallet, id, storage_key, settings)); + + let derivation = + AddressDerivationManager::new(wallet, BIP32_ACCOUNT_KIND.into(), &xpub_keys, ecdsa, 0, None, 1, Default::default()) + .await?; + + Ok(Self { inner, prv_key_data_id, account_index, xpub_keys, ecdsa, derivation }) + } + + pub async fn try_load(wallet: &Arc, storage: &AccountStorage, meta: Option>) -> Result { + let storable = Payload::try_load(storage)?; + let prv_key_data_id: PrvKeyDataId = storage.prv_key_data_ids.clone().try_into()?; + let inner = Arc::new(Inner::from_storage(wallet, storage)); + + let Payload { account_index, xpub_keys, ecdsa, .. } = storable; + + let address_derivation_indexes = meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or_default(); + + let derivation = AddressDerivationManager::new( + wallet, + BIP32_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + None, + 1, + address_derivation_indexes, + ) + .await?; + + // TODO - is this needed? + let _prv_key_data_info = wallet + .store() + .as_prv_key_data_store()? + .load_key_info(&prv_key_data_id) + .await? + .ok_or_else(|| Error::PrivateKeyNotFound(prv_key_data_id))?; + + Ok(Self { inner, prv_key_data_id, account_index, xpub_keys, ecdsa, derivation }) + } + + pub fn get_address_range_for_scan(&self, range: std::ops::Range) -> Result> { + let receive_addresses = self.derivation.receive_address_manager().get_range_with_args(range.clone(), false)?; + let change_addresses = self.derivation.change_address_manager().get_range_with_args(range, false)?; + Ok(receive_addresses.into_iter().chain(change_addresses).collect::>()) + } +} + +#[async_trait] +impl Account for Bip32 { + fn inner(&self) -> &Arc { + &self.inner + } + + fn account_kind(&self) -> AccountKind { + BIP32_ACCOUNT_KIND.into() + } + + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { + Ok(&self.prv_key_data_id) + } + + fn as_dyn_arc(self: Arc) -> Arc { + self + } + + fn sig_op_count(&self) -> u8 { + 1 + } + + fn minimum_signatures(&self) -> u16 { + 1 + } + + fn receive_address(&self) -> Result
{ + self.derivation.receive_address_manager().current_address() + } + fn change_address(&self) -> Result
{ + self.derivation.change_address_manager().current_address() + } + + fn to_storage(&self) -> Result { + let settings = self.context().settings.clone(); + let storable = Payload::new(self.account_index, self.xpub_keys.clone(), self.ecdsa); + let storage = AccountStorage::try_new( + BIP32_ACCOUNT_KIND.into(), + self.id(), + self.storage_key(), + self.prv_key_data_id.into(), + settings, + storable, + )?; + + Ok(storage) + } + + fn metadata(&self) -> Result> { + let metadata = AccountMetadata::new(self.inner.id, self.derivation.address_derivation_meta()); + Ok(Some(metadata)) + } + + fn descriptor(&self) -> Result { + let descriptor = AccountDescriptor::new( + BIP32_ACCOUNT_KIND.into(), + *self.id(), + self.name(), + self.prv_key_data_id.into(), + self.receive_address().ok(), + self.change_address().ok(), + ) + .with_property(AccountDescriptorProperty::AccountIndex, self.account_index.into()) + .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) + .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()) + .with_property(AccountDescriptorProperty::DerivationMeta, self.derivation.address_derivation_meta().into()); + + Ok(descriptor) + } + + fn as_derivation_capable(self: Arc) -> Result> { + Ok(self.clone()) + } +} + +impl DerivationCapableAccount for Bip32 { + fn derivation(&self) -> Arc { + self.derivation.clone() + } + + fn account_index(&self) -> u64 { + self.account_index + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_bip32() -> Result<()> { + let storable_in = Payload::new(0xbaadf00d, vec![make_xpub()].into(), false); + let guard = StorageGuard::new(&storable_in); + let storable_out = guard.validate()?; + + assert_eq!(storable_in.account_index, storable_out.account_index); + assert_eq!(storable_in.ecdsa, storable_out.ecdsa); + assert_eq!(storable_in.xpub_keys.len(), storable_out.xpub_keys.len()); + for idx in 0..storable_in.xpub_keys.len() { + assert_eq!(storable_in.xpub_keys[idx], storable_out.xpub_keys[idx]); + } + + Ok(()) + } +} diff --git a/wallet/core/src/account/variants/keypair.rs b/wallet/core/src/account/variants/keypair.rs new file mode 100644 index 000000000..ef051a75a --- /dev/null +++ b/wallet/core/src/account/variants/keypair.rs @@ -0,0 +1,192 @@ +//! +//! Secp256k1 keypair account implementation +//! + +use crate::account::Inner; +use crate::imports::*; +use kash_addresses::Version; +use secp256k1::PublicKey; + +pub const KEYPAIR_ACCOUNT_KIND: &str = "kash-keypair-standard"; + +pub struct Ctor {} + +#[async_trait] +impl Factory for Ctor { + fn name(&self) -> String { + "Keypair".to_string() + } + + fn description(&self) -> String { + "Secp265k1 Keypair Account".to_string() + } + + async fn try_load( + &self, + wallet: &Arc, + storage: &AccountStorage, + meta: Option>, + ) -> Result> { + Ok(Arc::new(Keypair::try_load(wallet, storage, meta).await?)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub struct Payload { + pub public_key: secp256k1::PublicKey, + pub ecdsa: bool, +} + +impl Payload { + pub fn new(public_key: secp256k1::PublicKey, ecdsa: bool) -> Self { + Self { public_key, ecdsa } + } + + pub fn try_load(storage: &AccountStorage) -> Result { + Ok(Self::try_from_slice(storage.serialized.as_slice())?) + } +} + +impl Storable for Payload { + const STORAGE_MAGIC: u32 = 0x52494150; + const STORAGE_VERSION: u32 = 0; +} + +impl AccountStorable for Payload {} + +impl BorshSerialize for Payload { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + let public_key = self.public_key.serialize(); + + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + + BorshSerialize::serialize(public_key.as_slice(), writer)?; + BorshSerialize::serialize(&self.ecdsa, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for Payload { + fn deserialize(buf: &mut &[u8]) -> IoResult { + use secp256k1::constants::PUBLIC_KEY_SIZE; + + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let public_key_bytes: [u8; PUBLIC_KEY_SIZE] = buf[..PUBLIC_KEY_SIZE] + .try_into() + .map_err(|_| IoError::new(IoErrorKind::Other, "Unable to deserialize keypair account (public_key buffer try_into)"))?; + let public_key = secp256k1::PublicKey::from_slice(&public_key_bytes) + .map_err(|_| IoError::new(IoErrorKind::Other, "Unable to deserialize keypair account (invalid public key)"))?; + *buf = &buf[PUBLIC_KEY_SIZE..]; + let ecdsa = BorshDeserialize::deserialize(buf)?; + + Ok(Self { public_key, ecdsa }) + } +} + +pub struct Keypair { + inner: Arc, + prv_key_data_id: PrvKeyDataId, + public_key: PublicKey, + ecdsa: bool, +} + +impl Keypair { + pub async fn try_new( + wallet: &Arc, + name: Option, + public_key: secp256k1::PublicKey, + prv_key_data_id: PrvKeyDataId, + ecdsa: bool, + ) -> Result { + let storable = Payload::new(public_key, ecdsa); + let settings = AccountSettings { name, ..Default::default() }; + + let (id, storage_key) = make_account_hashes(from_keypair(&prv_key_data_id, &storable)); + let inner = Arc::new(Inner::new(wallet, id, storage_key, settings)); + + let Payload { public_key, ecdsa, .. } = storable; + Ok(Self { inner, prv_key_data_id, public_key, ecdsa }) + } + + pub async fn try_load(wallet: &Arc, storage: &AccountStorage, _meta: Option>) -> Result { + let storable = Payload::try_load(storage)?; + let inner = Arc::new(Inner::from_storage(wallet, storage)); + + let Payload { public_key, ecdsa, .. } = storable; + Ok(Self { inner, prv_key_data_id: storage.prv_key_data_ids.clone().try_into()?, public_key, ecdsa }) + } +} + +#[async_trait] +impl Account for Keypair { + fn inner(&self) -> &Arc { + &self.inner + } + + fn account_kind(&self) -> AccountKind { + KEYPAIR_ACCOUNT_KIND.into() + } + + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { + Ok(&self.prv_key_data_id) + } + + fn as_dyn_arc(self: Arc) -> Arc { + self + } + + fn sig_op_count(&self) -> u8 { + 1 + } + + fn minimum_signatures(&self) -> u16 { + 1 + } + + fn receive_address(&self) -> Result
{ + let (xonly_public_key, _) = self.public_key.x_only_public_key(); + Ok(Address::new(self.inner().wallet.network_id()?.into(), Version::PubKey, &xonly_public_key.serialize())) + } + + fn change_address(&self) -> Result
{ + let (xonly_public_key, _) = self.public_key.x_only_public_key(); + Ok(Address::new(self.inner().wallet.network_id()?.into(), Version::PubKey, &xonly_public_key.serialize())) + } + + fn to_storage(&self) -> Result { + let settings = self.context().settings.clone(); + let storable = Payload::new(self.public_key, self.ecdsa); + let account_storage = AccountStorage::try_new( + KEYPAIR_ACCOUNT_KIND.into(), + self.id(), + self.storage_key(), + self.prv_key_data_id.into(), + settings, + storable, + )?; + + Ok(account_storage) + } + + fn metadata(&self) -> Result> { + Ok(None) + } + + fn descriptor(&self) -> Result { + let descriptor = AccountDescriptor::new( + KEYPAIR_ACCOUNT_KIND.into(), + *self.id(), + self.name(), + self.prv_key_data_id.into(), + self.receive_address().ok(), + self.change_address().ok(), + ) + .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()); + + Ok(descriptor) + } +} diff --git a/wallet/core/src/account/variants/legacy.rs b/wallet/core/src/account/variants/legacy.rs new file mode 100644 index 000000000..8cd69c3d8 --- /dev/null +++ b/wallet/core/src/account/variants/legacy.rs @@ -0,0 +1,237 @@ +//! +//! Legacy (KDX, kashnet.io Web Wallet) account implementation +//! + +use crate::account::{AsLegacyAccount, Inner}; +use crate::derivation::{AddressDerivationManager, AddressDerivationManagerTrait}; +use crate::imports::*; +use kash_bip32::{ExtendedPrivateKey, Prefix, SecretKey}; + +const CACHE_ADDRESS_OFFSET: u32 = 2048; + +pub const LEGACY_ACCOUNT_KIND: &str = "kash-legacy-standard"; + +pub struct Ctor {} + +#[async_trait] +impl Factory for Ctor { + fn name(&self) -> String { + "bip32/legacy".to_string() + } + + fn description(&self) -> String { + "Kash Legacy Account (KDX, kashnet.io Web Wallet)".to_string() + } + + async fn try_load( + &self, + wallet: &Arc, + storage: &AccountStorage, + meta: Option>, + ) -> Result> { + Ok(Arc::new(Legacy::try_load(wallet, storage, meta).await?)) + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub struct Payload; + +impl Payload { + pub fn try_load(storage: &AccountStorage) -> Result { + Ok(Self::try_from_slice(storage.serialized.as_slice())?) + } +} + +impl Storable for Payload { + const STORAGE_MAGIC: u32 = 0x5943474c; + const STORAGE_VERSION: u32 = 0; +} + +impl AccountStorable for Payload {} + +impl BorshSerialize for Payload { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for Payload { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + Ok(Self {}) + } +} + +pub struct Legacy { + inner: Arc, + prv_key_data_id: PrvKeyDataId, + derivation: Arc, +} + +impl Legacy { + pub async fn try_new(wallet: &Arc, name: Option, prv_key_data_id: PrvKeyDataId) -> Result { + let storable = Payload; + let settings = AccountSettings { name, ..Default::default() }; + + let (id, storage_key) = make_account_hashes(from_legacy(&prv_key_data_id, &storable)); + let inner = Arc::new(Inner::new(wallet, id, storage_key, settings)); + + let account_index = 0; + let derivation = AddressDerivationManager::create_legacy_pubkey_managers(wallet, account_index, Default::default())?; + + Ok(Self { inner, prv_key_data_id, derivation }) + } + + pub async fn try_load(wallet: &Arc, storage: &AccountStorage, meta: Option>) -> Result { + let prv_key_data_id: PrvKeyDataId = storage.prv_key_data_ids.clone().try_into()?; + + let inner = Arc::new(Inner::from_storage(wallet, storage)); + + let address_derivation_indexes = meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or_default(); + let account_index = 0; + let derivation = + AddressDerivationManager::create_legacy_pubkey_managers(wallet, account_index, address_derivation_indexes.clone())?; + + Ok(Self { inner, prv_key_data_id, derivation }) + } + + pub async fn initialize_derivation( + &self, + wallet_secret: &Secret, + payment_secret: Option<&Secret>, + index: Option, + ) -> Result<()> { + let prv_key_data = self + .inner + .wallet + .get_prv_key_data(wallet_secret, &self.prv_key_data_id) + .await? + .ok_or(Error::Custom(format!("Prv key data is missing for {}", self.prv_key_data_id.to_hex())))?; + let mnemonic = prv_key_data + .as_mnemonic(payment_secret)? + .ok_or(Error::Custom(format!("Could not convert Prv key data into mnemonic for {}", self.prv_key_data_id.to_hex())))?; + + let seed = mnemonic.to_seed(""); + let xprv = ExtendedPrivateKey::::new(seed).unwrap(); + let xprv = xprv.to_string(Prefix::XPRV).to_string(); + + for derivator in &self.derivation.derivators { + derivator.initialize(xprv.clone(), index)?; + } + + let m = self.derivation.receive_address_manager(); + m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + let m = self.derivation.change_address_manager(); + m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + + Ok(()) + } +} + +#[async_trait] +impl Account for Legacy { + fn inner(&self) -> &Arc { + &self.inner + } + + fn account_kind(&self) -> AccountKind { + LEGACY_ACCOUNT_KIND.into() + } + + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { + Ok(&self.prv_key_data_id) + } + + fn as_dyn_arc(self: Arc) -> Arc { + self + } + + fn sig_op_count(&self) -> u8 { + 1 + } + + fn minimum_signatures(&self) -> u16 { + 1 + } + + fn receive_address(&self) -> Result
{ + self.derivation.receive_address_manager().current_address() + } + + fn change_address(&self) -> Result
{ + self.derivation.change_address_manager().current_address() + } + + fn to_storage(&self) -> Result { + let settings = self.context().settings.clone(); + let storable = Payload; + let account_storage = AccountStorage::try_new( + LEGACY_ACCOUNT_KIND.into(), + self.id(), + self.storage_key(), + self.prv_key_data_id.into(), + settings, + storable, + )?; + + Ok(account_storage) + } + + fn metadata(&self) -> Result> { + let metadata = AccountMetadata::new(self.inner.id, self.derivation.address_derivation_meta()); + Ok(Some(metadata)) + } + + fn descriptor(&self) -> Result { + let descriptor = AccountDescriptor::new( + LEGACY_ACCOUNT_KIND.into(), + *self.id(), + self.name(), + self.prv_key_data_id.into(), + self.receive_address().ok(), + self.change_address().ok(), + ) + .with_property(AccountDescriptorProperty::DerivationMeta, self.derivation.address_derivation_meta().into()); + + Ok(descriptor) + } + + fn as_derivation_capable(self: Arc) -> Result> { + Ok(self.clone()) + } + + fn as_legacy_account(self: Arc) -> Result> { + Ok(self.clone()) + } +} + +#[async_trait] +impl AsLegacyAccount for Legacy { + async fn create_private_context(&self, wallet_secret: &Secret, payment_secret: Option<&Secret>, index: Option) -> Result<()> { + self.initialize_derivation(wallet_secret, payment_secret, index).await?; + Ok(()) + } + + async fn clear_private_context(&self) -> Result<()> { + for derivator in &self.derivation.derivators { + derivator.uninitialize()?; + } + Ok(()) + } +} + +impl DerivationCapableAccount for Legacy { + fn derivation(&self) -> Arc { + self.derivation.clone() + } + + // legacy accounts do not support bip44 + fn account_index(&self) -> u64 { + 0 + } +} diff --git a/wallet/core/src/account/variants/mod.rs b/wallet/core/src/account/variants/mod.rs new file mode 100644 index 000000000..77847f838 --- /dev/null +++ b/wallet/core/src/account/variants/mod.rs @@ -0,0 +1,15 @@ +//! +//! Kash core wallet account variant implementations. +//! + +pub mod bip32; +pub mod keypair; +pub mod legacy; +pub mod multisig; +pub mod resident; + +pub use bip32::BIP32_ACCOUNT_KIND; +pub use keypair::KEYPAIR_ACCOUNT_KIND; +pub use legacy::LEGACY_ACCOUNT_KIND; +pub use multisig::MULTISIG_ACCOUNT_KIND; +pub use resident::RESIDENT_ACCOUNT_KIND; diff --git a/wallet/core/src/account/variants/multisig.rs b/wallet/core/src/account/variants/multisig.rs new file mode 100644 index 000000000..3436f8877 --- /dev/null +++ b/wallet/core/src/account/variants/multisig.rs @@ -0,0 +1,272 @@ +//! +//! MultiSig account implementation. +//! + +use crate::account::Inner; +use crate::derivation::{AddressDerivationManager, AddressDerivationManagerTrait}; +use crate::imports::*; + +pub const MULTISIG_ACCOUNT_KIND: &str = "kash-multisig-standard"; + +pub struct Ctor {} + +#[async_trait] +impl Factory for Ctor { + fn name(&self) -> String { + "multisig".to_string() + } + + fn description(&self) -> String { + "Kash Core Multi-Signature Account".to_string() + } + + async fn try_load( + &self, + wallet: &Arc, + storage: &AccountStorage, + meta: Option>, + ) -> Result> { + Ok(Arc::new(MultiSig::try_load(wallet, storage, meta).await?)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub struct Payload { + pub xpub_keys: ExtendedPublicKeys, + pub cosigner_index: Option, + pub minimum_signatures: u16, + pub ecdsa: bool, +} + +impl Payload { + pub fn new(xpub_keys: ExtendedPublicKeys, cosigner_index: Option, minimum_signatures: u16, ecdsa: bool) -> Self { + Self { xpub_keys, cosigner_index, minimum_signatures, ecdsa } + } + + pub fn try_load(storage: &AccountStorage) -> Result { + Ok(Self::try_from_slice(storage.serialized.as_slice())?) + } +} + +impl Storable for Payload { + const STORAGE_MAGIC: u32 = 0x4749534d; + const STORAGE_VERSION: u32 = 0; +} + +impl AccountStorable for Payload {} + +impl BorshSerialize for Payload { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + + BorshSerialize::serialize(&self.xpub_keys, writer)?; + BorshSerialize::serialize(&self.cosigner_index, writer)?; + BorshSerialize::serialize(&self.minimum_signatures, writer)?; + BorshSerialize::serialize(&self.ecdsa, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for Payload { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let xpub_keys = BorshDeserialize::deserialize(buf)?; + let cosigner_index = BorshDeserialize::deserialize(buf)?; + let minimum_signatures = BorshDeserialize::deserialize(buf)?; + let ecdsa = BorshDeserialize::deserialize(buf)?; + + Ok(Self { xpub_keys, cosigner_index, minimum_signatures, ecdsa }) + } +} + +pub struct MultiSig { + inner: Arc, + xpub_keys: ExtendedPublicKeys, + prv_key_data_ids: Option>>, + cosigner_index: Option, + minimum_signatures: u16, + ecdsa: bool, + derivation: Arc, +} + +impl MultiSig { + pub async fn try_new( + wallet: &Arc, + name: Option, + xpub_keys: ExtendedPublicKeys, + prv_key_data_ids: Option>>, + cosigner_index: Option, + minimum_signatures: u16, + ecdsa: bool, + ) -> Result { + let storable = Payload::new(xpub_keys.clone(), cosigner_index, minimum_signatures, ecdsa); + let settings = AccountSettings { name, ..Default::default() }; + let (id, storage_key) = make_account_hashes(from_multisig(&prv_key_data_ids, &storable)); + let inner = Arc::new(Inner::new(wallet, id, storage_key, settings)); + + let derivation = AddressDerivationManager::new( + wallet, + MULTISIG_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + cosigner_index.map(|v| v as u32), + minimum_signatures, + Default::default(), + ) + .await?; + + Ok(Self { inner, xpub_keys, cosigner_index, minimum_signatures, ecdsa, derivation, prv_key_data_ids }) + } + + pub async fn try_load(wallet: &Arc, storage: &AccountStorage, meta: Option>) -> Result { + let storable = Payload::try_load(storage)?; + let inner = Arc::new(Inner::from_storage(wallet, storage)); + + let Payload { xpub_keys, cosigner_index, minimum_signatures, ecdsa, .. } = storable; + + let address_derivation_indexes = meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or_default(); + + let derivation = AddressDerivationManager::new( + wallet, + MULTISIG_ACCOUNT_KIND.into(), + &xpub_keys, + ecdsa, + 0, + cosigner_index.map(|v| v as u32), + minimum_signatures, + address_derivation_indexes, + ) + .await?; + + // TODO @maxim check variants transforms - None->Ok(None), Multiple->Ok(Some()), Single->Err() + let prv_key_data_ids = storage.prv_key_data_ids.clone().try_into()?; + + Ok(Self { inner, xpub_keys, cosigner_index, minimum_signatures, ecdsa, derivation, prv_key_data_ids }) + } + + pub fn prv_key_data_ids(&self) -> &Option>> { + &self.prv_key_data_ids + } + + pub fn minimum_signatures(&self) -> u16 { + self.minimum_signatures + } + + pub fn xpub_keys(&self) -> &ExtendedPublicKeys { + &self.xpub_keys + } +} + +#[async_trait] +impl Account for MultiSig { + fn inner(&self) -> &Arc { + &self.inner + } + + fn account_kind(&self) -> AccountKind { + MULTISIG_ACCOUNT_KIND.into() + } + + fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { + Err(Error::AccountKindFeature) + } + + fn as_dyn_arc(self: Arc) -> Arc { + self + } + + fn sig_op_count(&self) -> u8 { + // TODO @maxim + 1 + } + + fn minimum_signatures(&self) -> u16 { + self.minimum_signatures + } + + fn receive_address(&self) -> Result
{ + self.derivation.receive_address_manager().current_address() + } + + fn change_address(&self) -> Result
{ + self.derivation.change_address_manager().current_address() + } + + fn to_storage(&self) -> Result { + let settings = self.context().settings.clone(); + let storable = Payload::new(self.xpub_keys.clone(), self.cosigner_index, self.minimum_signatures, self.ecdsa); + let account_storage = AccountStorage::try_new( + MULTISIG_ACCOUNT_KIND.into(), + self.id(), + self.storage_key(), + self.prv_key_data_ids.clone().try_into()?, + settings, + storable, + )?; + + Ok(account_storage) + } + + fn metadata(&self) -> Result> { + let metadata = AccountMetadata::new(self.inner.id, self.derivation.address_derivation_meta()); + Ok(Some(metadata)) + } + + fn descriptor(&self) -> Result { + let descriptor = AccountDescriptor::new( + MULTISIG_ACCOUNT_KIND.into(), + *self.id(), + self.name(), + self.prv_key_data_ids.clone().try_into()?, + self.receive_address().ok(), + self.change_address().ok(), + ) + .with_property(AccountDescriptorProperty::XpubKeys, self.xpub_keys.clone().into()) + .with_property(AccountDescriptorProperty::Ecdsa, self.ecdsa.into()) + .with_property(AccountDescriptorProperty::DerivationMeta, self.derivation.address_derivation_meta().into()); + + Ok(descriptor) + } + + fn as_derivation_capable(self: Arc) -> Result> { + Ok(self.clone()) + } +} + +impl DerivationCapableAccount for MultiSig { + fn derivation(&self) -> Arc { + self.derivation.clone() + } + + fn account_index(&self) -> u64 { + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_multisig() -> Result<()> { + let storable_in = Payload::new(vec![make_xpub()].into(), Some(42), 0xc0fe, false); + let guard = StorageGuard::new(&storable_in); + let storable_out = guard.validate()?; + + assert_eq!(storable_in.cosigner_index, storable_out.cosigner_index); + assert_eq!(storable_in.minimum_signatures, storable_out.minimum_signatures); + assert_eq!(storable_in.ecdsa, storable_out.ecdsa); + assert_eq!(storable_in.xpub_keys.len(), storable_out.xpub_keys.len()); + for idx in 0..storable_in.xpub_keys.len() { + assert_eq!(storable_in.xpub_keys[idx], storable_out.xpub_keys[idx]); + } + + Ok(()) + } +} diff --git a/wallet/core/src/runtime/account/variants/resident.rs b/wallet/core/src/account/variants/resident.rs similarity index 50% rename from wallet/core/src/runtime/account/variants/resident.rs rename to wallet/core/src/account/variants/resident.rs index a066d411e..546a16c9b 100644 --- a/wallet/core/src/runtime/account/variants/resident.rs +++ b/wallet/core/src/account/variants/resident.rs @@ -1,11 +1,14 @@ +//! +//! Resident account implementation (for temporary use in runtime) +//! + +use crate::account::Inner; use crate::imports::*; -use crate::result::Result; -use crate::runtime::account::{Account, AccountId, AccountKind, Inner}; -use crate::runtime::Wallet; -use crate::storage::{self, Metadata, PrvKeyDataId}; use kash_addresses::Version; use secp256k1::{PublicKey, SecretKey}; +pub const RESIDENT_ACCOUNT_KIND: &str = "kash-resident-standard"; + pub struct Resident { inner: Arc, public_key: PublicKey, @@ -15,9 +18,9 @@ pub struct Resident { } impl Resident { - pub async fn try_new(wallet: &Arc, public_key: PublicKey, secret_key: Option) -> Result { - let id = AccountId::from_public_key(AccountKind::Resident, &public_key); - let inner = Arc::new(Inner::new(wallet, id, None)); + pub async fn try_load(wallet: &Arc, public_key: PublicKey, secret_key: Option) -> Result { + let (id, storage_key) = make_account_hashes(from_public_key(&RESIDENT_ACCOUNT_KIND.into(), &public_key)); + let inner = Arc::new(Inner::new(wallet, id, storage_key, Default::default())); Ok(Self { inner, public_key, secret_key }) } @@ -30,7 +33,7 @@ impl Account for Resident { } fn account_kind(&self) -> AccountKind { - AccountKind::Resident + RESIDENT_ACCOUNT_KIND.into() } fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { @@ -41,6 +44,16 @@ impl Account for Resident { self } + fn sig_op_count(&self) -> u8 { + // TODO - discuss + unreachable!() + } + + fn minimum_signatures(&self) -> u16 { + // TODO - discuss + unreachable!() + } + fn receive_address(&self) -> Result
{ let (xonly_public_key, _) = self.public_key.x_only_public_key(); Ok(Address::new(self.inner().wallet.network_id()?.into(), Version::PubKey, &xonly_public_key.serialize())) @@ -51,11 +64,24 @@ impl Account for Resident { Ok(Address::new(self.inner().wallet.network_id()?.into(), Version::PubKey, &xonly_public_key.serialize())) } - fn as_storable(&self) -> Result { + fn to_storage(&self) -> Result { Err(Error::ResidentAccount) } - fn metadata(&self) -> Result> { + fn metadata(&self) -> Result> { Err(Error::ResidentAccount) } + + fn descriptor(&self) -> Result { + let descriptor = AccountDescriptor::new( + RESIDENT_ACCOUNT_KIND.into(), + *self.id(), + self.name(), + AssocPrvKeyDataIds::None, + self.receive_address().ok(), + self.change_address().ok(), + ); + + Ok(descriptor) + } } diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs new file mode 100644 index 000000000..edc1f4ad5 --- /dev/null +++ b/wallet/core/src/api/message.rs @@ -0,0 +1,465 @@ +//! +//! Messages used by the Wallet API. +//! +//! Each Wallet API `xxx_call()` method has a corresponding +//! `XxxRequest` and `XxxResponse` message. +//! + +use crate::imports::*; +use crate::tx::{Fees, GeneratorSummary, PaymentDestination}; +use kash_addresses::Address; +use kash_consensus_core::asset_type::AssetType; +use kash_consensus_core::tx::TransactionAction; + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PingRequest { + pub payload: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PingResponse { + pub payload: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FlushRequest { + pub wallet_secret: Secret, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct FlushResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectRequest { + pub url: String, + pub network_id: NetworkId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct DisconnectRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct DisconnectResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetStatusRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetStatusResponse { + pub is_connected: bool, + pub is_synced: bool, + pub is_open: bool, + pub url: Option, + pub is_wrpc_client: bool, + pub network_id: Option, +} + +// --- + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletEnumerateRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletEnumerateResponse { + pub wallet_list: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletCreateRequest { + pub wallet_secret: Secret, + pub wallet_args: WalletCreateArgs, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletCreateResponse { + pub storage_descriptor: StorageDescriptor, + pub wallet_descriptor: WalletDescriptor, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletOpenRequest { + pub wallet_secret: Secret, + pub wallet_filename: Option, + pub account_descriptors: bool, + pub legacy_accounts: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletOpenResponse { + pub account_descriptors: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletCloseRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletCloseResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletRenameRequest { + pub title: Option, + pub filename: Option, + pub wallet_secret: Secret, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletRenameResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletRenameFileResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletChangeSecretRequest { + pub old_wallet_secret: Secret, + pub new_wallet_secret: Secret, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletChangeSecretResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletExportRequest { + pub wallet_secret: Secret, + pub include_transactions: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletExportResponse { + pub wallet_data: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletImportRequest { + pub wallet_secret: Secret, + pub wallet_data: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletImportResponse { + pub wallet_descriptor: WalletDescriptor, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataEnumerateRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataEnumerateResponse { + pub prv_key_data_list: Vec>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataCreateRequest { + pub wallet_secret: Secret, + pub prv_key_data_args: PrvKeyDataCreateArgs, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataCreateResponse { + pub prv_key_data_id: PrvKeyDataId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataRemoveRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataRemoveResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataGetRequest { + pub wallet_secret: Secret, + pub prv_key_data_id: PrvKeyDataId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataGetResponse { + pub prv_key_data: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsEnumerateRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsEnumerateResponse { + pub descriptor_list: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsRenameRequest { + pub account_id: AccountId, + pub name: Option, + pub wallet_secret: Secret, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsRenameResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub enum AccountsDiscoveryKind { + Bip44, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsDiscoveryRequest { + pub discovery_kind: AccountsDiscoveryKind, + pub address_scan_extent: u32, + pub account_scan_extent: u32, + pub bip39_passphrase: Option, + pub bip39_mnemonic: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsDiscoveryResponse { + pub last_account_index_found: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCreateRequest { + pub wallet_secret: Secret, + pub account_create_args: AccountCreateArgs, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCreateResponse { + pub account_descriptor: AccountDescriptor, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsImportRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsImportResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsActivateRequest { + pub account_ids: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsActivateResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsDeactivateRequest { + pub account_ids: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsDeactivateResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsGetRequest { + pub account_id: AccountId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsGetResponse { + pub descriptor: AccountDescriptor, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub enum NewAddressKind { + Receive, + Change, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCreateNewAddressRequest { + pub account_id: AccountId, + #[serde(rename = "type")] + pub kind: NewAddressKind, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsCreateNewAddressResponse { + pub address: Address, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsSendRequest { + pub account_id: AccountId, + pub asset_type: AssetType, + pub wallet_secret: Secret, + pub payment_secret: Option, + pub destination: PaymentDestination, + pub priority_fee_sompi: Fees, + pub payload: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsSendResponse { + pub generator_summary: GeneratorSummary, + pub transaction_ids: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsTransferRequest { + pub asset_type: AssetType, + pub source_account_id: AccountId, + pub destination_account_id: AccountId, + pub wallet_secret: Secret, + pub payment_secret: Option, + pub transfer_amount_sompi: u64, + pub priority_fee_sompi: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsTransferResponse { + pub generator_summary: GeneratorSummary, + pub transaction_ids: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsEstimateRequest { + pub account_id: AccountId, + pub tx_action: TransactionAction, + pub destination: PaymentDestination, + pub priority_fee_sompi: Fees, + pub payload: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsEstimateResponse { + pub generator_summary: GeneratorSummary, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionsDataGetRequest { + pub account_id: AccountId, + pub network_id: NetworkId, + pub filter: Option>, + pub start: u64, + pub end: u64, +} + +impl TransactionsDataGetRequest { + pub fn with_range(account_id: AccountId, network_id: NetworkId, range: std::ops::Range) -> Self { + Self { account_id, network_id, filter: None, start: range.start, end: range.end } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionsDataGetResponse { + pub account_id: AccountId, + pub transactions: Vec>, + pub start: u64, + pub total: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionsReplaceNoteRequest { + pub account_id: AccountId, + pub network_id: NetworkId, + pub transaction_id: TransactionId, + pub note: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionsReplaceNoteResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionsReplaceMetadataRequest { + pub account_id: AccountId, + pub network_id: NetworkId, + pub transaction_id: TransactionId, + pub metadata: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionsReplaceMetadataResponse {} + +// #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct TransactionGetRequest {} + +// #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct TransactionGetResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddressBookEnumerateRequest {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AddressBookEnumerateResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletNotification {} diff --git a/wallet/core/src/api/mod.rs b/wallet/core/src/api/mod.rs new file mode 100644 index 000000000..f0963d610 --- /dev/null +++ b/wallet/core/src/api/mod.rs @@ -0,0 +1,11 @@ +//! +//! Wallet API module that provides a unified interface for all wallet operations. +//! + +pub mod message; +pub use message::*; + +pub mod traits; +pub use traits::*; + +pub mod transport; diff --git a/wallet/core/src/api/traits.rs b/wallet/core/src/api/traits.rs new file mode 100644 index 000000000..7685dc980 --- /dev/null +++ b/wallet/core/src/api/traits.rs @@ -0,0 +1,380 @@ +//! +//! API trait for interfacing with the Kash wallet subsystem. +//! +//! The wallet API is a high-level API that allows applications to perform +//! wallet operations such as creating a wallet, opening a wallet, creating +//! accounts, sending funds etc. The wallet API is an asynchronous trait that +//! is implemented by the [`Wallet`] struct. +//! + +use crate::api::message::*; +use crate::imports::*; +use crate::result::Result; +use crate::secret::Secret; +use crate::storage::{PrvKeyData, PrvKeyDataId, PrvKeyDataInfo, WalletDescriptor}; +use crate::tx::GeneratorSummary; +use workflow_core::channel::Receiver; + +/// +/// API trait for interfacing with the Kash wallet subsystem. +/// +#[async_trait] +pub trait WalletApi: Send + Sync + AnySync { + async fn register_notifications(self: Arc, channel: Receiver) -> Result; + async fn unregister_notifications(self: Arc, channel_id: u64) -> Result<()>; + + /// Wrapper around [`get_status_call()`](Self::get_status_call). + async fn get_status(self: Arc) -> Result { + Ok(self.get_status_call(GetStatusRequest {}).await?) + } + + /// Returns the current wallet state comprised of the following: + /// - `is_connected` - whether the wallet is connected to the node + /// - `network_id` - the network id of the node the wallet is connected to + /// - `is_synced` - the sync status of the node the wallet is connected to + /// - `is_open` - whether a wallet is currently open + /// - `url` - the wRPC url of the node the wallet is connected to + /// - `is_wrpc_client` - whether the wallet is connected to a node via wRPC + async fn get_status_call(self: Arc, request: GetStatusRequest) -> Result; + + /// Request the wallet RPC subsystem to connect to a node with a given configuration + /// comprised of the `url` and a `network_id`. + async fn connect_call(self: Arc, request: ConnectRequest) -> Result; + + /// Disconnect the wallet RPC subsystem from the node. + async fn disconnect_call(self: Arc, request: DisconnectRequest) -> Result; + + // --- + + /// Wrapper around `ping_call()`. + async fn ping(self: Arc, payload: Option) -> Result> { + Ok(self.ping_call(PingRequest { payload }).await?.payload) + } + /// Ping the wallet service. Accepts an optional `u64` value that is returned in the response. + async fn ping_call(self: Arc, request: PingRequest) -> Result; + + async fn batch(self: Arc) -> Result<()> { + self.batch_call(BatchRequest {}).await?; + Ok(()) + } + /// Initiates the wallet storage batch mode. Must be followed by the [`flush_call()`](Self::flush_call) + /// after the desired wallet operations have been executed. + /// + /// Batch mode allows user to perform multiple wallet operations without storing the + /// wallet data into the storage subsystem (disk, localstorage etc). This is helpful + /// in web browsers as each time the wallet is stored, it needs to be encrypted, + /// which can be costly in low-performance environments such as web browsers. + /// + async fn batch_call(self: Arc, request: BatchRequest) -> Result; + + async fn flush(self: Arc, wallet_secret: Secret) -> Result<()> { + self.flush_call(FlushRequest { wallet_secret }).await?; + Ok(()) + } + /// Saves the wallet data into the storage subsystem (disk, localstorage etc) if + /// the wallet is in the batch mode and it's data has been marked as dirty. + async fn flush_call(self: Arc, request: FlushRequest) -> Result; + + /// Wrapper around [`wallet_enumerate_call()`](Self::wallet_enumerate_call). + async fn wallet_enumerate(self: Arc) -> Result> { + Ok(self.wallet_enumerate_call(WalletEnumerateRequest {}).await?.wallet_list) + } + + /// Enumerates all wallets available in the storage. Returns `Vec` + /// that can be subsequently used to perform wallet operations such as open the wallet. + /// See [`wallet_enumerate()`](Self::wallet_enumerate) for a convenience wrapper + /// around this call. + async fn wallet_enumerate_call(self: Arc, request: WalletEnumerateRequest) -> Result; + + /// Wrapper around [`wallet_create_call()`](Self::wallet_create_call) + async fn wallet_create(self: Arc, wallet_secret: Secret, wallet_args: WalletCreateArgs) -> Result { + self.wallet_create_call(WalletCreateRequest { wallet_secret, wallet_args }).await + } + + /// Creates a new wallet. Returns [`WalletCreateResponse`] that contains `wallet_descriptor` + /// that can be used to subsequently open the wallet. After the wallet is created, it + /// is considered to be in an `open` state. + async fn wallet_create_call(self: Arc, request: WalletCreateRequest) -> Result; + + /// Wrapper around [`wallet_open_call()`](Self::wallet_open_call) + async fn wallet_open( + self: Arc, + wallet_secret: Secret, + wallet_filename: Option, + account_descriptors: bool, + legacy_accounts: bool, + ) -> Result>> { + Ok(self + .wallet_open_call(WalletOpenRequest { + wallet_secret, + wallet_filename, + account_descriptors, + legacy_accounts: legacy_accounts.then_some(true), + }) + .await? + .account_descriptors) + } + + /// Opens a wallet. A wallet is opened by it's `filename`, which is available + /// as a part of the `WalletDescriptor` struct returned during the `wallet_enumerate_call()` call. + /// If the `filename` is `None`, the wallet opens the default wallet named `kash`. + /// + /// If `account_descriptors` is true, this call will return `Some(Vec)` + /// for all accounts in the wallet. + /// + /// If `legacy_accounts` is true, the wallet will enable legacy account compatibility mode + /// allowing the wallet to operate on legacy accounts. Legacy accounts were created by + /// applications such as KDX and kashnet.io web wallet using a deprecated derivation path + /// and are considered deprecated. Legacy accounts should not be used in 3rd-party applications. + /// + /// See [`wallet_open`](Self::wallet_open) for a convenience wrapper around this call. + async fn wallet_open_call(self: Arc, request: WalletOpenRequest) -> Result; + + /// Wrapper around [`wallet_close_call()`](Self::wallet_close_call) + async fn wallet_close(self: Arc) -> Result<()> { + self.wallet_close_call(WalletCloseRequest {}).await?; + Ok(()) + } + /// Close the currently open wallet + async fn wallet_close_call(self: Arc, request: WalletCloseRequest) -> Result; + + /// Wrapper around [`wallet_rename_call()`](Self::wallet_rename_call) + async fn wallet_rename(self: Arc, title: Option<&str>, filename: Option<&str>, wallet_secret: Secret) -> Result<()> { + self.wallet_rename_call(WalletRenameRequest { + title: title.map(String::from), + filename: filename.map(String::from), + wallet_secret, + }) + .await?; + Ok(()) + } + /// Change the wallet title or rename the file in which the wallet is stored. + /// This call will produce an error if the destination filename already exists. + /// See [`wallet_rename`](Self::wallet_rename) for a convenience wrapper around + /// this call. + async fn wallet_rename_call(self: Arc, request: WalletRenameRequest) -> Result; + + /// Return a JSON string that contains raw wallet data. This is available only + /// in the default wallet storage backend and may not be available if the wallet + /// subsystem uses a custom storage backend. + async fn wallet_export_call(self: Arc, request: WalletExportRequest) -> Result; + + /// Import the raw wallet data from a JSON string. This is available only + /// in the default wallet storage backend and may not be available if the wallet + /// subsystem uses a custom storage backend. + async fn wallet_import_call(self: Arc, request: WalletImportRequest) -> Result; + + /// Wrapper around [`wallet_change_secret_call()`](Self::wallet_change_secret_call) + async fn wallet_change_secret(self: Arc, old_wallet_secret: Secret, new_wallet_secret: Secret) -> Result<()> { + let request = WalletChangeSecretRequest { old_wallet_secret, new_wallet_secret }; + self.wallet_change_secret_call(request).await?; + Ok(()) + } + + /// Change the wallet secret. This call will re-encrypt the wallet data using the new secret. + /// See [`wallet_change_secret`](Self::wallet_change_secret) for a convenience wrapper around + /// this call. + async fn wallet_change_secret_call(self: Arc, request: WalletChangeSecretRequest) -> Result; + + /// Wrapper around [`prv_key_data_enumerate_call()`](Self::prv_key_data_enumerate_call) + async fn prv_key_data_enumerate(self: Arc) -> Result>> { + Ok(self.prv_key_data_enumerate_call(PrvKeyDataEnumerateRequest {}).await?.prv_key_data_list) + } + + /// Enumerate all private key data available in the wallet. + /// The returned [`PrvKeyDataEnumerateResponse`] contains a list + /// of [`PrvKeyDataInfo`] structs that acts as private key descriptors. + async fn prv_key_data_enumerate_call(self: Arc, request: PrvKeyDataEnumerateRequest) -> Result; + + /// Wrapper around [`prv_key_data_create_call()`](Self::prv_key_data_create_call) + async fn prv_key_data_create( + self: Arc, + wallet_secret: Secret, + prv_key_data_args: PrvKeyDataCreateArgs, + ) -> Result { + let request = PrvKeyDataCreateRequest { wallet_secret, prv_key_data_args }; + Ok(self.prv_key_data_create_call(request).await?.prv_key_data_id) + } + /// Create a new private key data. This call receives a user-supplied bip39 mnemonic as well as + /// an optional bip39 passphrase (payment secret). Please note that a mnemonic that contains + /// bip39 passphrase is also encrypted at runtime using the same passphrase. This is specific + /// to this wallet implementation. To gain access to such mnemonic, the user must supply the + /// bip39 passphrase. + /// + /// See [`prv_key_data_create`](Self::prv_key_data_create) for a convenience wrapper around + /// this call. + async fn prv_key_data_create_call(self: Arc, request: PrvKeyDataCreateRequest) -> Result; + + /// Not implemented + async fn prv_key_data_remove_call(self: Arc, request: PrvKeyDataRemoveRequest) -> Result; + + /// Wrapper around [`prv_key_data_get_call()`](Self::prv_key_data_get_call) + async fn prv_key_data_get(self: Arc, prv_key_data_id: PrvKeyDataId, wallet_secret: Secret) -> Result { + Ok(self + .prv_key_data_get_call(PrvKeyDataGetRequest { prv_key_data_id, wallet_secret }) + .await? + .prv_key_data + .ok_or(Error::PrivateKeyNotFound(prv_key_data_id))?) + } + /// Obtain a private key data using [`PrvKeyDataId`]. + async fn prv_key_data_get_call(self: Arc, request: PrvKeyDataGetRequest) -> Result; + + /// Wrapper around [`accounts_rename_call()`](Self::accounts_rename_call) + async fn accounts_rename(self: Arc, account_id: AccountId, name: Option, wallet_secret: Secret) -> Result<()> { + self.accounts_rename_call(AccountsRenameRequest { account_id, name, wallet_secret }).await?; + Ok(()) + } + /// Change the account title. + /// + /// See [`accounts_rename`](Self::accounts_rename) for a convenience wrapper + /// around this call. + async fn accounts_rename_call(self: Arc, request: AccountsRenameRequest) -> Result; + + /// Wrapper around [`accounts_activate_call()`](Self::accounts_activate_call) + async fn accounts_activate(self: Arc, account_ids: Option>) -> Result { + self.accounts_activate_call(AccountsActivateRequest { account_ids }).await + } + /// Activate a specific set of accounts. + /// An account can be in 2 states - active and inactive. When an account + /// is activated, it performs a discovery of UTXO entries related to its + /// addresses, registers for appropriate notifications and starts tracking + /// its state. As long as an account is active and the wallet is connected + /// to the node, the account will give a consistent view of its state. + /// Deactivating an account will cause it to unregister from notifications + /// and stop tracking its state. + /// + async fn accounts_activate_call(self: Arc, request: AccountsActivateRequest) -> Result; + + /// Wrapper around [`accounts_deactivate_call()`](Self::accounts_deactivate_call) + async fn accounts_deactivate(self: Arc, account_ids: Option>) -> Result { + self.accounts_deactivate_call(AccountsDeactivateRequest { account_ids }).await + } + + /// Deactivate a specific set of accounts. If `account_ids` in [`AccountsDeactivateRequest`] + /// is `None`, all currently active accounts will be deactivated. + async fn accounts_deactivate_call(self: Arc, request: AccountsDeactivateRequest) -> Result; + + /// Wrapper around [`accounts_enumerate_call()`](Self::accounts_enumerate_call) + async fn accounts_enumerate(self: Arc) -> Result> { + Ok(self.accounts_enumerate_call(AccountsEnumerateRequest {}).await?.descriptor_list) + } + /// Returns a list of [`AccountDescriptor`] structs for all accounts stored in the wallet. + async fn accounts_enumerate_call(self: Arc, request: AccountsEnumerateRequest) -> Result; + + /// Performs a bip44 account discovery by scanning the account address space. + /// Returns the last sequential bip44 index of an account that contains a balance. + /// The discovery is performed by scanning `account_scan_extent` accounts where + /// each account is scanned for `address_scan_extent` addresses. If a UTXO is found + /// during the scan, ths account index and all account indexes preceding it are + /// considered as viable. + async fn accounts_discovery_call(self: Arc, request: AccountsDiscoveryRequest) -> Result; + + /// Wrapper around [`accounts_create_call()`](Self::accounts_create_call) + async fn accounts_create( + self: Arc, + wallet_secret: Secret, + account_create_args: AccountCreateArgs, + ) -> Result { + Ok(self.accounts_create_call(AccountsCreateRequest { wallet_secret, account_create_args }).await?.account_descriptor) + } + /// Create a new account based on the [`AccountCreateArgs`] enum. + /// Returns an [`AccountDescriptor`] for the newly created account. + /// + /// See [`accounts_create`](Self::accounts_create) for a convenience wrapper + /// around this call. + async fn accounts_create_call(self: Arc, request: AccountsCreateRequest) -> Result; + + // TODO + async fn accounts_import_call(self: Arc, request: AccountsImportRequest) -> Result; + + /// Get an [`AccountDescriptor`] for a specific account id. + async fn accounts_get_call(self: Arc, request: AccountsGetRequest) -> Result; + + /// Wrapper around [`accounts_create_new_address`](Self::accounts_create_new_address) + async fn accounts_create_new_address( + self: Arc, + account_id: AccountId, + kind: NewAddressKind, + ) -> Result { + self.accounts_create_new_address_call(AccountsCreateNewAddressRequest { account_id, kind }).await + } + + /// Creates a new address for a specified account id. This call is applicable + /// only to derivation-capable accounts (bip32 and legacy accounts). Returns + /// a [`AccountsCreateNewAddressResponse`] that contains a newly generated address. + async fn accounts_create_new_address_call( + self: Arc, + request: AccountsCreateNewAddressRequest, + ) -> Result; + + /// Wrapper around [`Self::accounts_send_call()`](Self::accounts_send_call) + async fn accounts_send(self: Arc, request: AccountsSendRequest) -> Result { + Ok(self.accounts_send_call(request).await?.generator_summary) + } + /// Send funds from an account to one or more external addresses. Returns + /// an [`AccountsSendResponse`] struct that contains a [`GeneratorSummary`] as + /// well `transaction_ids` containing a list of submitted transaction ids. + async fn accounts_send_call(self: Arc, request: AccountsSendRequest) -> Result; + + /// Transfer funds to another account. Returns an [`AccountsTransferResponse`] + /// struct that contains a [`GeneratorSummary`] as well `transaction_ids` + /// containing a list of submitted transaction ids. Unlike funds sent to an + /// external address, funds transferred between wallet accounts are + /// available immediately upon transaction acceptance. + async fn accounts_transfer_call(self: Arc, request: AccountsTransferRequest) -> Result; + + /// Performs a transaction estimate, returning [`AccountsEstimateResponse`] + /// that contains [`GeneratorSummary`]. This call will estimate the total + /// amount of fees that will be required by the transaction as well as + /// the number of UTXOs that will be consumed by the transaction. If this + /// call is invoked while the previous instance of this call is already + /// running for the same account, the previous call will be aborted returning + /// an error. + async fn accounts_estimate_call(self: Arc, request: AccountsEstimateRequest) -> Result; + + /// Get a range of transaction records for a specific account id. + async fn transactions_data_get_range( + self: Arc, + account_id: AccountId, + network_id: NetworkId, + range: std::ops::Range, + ) -> Result { + self.transactions_data_get_call(TransactionsDataGetRequest::with_range(account_id, network_id, range)).await + } + + async fn transactions_data_get_call(self: Arc, request: TransactionsDataGetRequest) -> Result; + // async fn transaction_get_call(self: Arc, request: TransactionGetRequest) -> Result; + + /// Replaces the note of a transaction with a new note. Note is meant + /// to explicitly store a user-supplied string. The note is treated + /// as a raw string without any assumptions about the note format. + /// + /// Supply [`Option::None`] in the `note` field to remove the note. + async fn transactions_replace_note_call( + self: Arc, + request: TransactionsReplaceNoteRequest, + ) -> Result; + + /// Replaces the metadata of a transaction with a new metadata. + /// Metadata is meant to store an application-specific data. + /// If used, the application and encode custom JSON data into the + /// metadata string. The metadata is treated as a raw string + /// without any assumptions about the metadata format. + /// + /// Supply [`Option::None`] in the `metadata` field to + /// remove the metadata. + async fn transactions_replace_metadata_call( + self: Arc, + request: TransactionsReplaceMetadataRequest, + ) -> Result; + + async fn address_book_enumerate_call( + self: Arc, + request: AddressBookEnumerateRequest, + ) -> Result; +} + +/// alias for `Arc` +pub type DynWalletApi = Arc; + +downcast_sync!(dyn WalletApi); diff --git a/wallet/core/src/api/transport.rs b/wallet/core/src/api/transport.rs new file mode 100644 index 000000000..7efd5f8cd --- /dev/null +++ b/wallet/core/src/api/transport.rs @@ -0,0 +1,148 @@ +//! +//! Server and Client transport wrappers that provide automatic +//! serialization and deserialization of Wallet API method +//! arguments and their return values. +//! +//! The serialization occurs using the underlying transport +//! which can be either Borsh or Serde JSON. At compile time, +//! the transport interface macro generates a unique `u64` id +//! (hash) for each API method based on the method name. +//! This id is then use to identify the method. +//! + +use std::sync::Arc; + +use super::message::*; +use super::traits::WalletApi; +use crate::error::Error; +use crate::result::Result; +use async_trait::async_trait; +use borsh::{BorshDeserialize, BorshSerialize}; +use kash_wallet_macros::{build_wallet_client_transport_interface, build_wallet_server_transport_interface}; + +#[async_trait] +pub trait BorshTransport: Send + Sync { + async fn call(&self, op: u64, request: Vec) -> Result>; +} + +#[async_trait] +pub trait SerdeTransport: Send + Sync { + async fn call(&self, op: &str, request: &str) -> Result; +} + +#[derive(Clone)] +pub enum Transport { + Borsh(Arc), + Serde(Arc), +} + +pub struct WalletServer { + pub wallet_api: Arc, +} + +impl WalletServer { + pub fn new(wallet_api: Arc) -> Self { + Self { wallet_api } + } + + pub fn wallet_api(&self) -> &Arc { + &self.wallet_api + } +} + +impl WalletServer { + build_wallet_server_transport_interface! {[ + Ping, + GetStatus, + Connect, + Disconnect, + Batch, + Flush, + WalletEnumerate, + WalletCreate, + WalletOpen, + WalletClose, + WalletRename, + WalletChangeSecret, + WalletExport, + WalletImport, + PrvKeyDataEnumerate, + PrvKeyDataCreate, + PrvKeyDataRemove, + PrvKeyDataGet, + AccountsRename, + AccountsEnumerate, + AccountsDiscovery, + AccountsCreate, + AccountsImport, + AccountsActivate, + AccountsDeactivate, + AccountsGet, + AccountsCreateNewAddress, + AccountsSend, + AccountsTransfer, + AccountsEstimate, + TransactionsDataGet, + TransactionsReplaceNote, + TransactionsReplaceMetadata, + AddressBookEnumerate, + ]} +} + +pub struct WalletClient { + pub transport: Transport, +} + +impl WalletClient { + pub fn new(transport: Transport) -> Self { + Self { transport } + } +} + +use workflow_core::channel::Receiver; +#[async_trait] +impl WalletApi for WalletClient { + async fn register_notifications(self: Arc, _channel: Receiver) -> Result { + todo!() + } + async fn unregister_notifications(self: Arc, _channel_id: u64) -> Result<()> { + todo!() + } + + build_wallet_client_transport_interface! {[ + Ping, + GetStatus, + Connect, + Disconnect, + Batch, + Flush, + WalletEnumerate, + WalletCreate, + WalletOpen, + WalletClose, + WalletRename, + WalletChangeSecret, + WalletExport, + WalletImport, + PrvKeyDataEnumerate, + PrvKeyDataCreate, + PrvKeyDataRemove, + PrvKeyDataGet, + AccountsRename, + AccountsEnumerate, + AccountsDiscovery, + AccountsCreate, + AccountsImport, + AccountsActivate, + AccountsDeactivate, + AccountsGet, + AccountsCreateNewAddress, + AccountsSend, + AccountsTransfer, + AccountsEstimate, + TransactionsDataGet, + TransactionsReplaceNote, + TransactionsReplaceMetadata, + AddressBookEnumerate, + ]} +} diff --git a/wallet/core/src/derivation/gen0/hd.rs b/wallet/core/src/derivation/gen0/hd.rs index 3d2186f0e..e750c8bf0 100644 --- a/wallet/core/src/derivation/gen0/hd.rs +++ b/wallet/core/src/derivation/gen0/hd.rs @@ -1,28 +1,17 @@ -// use futures::future::join_all; -use crate::imports::{AtomicBool, Ordering}; +use crate::derivation::traits::*; +use crate::imports::*; use hmac::Mac; use kash_addresses::{Address, Prefix as AddressPrefix, Version as AddressVersion}; -use ripemd::Ripemd160; -use sha2::{Digest, Sha256}; -use std::{ - collections::HashMap, - fmt::Debug, - str::FromStr, - sync::{Arc, Mutex, MutexGuard}, -}; -use zeroize::Zeroizing; - -use crate::derivation::traits::*; -use crate::result::Result; -use async_trait::async_trait; +use kash_bip32::types::{ChainCode, HmacSha512, KeyFingerprint, PublicKeyBytes, KEY_SIZE}; use kash_bip32::{ - types::*, AddressType, ChildNumber, DerivationPath, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, ExtendedPublicKey, Prefix, + AddressType, ChildNumber, DerivationPath, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, ExtendedPublicKey, Prefix, PrivateKey, PublicKey, SecretKey, SecretKeyExt, }; +use ripemd::Ripemd160; +use sha2::{Digest, Sha256}; +use std::fmt::Debug; use wasm_bindgen::prelude::*; -//pub const CACHE_LIMIT: u32 = 10_000; - fn get_fingerprint(private_key: &K) -> KeyFingerprint where K: PrivateKey, @@ -398,12 +387,12 @@ impl WalletDerivationManagerV0 { let digest = Ripemd160::digest(Sha256::digest(&public_key.to_bytes()[1..])); let fingerprint = digest[..4].try_into().expect("digest truncated"); - let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(Error::Hmac)?; + let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(kash_bip32::Error::Hmac)?; hmac.update(&public_key.to_bytes()); let (key, chain_code) = Self::derive_public_key_child(public_key, child_number, hmac)?; - let depth = attrs.depth.checked_add(1).ok_or(Error::Depth)?; + let depth = attrs.depth.checked_add(1).ok_or(kash_bip32::Error::Depth)?; let attrs = ExtendedKeyAttrs { parent_fingerprint: fingerprint, child_number, chain_code, depth }; @@ -458,7 +447,7 @@ impl WalletDerivationManagerV0 { let (private_key, chain_code) = Self::derive_key(private_key, child_number, hmac)?; - let depth = attrs.depth.checked_add(1).ok_or(Error::Depth)?; + let depth = attrs.depth.checked_add(1).ok_or(kash_bip32::Error::Depth)?; let attrs = ExtendedKeyAttrs { parent_fingerprint: fingerprint, child_number, chain_code, depth }; @@ -489,7 +478,7 @@ impl WalletDerivationManagerV0 { where K: PrivateKey, { - let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(Error::Hmac)?; + let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(kash_bip32::Error::Hmac)?; if hardened { hmac.update(&[0]); hmac.update(&private_key.to_bytes()); @@ -608,36 +597,14 @@ impl WalletDerivationManagerTrait for WalletDerivationManagerV0 { } fn from_extended_public_key_str(_xpub: &str, _cosigner_index: Option) -> Result { - // let extended_public_key = ExtendedPublicKey::::from_str(xpub)?; - // let wallet = Self::from_extended_public_key(extended_public_key, cosigner_index).await?; - // Ok(wallet) - todo!() + unreachable!(); } fn from_extended_public_key( _extended_public_key: ExtendedPublicKey, _cosigner_index: Option, ) -> Result { - // let receive_wallet = - // Self::derive_child_pubkey_manager(extended_public_key.clone(), AddressType::Receive, cosigner_index).await?; - - // println!("###: public_key {:?}", receive_wallet.public_key); - // println!("###: attrs {:?}", receive_wallet.attrs()); - // println!("###: fingerprint {:?}", receive_wallet.fingerprint); - // println!("###: hmac {:?}", receive_wallet.hmac); - - // let change_wallet = - // Self::derive_child_pubkey_manager(extended_public_key.clone(), AddressType::Change, cosigner_index).await?; - - // let wallet = Self { - // extended_public_key, - // receive_pubkey_manager: Arc::new(receive_wallet), - // change_pubkey_manager: Arc::new(change_wallet), - // }; - - // Ok(wallet) - - todo!() + unreachable!(); } fn receive_pubkey_manager(&self) -> Arc { diff --git a/wallet/core/src/derivation/gen1/hd.rs b/wallet/core/src/derivation/gen1/hd.rs index 9d3a937db..551000854 100644 --- a/wallet/core/src/derivation/gen1/hd.rs +++ b/wallet/core/src/derivation/gen1/hd.rs @@ -1,22 +1,15 @@ -// use futures::future::join_all; +use crate::derivation::traits::*; +use crate::imports::*; use hmac::Mac; use kash_addresses::{Address, Prefix as AddressPrefix, Version as AddressVersion}; -use ripemd::Ripemd160; -use sha2::{Digest, Sha256}; -use std::{ - fmt::Debug, - str::FromStr, - sync::{Arc, Mutex}, -}; -use zeroize::Zeroizing; - -use crate::derivation::traits::*; -use crate::result::Result; -use async_trait::async_trait; +use kash_bip32::types::{ChainCode, HmacSha512, KeyFingerprint, PublicKeyBytes, KEY_SIZE}; use kash_bip32::{ - types::*, AddressType, ChildNumber, DerivationPath, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, ExtendedPublicKey, Prefix, + AddressType, ChildNumber, DerivationPath, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, ExtendedPublicKey, Prefix, PrivateKey, PublicKey, SecretKey, SecretKeyExt, }; +use ripemd::Ripemd160; +use sha2::{Digest, Sha256}; +use std::fmt::Debug; use wasm_bindgen::prelude::*; fn get_fingerprint(private_key: &K) -> KeyFingerprint @@ -232,7 +225,7 @@ impl WalletDerivationManager { public_key = public_key.derive_child(ChildNumber::new(address_type.index(), false)?)?; - let mut hmac = HmacSha512::new_from_slice(&public_key.attrs().chain_code).map_err(Error::Hmac)?; + let mut hmac = HmacSha512::new_from_slice(&public_key.attrs().chain_code).map_err(kash_bip32::Error::Hmac)?; hmac.update(&public_key.to_bytes()); PubkeyDerivationManager::new(*public_key.public_key(), public_key.attrs().clone(), public_key.fingerprint(), hmac, 0) @@ -245,12 +238,12 @@ impl WalletDerivationManager { ) -> Result<(secp256k1::PublicKey, ExtendedKeyAttrs)> { let fingerprint = public_key.fingerprint(); - let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(Error::Hmac)?; + let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(kash_bip32::Error::Hmac)?; hmac.update(&public_key.to_bytes()); let (key, chain_code) = Self::derive_public_key_child(public_key, index, hmac)?; - let depth = attrs.depth.checked_add(1).ok_or(Error::Depth)?; + let depth = attrs.depth.checked_add(1).ok_or(kash_bip32::Error::Depth)?; let attrs = ExtendedKeyAttrs { parent_fingerprint: fingerprint, child_number: ChildNumber::new(index, false)?, chain_code, depth }; @@ -294,7 +287,7 @@ impl WalletDerivationManager { let (private_key, chain_code) = Self::derive_key(private_key, child_number, hmac)?; - let depth = attrs.depth.checked_add(1).ok_or(Error::Depth)?; + let depth = attrs.depth.checked_add(1).ok_or(kash_bip32::Error::Depth)?; let attrs = ExtendedKeyAttrs { parent_fingerprint: fingerprint, child_number, chain_code, depth }; @@ -325,7 +318,7 @@ impl WalletDerivationManager { where K: PrivateKey, { - let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(Error::Hmac)?; + let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(kash_bip32::Error::Hmac)?; if hardened { hmac.update(&[0]); hmac.update(&private_key.to_bytes()); diff --git a/wallet/core/src/derivation/gen1/import.rs b/wallet/core/src/derivation/gen1/import.rs index 362c3907b..cfc20484e 100644 --- a/wallet/core/src/derivation/gen1/import.rs +++ b/wallet/core/src/derivation/gen1/import.rs @@ -4,5 +4,4 @@ pub struct PrivateKeyDataV1; pub async fn load_v1_keydata(_phrase: &Secret) -> Result { unimplemented!() - // Ok(PrivateKeyDataV1) } diff --git a/wallet/core/src/derivation/mod.rs b/wallet/core/src/derivation/mod.rs index 97ad0890e..1b057f00d 100644 --- a/wallet/core/src/derivation/mod.rs +++ b/wallet/core/src/derivation/mod.rs @@ -1,34 +1,28 @@ +//! +//! Module handling bip32 address derivation (bip32+bip44 and legacy accounts) +//! + pub mod gen0; pub mod gen1; pub mod traits; pub use traits::*; +use crate::account::create_private_keys; +use crate::account::AccountKind; use crate::derivation::gen0::{PubkeyDerivationManagerV0, WalletDerivationManagerV0}; use crate::derivation::gen1::{PubkeyDerivationManager, WalletDerivationManager}; use crate::error::Error; use crate::imports::*; -use crate::runtime; -use crate::runtime::account::create_private_keys; -use crate::runtime::AccountKind; -use crate::Result; +use crate::result::Result; use kash_bip32::{AddressType, DerivationPath, ExtendedPrivateKey, ExtendedPublicKey, Language, Mnemonic, SecretKeyExt}; use kash_consensus_core::network::NetworkType; use kash_txscript::{extract_script_pub_key_address, multisig_redeem_script, multisig_redeem_script_ecdsa, pay_to_script_hash_script}; -use std::collections::HashMap; -use std::sync::{Arc, Mutex, MutexGuard}; -use wasm_bindgen::prelude::*; use workflow_wasm::serde::from_value; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct AddressDerivationMeta([u32; 2]); -impl Default for AddressDerivationMeta { - fn default() -> Self { - Self([1, 1]) - } -} - impl AddressDerivationMeta { pub fn new(receive: u32, change: u32) -> Self { Self([receive, change]) @@ -43,14 +37,19 @@ impl AddressDerivationMeta { } } +impl std::fmt::Display for AddressDerivationMeta { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}, {}]", self.receive(), self.change()) + } +} + pub struct Inner { pub index: u32, pub address_to_index_map: HashMap, } pub struct AddressManager { - // pub prefix: Prefix, - pub wallet: Arc, + pub wallet: Arc, pub account_kind: AccountKind, pub pubkey_managers: Vec>, pub ecdsa: bool, @@ -60,7 +59,7 @@ pub struct AddressManager { impl AddressManager { pub fn new( - wallet: Arc, + wallet: Arc, account_kind: AccountKind, pubkey_managers: Vec>, ecdsa: bool, @@ -178,16 +177,16 @@ pub struct AddressDerivationManager { pub cosigner_index: Option, pub derivators: Vec>, #[allow(dead_code)] - wallet: Arc, + wallet: Arc, pub receive_address_manager: Arc, pub change_address_manager: Arc, } impl AddressDerivationManager { pub async fn new( - wallet: &Arc, + wallet: &Arc, account_kind: AccountKind, - keys: &Vec, + keys: &ExtendedPublicKeys, ecdsa: bool, account_index: u64, cosigner_index: Option, @@ -201,14 +200,16 @@ impl AddressDerivationManager { let mut receive_pubkey_managers = vec![]; let mut change_pubkey_managers = vec![]; let mut derivators = vec![]; - for xpub in keys { - let derivator: Arc = match account_kind { - AccountKind::Legacy => Arc::new(gen0::WalletDerivationManagerV0::from_extended_public_key_str(xpub, cosigner_index)?), - AccountKind::MultiSig => { + for xpub in keys.iter() { + let derivator: Arc = match account_kind.as_ref() { + LEGACY_ACCOUNT_KIND => { + Arc::new(gen0::WalletDerivationManagerV0::from_extended_public_key(xpub.clone(), cosigner_index)?) + } + MULTISIG_ACCOUNT_KIND => { let cosigner_index = cosigner_index.ok_or(Error::InvalidAccountKind)?; - Arc::new(gen1::WalletDerivationManager::from_extended_public_key_str(xpub, Some(cosigner_index))?) + Arc::new(gen1::WalletDerivationManager::from_extended_public_key(xpub.clone(), Some(cosigner_index))?) } - _ => Arc::new(gen1::WalletDerivationManager::from_extended_public_key_str(xpub, cosigner_index)?), + _ => Arc::new(gen1::WalletDerivationManager::from_extended_public_key(xpub.clone(), cosigner_index)?), }; receive_pubkey_managers.push(derivator.receive_pubkey_manager()); @@ -248,10 +249,9 @@ impl AddressDerivationManager { } pub fn create_legacy_pubkey_managers( - wallet: &Arc, + wallet: &Arc, account_index: u64, address_derivation_indexes: AddressDerivationMeta, - _data: storage::account::Legacy, ) -> Result> { let mut receive_pubkey_managers = vec![]; let mut change_pubkey_managers = vec![]; @@ -260,7 +260,7 @@ impl AddressDerivationManager { receive_pubkey_managers.push(derivator.receive_pubkey_manager()); change_pubkey_managers.push(derivator.change_pubkey_manager()); - let account_kind = AccountKind::Legacy; + let account_kind = AccountKind::from(LEGACY_ACCOUNT_KIND); let receive_address_manager = AddressManager::new( wallet.clone(), @@ -332,7 +332,7 @@ impl AddressDerivationManager { let (receive, change) = if change_address { (vec![], addresses) } else { (addresses, vec![]) }; let private_keys = - create_private_keys(self.account_kind, self.cosigner_index.unwrap_or(0), self.account_index, xkey, &receive, &change)?; + create_private_keys(&self.account_kind, self.cosigner_index.unwrap_or(0), self.account_index, xkey, &receive, &change)?; let mut result = vec![]; for (address, private_key) in private_keys { @@ -502,7 +502,7 @@ pub fn create_address( return create_multisig_address(minimum_signatures, keys, prefix, ecdsa); } - if matches!(account_kind, Some(AccountKind::Legacy)) { + if account_kind.map(|kind| kind == LEGACY_ACCOUNT_KIND).unwrap_or(false) { PubkeyDerivationManagerV0::create_address(&keys[0], prefix, ecdsa) } else { PubkeyDerivationManager::create_address(&keys[0], prefix, ecdsa) @@ -518,9 +518,9 @@ pub async fn create_xpub_from_mnemonic( let seed = mnemonic.to_seed(""); let xkey = ExtendedPrivateKey::::new(seed)?; - let (secret_key, attrs) = match account_kind { - AccountKind::Legacy => WalletDerivationManagerV0::derive_extended_key_from_master_key(xkey, true, account_index)?, - AccountKind::MultiSig => WalletDerivationManager::derive_extended_key_from_master_key(xkey, true, account_index)?, + let (secret_key, attrs) = match account_kind.as_ref() { + LEGACY_ACCOUNT_KIND => WalletDerivationManagerV0::derive_extended_key_from_master_key(xkey, false, account_index)?, + MULTISIG_ACCOUNT_KIND => WalletDerivationManager::derive_extended_key_from_master_key(xkey, true, account_index)?, _ => gen1::WalletDerivationManager::derive_extended_key_from_master_key(xkey, false, account_index)?, }; @@ -534,10 +534,10 @@ pub async fn create_xpub_from_xprv( account_kind: AccountKind, account_index: u64, ) -> Result> { - let (secret_key, attrs) = match account_kind { - AccountKind::Legacy => WalletDerivationManagerV0::derive_extended_key_from_master_key(xprv, true, account_index)?, - AccountKind::MultiSig => WalletDerivationManager::derive_extended_key_from_master_key(xprv, true, account_index)?, - AccountKind::Bip32 => WalletDerivationManager::derive_extended_key_from_master_key(xprv, false, account_index)?, + let (secret_key, attrs) = match account_kind.as_ref() { + LEGACY_ACCOUNT_KIND => WalletDerivationManagerV0::derive_extended_key_from_master_key(xprv, false, account_index)?, + MULTISIG_ACCOUNT_KIND => WalletDerivationManager::derive_extended_key_from_master_key(xprv, true, account_index)?, + BIP32_ACCOUNT_KIND => WalletDerivationManager::derive_extended_key_from_master_key(xprv, false, account_index)?, _ => panic!("create_xpub_from_xprv not supported for account kind: {:?}", account_kind), }; @@ -547,15 +547,15 @@ pub async fn create_xpub_from_xprv( } pub fn build_derivate_path( - account_kind: AccountKind, + account_kind: &AccountKind, account_index: u64, cosigner_index: u32, address_type: AddressType, ) -> Result { - match account_kind { - AccountKind::Legacy => WalletDerivationManagerV0::build_derivate_path(account_index, Some(address_type)), - AccountKind::Bip32 => WalletDerivationManager::build_derivate_path(false, account_index, None, Some(address_type)), - AccountKind::MultiSig => { + match account_kind.as_ref() { + LEGACY_ACCOUNT_KIND => WalletDerivationManagerV0::build_derivate_path(account_index, Some(address_type)), + BIP32_ACCOUNT_KIND => WalletDerivationManager::build_derivate_path(false, account_index, None, Some(address_type)), + MULTISIG_ACCOUNT_KIND => { WalletDerivationManager::build_derivate_path(true, account_index, Some(cosigner_index), Some(address_type)) } _ => { @@ -565,7 +565,7 @@ pub fn build_derivate_path( } pub fn build_derivate_paths( - account_kind: AccountKind, + account_kind: &AccountKind, account_index: u64, cosigner_index: u32, ) -> Result<(DerivationPath, DerivationPath)> { diff --git a/wallet/core/src/derivation/traits.rs b/wallet/core/src/derivation/traits.rs index c48a3b628..cd01bf8ec 100644 --- a/wallet/core/src/derivation/traits.rs +++ b/wallet/core/src/derivation/traits.rs @@ -1,4 +1,8 @@ -use crate::Result; +//! +//! Traits for derivation managers. +//! + +use crate::result::Result; use async_trait::async_trait; use kash_bip32::ExtendedPublicKey; use std::{collections::HashMap, sync::Arc}; diff --git a/wallet/core/src/deterministic.rs b/wallet/core/src/deterministic.rs new file mode 100644 index 000000000..775486fa6 --- /dev/null +++ b/wallet/core/src/deterministic.rs @@ -0,0 +1,159 @@ +//! +//! Deterministic byte sequence generation (used by Account ids). +//! + +pub use crate::account::{bip32, keypair, legacy, multisig}; +use crate::encryption::sha256_hash; +use crate::imports::*; +use crate::storage::PrvKeyDataId; +use kash_hashes::Hash; +use kash_utils::as_slice::AsSlice; +use secp256k1::PublicKey; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct AccountStorageKey(pub(crate) Hash); + +impl AccountStorageKey { + pub fn short(&self) -> String { + let hex = self.to_hex(); + format!("[{}]", &hex[0..8]) + } +} + +impl ToHex for AccountStorageKey { + fn to_hex(&self) -> String { + format!("{}", self.0) + } +} + +impl std::fmt::Display for AccountStorageKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct AccountId(pub(crate) Hash); + +impl AccountId { + pub fn short(&self) -> String { + let hex = self.to_hex(); + format!("[{}]", &hex[0..8]) + } +} + +impl ToHex for AccountId { + fn to_hex(&self) -> String { + format!("{}", self.0) + } +} + +impl std::fmt::Display for AccountId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +seal! { 0xa7a4, { + // IMPORTANT: This data structure is meant to be deterministic + // so it can not contain any new fields or be changed. + #[derive(BorshSerialize)] + struct DeterministicHashData<'data, T: AsSlice> { + account_kind: &'data AccountKind, + prv_key_data_ids: &'data Option, + ecdsa: Option, + account_index: Option, + secp256k1_public_key: Option>, + data: Option>, + } +}} + +#[inline(always)] +pub(crate) fn make_account_hashes(v: [Hash; 2]) -> (AccountId, AccountStorageKey) { + (AccountId(v[1]), AccountStorageKey(v[0])) +} + +fn make_hashes(hashable: DeterministicHashData) -> [Hash; N] +where + T: AsSlice + BorshSerialize, +{ + let mut hashes: [Hash; N] = [Hash::default(); N]; + let bytes = hashable.try_to_vec().unwrap(); + hashes[0] = Hash::from_slice(sha256_hash(&bytes).as_ref()); + for i in 1..N { + hashes[i] = Hash::from_slice(sha256_hash(&hashes[i - 1].as_bytes()).as_ref()); + } + hashes +} + +pub fn from_bip32(prv_key_data_id: &PrvKeyDataId, data: &bip32::Payload) -> [Hash; N] { + let hashable = DeterministicHashData { + account_kind: &bip32::BIP32_ACCOUNT_KIND.into(), + prv_key_data_ids: &Some([*prv_key_data_id]), + ecdsa: Some(data.ecdsa), + account_index: Some(data.account_index), + secp256k1_public_key: None, + data: None, + }; + make_hashes(hashable) +} + +pub fn from_legacy(prv_key_data_id: &PrvKeyDataId, _data: &legacy::Payload) -> [Hash; N] { + let hashable = DeterministicHashData { + account_kind: &legacy::LEGACY_ACCOUNT_KIND.into(), + prv_key_data_ids: &Some([*prv_key_data_id]), + ecdsa: Some(false), + account_index: Some(0), + secp256k1_public_key: None, + data: None, + }; + make_hashes(hashable) +} + +pub fn from_multisig(prv_key_data_ids: &Option>>, data: &multisig::Payload) -> [Hash; N] { + let hashable = DeterministicHashData { + account_kind: &multisig::MULTISIG_ACCOUNT_KIND.into(), + prv_key_data_ids, + ecdsa: Some(data.ecdsa), + account_index: None, + secp256k1_public_key: None, + data: Some(data.xpub_keys.try_to_vec().unwrap()), + }; + make_hashes(hashable) +} + +pub(crate) fn from_keypair(prv_key_data_id: &PrvKeyDataId, data: &keypair::Payload) -> [Hash; N] { + let hashable = DeterministicHashData { + account_kind: &keypair::KEYPAIR_ACCOUNT_KIND.into(), + prv_key_data_ids: &Some([*prv_key_data_id]), + ecdsa: Some(data.ecdsa), + account_index: None, + secp256k1_public_key: Some(data.public_key.serialize().to_vec()), + data: None, + }; + make_hashes(hashable) +} + +pub fn from_public_key(account_kind: &AccountKind, public_key: &PublicKey) -> [Hash; N] { + let hashable: DeterministicHashData<[PrvKeyDataId; 0]> = DeterministicHashData { + account_kind, + prv_key_data_ids: &None, + ecdsa: None, + account_index: None, + secp256k1_public_key: Some(public_key.serialize().to_vec()), + data: None, + }; + make_hashes(hashable) +} + +pub fn from_data(account_kind: &AccountKind, data: &[u8]) -> [Hash; N] { + let hashable: DeterministicHashData<[PrvKeyDataId; 0]> = DeterministicHashData { + account_kind, + prv_key_data_ids: &None, + ecdsa: None, + account_index: None, + secp256k1_public_key: None, + data: Some(data.to_vec()), + }; + make_hashes(hashable) +} diff --git a/wallet/core/src/encryption.rs b/wallet/core/src/encryption.rs index 8c08640fc..163d0ee97 100644 --- a/wallet/core/src/encryption.rs +++ b/wallet/core/src/encryption.rs @@ -1,3 +1,7 @@ +//! +//! Wallet data encryption module. +//! + use crate::imports::*; use crate::result::Result; use crate::secret::Secret; @@ -7,13 +11,16 @@ use chacha20poly1305::{ aead::{AeadCore, AeadInPlace, KeyInit, OsRng}, Key, XChaCha20Poly1305, }; -use faster_hex::{hex_decode, hex_string}; -use serde::{de::DeserializeOwned, Serializer}; use sha2::{Digest, Sha256}; use std::ops::{Deref, DerefMut}; use zeroize::Zeroize; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum EncryptionKind { + XChaCha20Poly1305, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(tag = "encryptable", content = "payload")] pub enum Encryptable { #[serde(rename = "plain")] @@ -36,7 +43,7 @@ where impl Encryptable where - T: Clone + Serialize + DeserializeOwned + Zeroize, + T: Clone + Zeroize + BorshDeserialize + BorshSerialize, { pub fn is_encrypted(&self) -> bool { !matches!(self, Self::Plain(_)) @@ -49,22 +56,24 @@ where if let Some(secret) = secret { Ok(v.decrypt(secret)?) } else { - Err("decrypted() secret is 'None' when the data is encryted!".into()) + Err("Decryption secret is 'None' when the data is encrypted!".into()) } } } } - pub fn encrypt(&self, secret: &Secret) -> Result { + pub fn encrypt(&self, secret: &Secret, encryption_kind: EncryptionKind) -> Result { match self { - Self::Plain(v) => Ok(Decrypted::new(v.clone()).encrypt(secret)?), - Self::XChaCha20Poly1305(v) => Ok(v.clone()), + Self::Plain(v) => Ok(Decrypted::new(v.clone()).encrypt(secret, encryption_kind)?), + Self::XChaCha20Poly1305(v) => match encryption_kind { + EncryptionKind::XChaCha20Poly1305 => Ok(v.clone()), + }, } } - pub fn into_encrypted(&self, secret: &Secret) -> Result { + pub fn into_encrypted(&self, secret: &Secret, encryption_kind: EncryptionKind) -> Result { match self { - Self::Plain(v) => Ok(Self::XChaCha20Poly1305(Decrypted::new(v.clone()).encrypt(secret)?)), + Self::Plain(v) => Ok(Self::XChaCha20Poly1305(Decrypted::new(v.clone()).encrypt(secret, encryption_kind)?)), Self::XChaCha20Poly1305(v) => Ok(Self::XChaCha20Poly1305(v.clone())), } } @@ -72,7 +81,7 @@ where pub fn into_decrypted(self, secret: &Secret) -> Result { match self { Self::Plain(v) => Ok(Self::Plain(v)), - Self::XChaCha20Poly1305(v) => Ok(Self::Plain(v.decrypt::(secret)?.clone())), + Self::XChaCha20Poly1305(v) => Ok(Self::Plain(v.decrypt::(secret)?.unwrap())), } } } @@ -83,28 +92,43 @@ impl From for Encryptable { } } -pub struct Decrypted(pub(crate) T); +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] +pub struct Decrypted(pub(crate) T) +where + T: BorshSerialize + BorshDeserialize; -impl AsRef for Decrypted { +impl AsRef for Decrypted +where + T: BorshSerialize + BorshDeserialize, +{ fn as_ref(&self) -> &T { &self.0 } } -impl Deref for Decrypted { +impl Deref for Decrypted +where + T: BorshSerialize + BorshDeserialize, +{ type Target = T; fn deref(&self) -> &T { &self.0 } } -impl DerefMut for Decrypted { +impl DerefMut for Decrypted +where + T: BorshSerialize + BorshDeserialize, +{ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl AsMut for Decrypted { +impl AsMut for Decrypted +where + T: BorshSerialize + BorshDeserialize, +{ fn as_mut(&mut self) -> &mut T { &mut self.0 } @@ -112,21 +136,28 @@ impl AsMut for Decrypted { impl Decrypted where - T: Serialize, + T: BorshSerialize + BorshDeserialize, { pub fn new(value: T) -> Self { Self(value) } - pub fn encrypt(&self, secret: &Secret) -> Result { - let json = serde_json::to_string(&self.0)?; - let encrypted = encrypt_xchacha20poly1305(json.as_bytes(), secret)?; - Ok(Encrypted::new(encrypted)) + pub fn encrypt(&self, secret: &Secret, encryption_kind: EncryptionKind) -> Result { + let bytes = self.0.try_to_vec()?; + let encrypted = match encryption_kind { + EncryptionKind::XChaCha20Poly1305 => encrypt_xchacha20poly1305(bytes.as_slice(), secret)?, + }; + Ok(Encrypted::new(encryption_kind, encrypted)) + } + + pub fn unwrap(self) -> T { + self.0 } } -#[derive(Debug, Clone, Default)] +#[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct Encrypted { + encryption_kind: EncryptionKind, payload: Vec, } @@ -136,42 +167,35 @@ impl Zeroize for Encrypted { } } +impl std::fmt::Debug for Encrypted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Encrypted").field("encryption_kind", &self.encryption_kind).field("payload", &self.payload.to_hex()).finish() + } +} + impl Encrypted { - pub fn new(payload: Vec) -> Self { - Encrypted { payload } + pub fn new(encryption_kind: EncryptionKind, payload: Vec) -> Self { + Encrypted { encryption_kind, payload } } pub fn replace(&mut self, from: Encrypted) { self.payload = from.payload; } - pub fn decrypt(&self, secret: &Secret) -> Result> - where - T: DeserializeOwned, - { - let t: T = serde_json::from_slice(decrypt_xchacha20poly1305(&self.payload, secret)?.as_ref())?; - Ok(Decrypted(t)) - } -} - -impl Serialize for Encrypted { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&hex_string(&self.payload)) + pub fn kind(&self) -> EncryptionKind { + self.encryption_kind } -} -impl<'de> Deserialize<'de> for Encrypted { - fn deserialize(deserializer: D) -> Result + pub fn decrypt(&self, secret: &Secret) -> Result> where - D: Deserializer<'de>, + T: BorshSerialize + BorshDeserialize, { - let s = ::deserialize(deserializer)?; - let mut data = vec![0u8; s.len() / 2]; - hex_decode(s.as_bytes(), &mut data).map_err(serde::de::Error::custom)?; - Ok(Self::new(data)) + match self.encryption_kind { + EncryptionKind::XChaCha20Poly1305 => { + let decrypted = decrypt_xchacha20poly1305(&self.payload, secret)?; + Ok(Decrypted(T::try_from_slice(decrypted.as_ref())?)) + } + } } } @@ -197,13 +221,13 @@ pub fn js_argon2_sha256iv_phash(data: JsValue, byte_length: usize) -> Result Secret { - let mut sha256 = Sha256::new(); + let mut sha256 = Sha256::default(); sha256.update(data); Secret::new(sha256.finalize().to_vec()) } pub fn sha256d_hash(data: &[u8]) -> Secret { - let mut sha256 = Sha256::new(); + let mut sha256 = Sha256::default(); sha256.update(data); sha256_hash(sha256.finalize().as_slice()) } diff --git a/wallet/core/src/error.rs b/wallet/core/src/error.rs index 25f746ba8..0d4ce5ce0 100644 --- a/wallet/core/src/error.rs +++ b/wallet/core/src/error.rs @@ -1,10 +1,17 @@ +//! +//! Error types used by the wallet framework. +//! + +use crate::imports::{AccountId, AccountKind, AssocPrvKeyDataIds, PrvKeyDataId}; use base64::DecodeError; use downcast::DowncastError; use kash_bip32::Error as BIP32Error; +use kash_consensus_core::asset_type::AssetType; use kash_consensus_core::sign::Error as CoreSignError; use kash_rpc_core::RpcError as KashRpcError; use kash_wrpc_client::error::Error as KashWorkflowRpcError; use std::sync::PoisonError; +use thiserror::Error; use wasm_bindgen::JsValue; use workflow_core::abortable::Aborted; use workflow_core::sendable::*; @@ -12,9 +19,6 @@ use workflow_rpc::client::error::Error as RpcError; use workflow_wasm::jserror::*; use workflow_wasm::printable::*; -use kash_consensus_core::asset_type::AssetType; -use thiserror::Error; - #[derive(Debug, Error)] pub enum Error { #[error("{0}")] @@ -32,6 +36,9 @@ pub enum Error { #[error("Wallet wRPC -> {0}")] KashWorkflowRpcError(#[from] KashWorkflowRpcError), + #[error("The wallet RPC client is not wRPC")] + NotWrpcClient, + #[error("Bip32 -> {0}")] BIP32Error(#[from] BIP32Error), @@ -98,7 +105,7 @@ pub enum Error { #[error("Invalid filename: {0}")] InvalidFilename(String), - #[error("(I/O) {0}")] + #[error("{0}")] Io(#[from] std::io::Error), #[error("{0}")] @@ -144,10 +151,13 @@ pub enum Error { VarError(#[from] std::env::VarError), #[error("private key {0} not found")] - PrivateKeyNotFound(String), + PrivateKeyNotFound(PrvKeyDataId), #[error("private key {0} already exists")] - PrivateKeyAlreadyExists(String), + PrivateKeyAlreadyExists(PrvKeyDataId), + + #[error("account {0} already exists")] + AccountAlreadyExists(AccountId), #[error("xprv key is not supported for this key type")] XPrvSupport, @@ -164,6 +174,15 @@ pub enum Error { #[error("{0}")] TryFromEnum(#[from] workflow_core::enums::TryFromError), + #[error("Account factory found for type: {0}")] + AccountFactoryNotFound(AccountKind), + + #[error("Account not found: {0}")] + AccountNotFound(AccountId), + + #[error("Account not active: {0}")] + AccountNotActive(AccountId), + #[error("Invalid account type (must be one of: bip32|multisig|legacy")] InvalidAccountKind, @@ -188,6 +207,9 @@ pub enum Error { #[error("The feature is not supported")] NotImplemented, + #[error("Not allowed on a resident wallet")] + ResidentWallet, + #[error("Not allowed on a resident account")] ResidentAccount, @@ -218,6 +240,9 @@ pub enum Error { #[error("Requested transaction is too heavy")] GeneratorTransactionIsTooHeavy, + #[error("Invalid range {0}..{1}")] + InvalidRange(u64, u64), + #[error(transparent)] MultisigCreateError(#[from] kash_txscript::MultisigCreateError), @@ -232,6 +257,24 @@ pub enum Error { #[error("UTXOs asset type mismatch: expected {expected}, found {found}")] MismatchedAssetType { expected: AssetType, found: AssetType }, + + #[error("Legacy account is not initialized")] + LegacyAccountNotInitialized, + + #[error("AssocPrvKeyDataIds required {0} but got {1:?}")] + AssocPrvKeyDataIds(String, AssocPrvKeyDataIds), + + #[error("AssocPrvKeyDataIds are empty")] + AssocPrvKeyDataIdsEmpty, + + #[error("Invalid extended public key '{0}': {1}")] + InvalidExtendedPublicKey(String, BIP32Error), + + #[error("Missing DAA score while processing '{0}' (this may be a node connection issue)")] + MissingDaaScore(&'static str), + + #[error("Missing RPC listener id (this may be a node connection issue)")] + ListenerId, } impl From for Error { @@ -309,14 +352,14 @@ impl From for Error { } } -// impl From for Error { -// fn from(err: workflow_wasm::serde::Error) -> Self { -// Self::ToValue(err.to_string()) -// } -// } - impl From> for Error { fn from(e: DowncastError) -> Self { Error::DowncastError(e.to_string()) } } + +impl From> for Error { + fn from(e: workflow_core::channel::SendError) -> Self { + Error::Custom(e.to_string()) + } +} diff --git a/wallet/core/src/events.rs b/wallet/core/src/events.rs index 6489c26de..2191f8329 100644 --- a/wallet/core/src/events.rs +++ b/wallet/core/src/events.rs @@ -1,7 +1,11 @@ +//! +//! Events emitted by the wallet framework. This includes various wallet, +//! account and transaction events as well as state and sync events +//! produced by the client RPC and the Kash node monitoring subsystems. +//! + use crate::imports::*; -use crate::runtime::Balance; -use crate::storage::Hint; -use crate::storage::TransactionRecord; +use crate::storage::{Hint, PrvKeyDataInfo, StorageDescriptor, TransactionRecord, WalletDescriptor}; use crate::utxo::context::UtxoContextId; /// Sync state of the kashd node @@ -71,20 +75,58 @@ pub enum Events { }, /// [`SyncState`] notification posted /// when the node sync state changes - SyncState { sync_state: SyncState }, + SyncState { + sync_state: SyncState, + }, /// Emitted after the wallet has loaded and /// contains anti-phishing 'hint' set by the user. - WalletHint { hint: Option }, + WalletHint { + hint: Option, + }, /// Wallet has opened - WalletOpen, - /// Wallet open failure - WalletError { message: String }, + WalletOpen { + wallet_descriptor: Option, + account_descriptors: Option>, + }, + WalletCreate { + wallet_descriptor: WalletDescriptor, + storage_descriptor: StorageDescriptor, + }, /// Wallet reload initiated (development only) - WalletReload, + WalletReload { + wallet_descriptor: Option, + account_descriptors: Option>, + }, + /// Wallet open failure + WalletError { + message: String, + }, /// Wallet has been closed WalletClose, + PrvKeyDataCreate { + prv_key_data_info: PrvKeyDataInfo, + }, + /// Accounts have been activated + AccountActivation { + ids: Vec, + }, + /// Accounts have been deactivated + AccountDeactivation { + ids: Vec, + }, /// Account selection change (`None` if no account is selected) - AccountSelection { id: Option }, + AccountSelection { + id: Option, + }, + /// Account has been created + AccountCreate { + account_descriptor: AccountDescriptor, + }, + /// Account has been changed + /// (emitted on new address generation) + AccountUpdate { + account_descriptor: AccountDescriptor, + }, /// Emitted after successful RPC connection /// after the initial state negotiation. ServerStatus { @@ -99,53 +141,78 @@ pub enum Events { url: Option, }, - /// Successful start of [`UtxoProcessor`](crate::utxo::processor::UtxoProcessor). + /// Successful start of [`UtxoProcessor`]. /// This event signifies that the application can /// start interfacing with the UTXO processor. UtxoProcStart, - /// [`UtxoProcessor`](crate::utxo::processor::UtxoProcessor) has shut down. + /// [`UtxoProcessor`] has shut down. UtxoProcStop, /// Occurs when UtxoProcessor has failed to connect to the node - /// for an unknown reason. (general error trap) - UtxoProcError { message: String }, + /// for an unknown reason. Can also occur during general unexpected + /// UtxoProcessor processing errors, such as node disconnection + /// then submitting an outgoing transaction. This is a general + /// error trap for logging purposes and is safe to ignore. + UtxoProcError { + message: String, + }, /// DAA score change - DAAScoreChange { current_daa_score: u64 }, + DAAScoreChange { + current_daa_score: u64, + }, /// New incoming pending UTXO/transaction Pending { record: TransactionRecord, - /// `true` if the transaction is a result of an earlier - /// created outgoing transaction. (such as a UTXO returning - /// change to the account) - is_outgoing: bool, }, /// Pending UTXO has been removed (reorg) - Reorg { record: TransactionRecord }, - /// UtxoProcessor has received a foreign unknown transaction - /// withdrawing funds from the wallet. This occurs when another - /// instance of the wallet creates an outgoing transaction. - External { record: TransactionRecord }, + Reorg { + record: TransactionRecord, + }, + /// Coinbase stasis UTXO has been removed (reorg) + /// NOTE: These transactions should be ignored by clients. + Stasis { + record: TransactionRecord, + }, /// Transaction has been confirmed Maturity { record: TransactionRecord, - /// `true` if the transaction is a result of an earlier - /// created outgoing transaction. (such as a UTXO returning - /// change to the account) - is_outgoing: bool, - }, - /// Emitted when a transaction has been created and broadcasted - /// by the Transaction [`Generator`](crate::tx::generator::Generator) - Outgoing { record: TransactionRecord }, + }, + /// Emitted when a transaction has been discovered + /// during the UTXO scan. This event is generated + /// when a runtime [`Account`] + /// initiates address monitoring and performs + /// an initial scan of the UTXO set. + /// + /// This event is emitted when UTXOs are + /// registered with the UtxoContext using the + /// [`UtxoContext::extend_from_scan()`](UtxoContext::extend_from_scan) method. + /// + /// NOTE: if using runtime [`Wallet`], + /// the wallet will not emit this event if it detects + /// that the transaction already exist in its transaction + /// record set. If it doesn't, the wallet will create + /// such record and emit this event only once. These + /// transactions will be subsequently available when + /// accessing the wallet's transaction record set. + /// (i.e. when using runtime Wallet, this event can be + /// ignored and transaction record can be accessed from + /// the transaction history instead). + Discovery { + record: TransactionRecord, + }, /// UtxoContext (Account) balance update. Emitted for each /// balance change within the UtxoContext. Balance { - #[serde(rename = "matureUtxoSize")] - mature_utxo_size: usize, - #[serde(rename = "pendingUtxoSize")] - pending_utxo_size: usize, + // #[serde(rename = "matureUtxoSize")] + // mature_utxo_size: usize, + // #[serde(rename = "pendingUtxoSize")] + // pending_utxo_size: usize, balance: Option, /// If UtxoContext is bound to a Runtime Account, this /// field will contain the account id. Otherwise, it will /// contain a developer-assigned internal id. id: UtxoContextId, }, + Error { + message: String, + }, } diff --git a/wallet/core/src/factory.rs b/wallet/core/src/factory.rs new file mode 100644 index 000000000..c0472059c --- /dev/null +++ b/wallet/core/src/factory.rs @@ -0,0 +1,59 @@ +//! +//! Wallet Account factories (Account type registration and creation). +//! + +use crate::imports::*; +use crate::result::Result; +use std::sync::OnceLock; + +#[async_trait] +pub trait Factory { + fn name(&self) -> String; + fn description(&self) -> String; + async fn try_load( + &self, + wallet: &Arc, + storage: &AccountStorage, + meta: Option>, + ) -> Result>; +} + +type FactoryMap = AHashMap>; +static EXTERNAL: OnceLock> = OnceLock::new(); +static INITIALIZED: AtomicBool = AtomicBool::new(false); + +pub fn factories() -> &'static FactoryMap { + static FACTORIES: OnceLock = OnceLock::new(); + FACTORIES.get_or_init(|| { + INITIALIZED.store(true, Ordering::Relaxed); + + let factories: &[(AccountKind, Arc)] = &[ + (BIP32_ACCOUNT_KIND.into(), Arc::new(bip32::Ctor {})), + (LEGACY_ACCOUNT_KIND.into(), Arc::new(legacy::Ctor {})), + (MULTISIG_ACCOUNT_KIND.into(), Arc::new(multisig::Ctor {})), + (KEYPAIR_ACCOUNT_KIND.into(), Arc::new(keypair::Ctor {})), + ]; + + let external = EXTERNAL.get_or_init(|| Mutex::new(AHashMap::new())).lock().unwrap().clone(); + + AHashMap::from_iter(factories.iter().map(|(k, v)| (*k, v.clone())).chain(external)) + }) +} + +pub fn register(kind: AccountKind, factory: Arc) { + if INITIALIZED.load(Ordering::Relaxed) { + panic!("Factory registrations must occur before the framework initialization"); + } + let external = EXTERNAL.get_or_init(|| Mutex::new(AHashMap::new())); + external.lock().unwrap().insert(kind, factory); +} + +pub(crate) async fn try_load_account( + wallet: &Arc, + storage: Arc, + meta: Option>, +) -> Result> { + let factory = factories().get(&storage.kind).ok_or_else(|| Error::AccountFactoryNotFound(storage.kind))?; + + factory.try_load(wallet, &storage, meta).await +} diff --git a/wallet/core/src/imports.rs b/wallet/core/src/imports.rs index 01660cb93..d847c3c2c 100644 --- a/wallet/core/src/imports.rs +++ b/wallet/core/src/imports.rs @@ -1,16 +1,37 @@ +//! +//! This file contains most common imports that +//! are used internally in the wallet framework core. +//! + +pub use crate::account::descriptor::{AccountDescriptor, AccountDescriptorProperty, AccountDescriptorValue}; +pub use crate::account::variants::*; +pub use crate::account::{Account, AccountKind, DerivationCapableAccount}; +pub use crate::deterministic::*; +pub use crate::encryption::{Encryptable, EncryptionKind}; pub use crate::error::Error; pub use crate::events::{Events, SyncState}; +pub use crate::factory::{factories, Factory}; +pub use crate::result::Result; pub use crate::rpc::Rpc; pub use crate::rpc::{DynRpcApi, RpcCtl}; +pub use crate::secret::Secret; +pub use crate::serializer::*; +pub use crate::storage::*; +pub use crate::types::*; +pub use crate::utxo::balance::Balance; pub use crate::utxo::scan::{Scan, ScanExtent}; -pub use crate::{runtime, storage, utils, utxo}; +pub use crate::utxo::{Maturity, OutgoingTransaction, UtxoContext, UtxoEntryReference, UtxoProcessor}; +pub use crate::wallet::*; +pub use crate::{storage, utils, utxo}; + +pub use ahash::{AHashMap, AHashSet}; pub use async_trait::async_trait; pub use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; pub use cfg_if::cfg_if; pub use dashmap::{DashMap, DashSet}; pub use downcast::{downcast_sync, AnySync}; pub use futures::future::join_all; -pub use futures::{select, stream, FutureExt, Stream, StreamExt, TryStreamExt}; +pub use futures::{select, select_biased, stream, FutureExt, Stream, StreamExt, TryStreamExt}; pub use js_sys::{Array, BigInt, Object}; pub use kash_addresses::{Address, Prefix}; pub use kash_consensus_core::network::{NetworkId, NetworkType}; @@ -21,16 +42,19 @@ pub use kash_consensus_core::tx::{ScriptPublicKey, TransactionId, TransactionInd pub use kash_utils::hashmap::*; pub use kash_utils::hex::{FromHex, ToHex}; pub use pad::PadStr; +pub use separator::Separatable; pub use serde::{Deserialize, Deserializer, Serialize}; pub use std::collections::{HashMap, HashSet}; pub use std::pin::Pin; pub use std::str::FromStr; -pub use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -pub use std::sync::{Arc, Mutex, MutexGuard}; +pub use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +pub use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; pub use std::task::{Context, Poll}; pub use wasm_bindgen::prelude::*; pub use workflow_core::prelude::*; +pub use workflow_core::seal; pub use workflow_log::prelude::*; pub use workflow_wasm::prelude::*; pub use workflow_wasm::stream::AsyncStream; pub use xxhash_rust::xxh3::xxh3_64; +pub use zeroize::*; diff --git a/wallet/core/src/lib.rs b/wallet/core/src/lib.rs index 665cab4fd..296e181c2 100644 --- a/wallet/core/src/lib.rs +++ b/wallet/core/src/lib.rs @@ -1,31 +1,69 @@ +//! +//! Kash Wallet Core - Multi-platform Rust framework for Kash Wallet. +//! +//! This framework provides a series of APIs and primitives +//! to simplify building applications that interface with +//! the Kash p2p network. +//! +//! Included are low-level primitives +//! such as [`UtxoProcessor`](crate::utxo::UtxoProcessor) +//! and [`UtxoContext`](crate::utxo::UtxoContext) that provide +//! various levels of automation as well as higher-level +//! APIs such as [`Wallet`](crate::wallet::Wallet), +//! [`Account`](crate::account::Account) (managed via +//! [`WalletApi`](crate::api::WalletApi) trait) +//! that offer a fully-featured wallet implementation +//! backed by a multi-platform data storage layer capable of +//! storing wallet data on a local file-system as well as +//! within the browser environment. +//! +//! The wallet framework also includes transaction +//! [`Generator`](crate::tx::generator::Generator) +//! that can be used to generate transactions from a set of +//! UTXO entries. The generator can be used to create +//! simple transactions as well as batch transactions +//! comprised of multiple chained transactions. Batch +//! transactions (also known as compound transactions) +//! are needed when the total number of inputs required +//! to satisfy the requested amount exceeds the maximum +//! allowed transaction mass. +//! +//! The framework can operate +//! within native Rust applications as well as within the NodeJS +//! and browser environments via WASM32. +//! + extern crate alloc; extern crate self as kash_wallet_core; +pub mod account; +pub mod api; pub mod derivation; +pub mod deterministic; pub mod encryption; pub mod error; pub mod events; +pub mod factory; mod imports; pub mod message; +pub mod prelude; pub mod result; pub mod rpc; -pub mod runtime; pub mod secret; +pub mod serializer; pub mod settings; pub mod storage; pub mod tx; +pub mod types; pub mod utils; pub mod utxo; +pub mod wallet; pub mod wasm; -pub use derivation::{AddressDerivationManager, AddressDerivationManagerTrait}; -pub use events::{Events, SyncState}; -pub use kash_addresses::{Address, Prefix as AddressPrefix}; -pub use kash_wrpc_client::client::{ConnectOptions, ConnectStrategy}; -pub use result::Result; -pub use settings::{DefaultSettings, SettingsStore, SettingsStoreT, WalletSettings}; - /// Returns the version of the Wallet framework. pub fn version() -> String { env!("CARGO_PKG_VERSION").to_string() } + +#[cfg(test)] +pub mod tests; diff --git a/wallet/core/src/message.rs b/wallet/core/src/message.rs index a7d16e838..9de75c20a 100644 --- a/wallet/core/src/message.rs +++ b/wallet/core/src/message.rs @@ -1,3 +1,7 @@ +//! +//! Message signing and verification functions. +//! + use kash_hashes::{Hash, PersonalMessageSigningHash}; use secp256k1::{Error, XOnlyPublicKey}; diff --git a/wallet/core/src/prelude.rs b/wallet/core/src/prelude.rs new file mode 100644 index 000000000..efeabc142 --- /dev/null +++ b/wallet/core/src/prelude.rs @@ -0,0 +1,22 @@ +//! +//! A module which is typically glob imported. +//! Contains most commonly used imports. +//! + +pub use crate::account::descriptor::AccountDescriptor; +pub use crate::account::{Account, AccountKind}; +pub use crate::api::*; +pub use crate::deterministic::{AccountId, AccountStorageKey}; +pub use crate::encryption::EncryptionKind; +pub use crate::events::{Events, SyncState}; +pub use crate::rpc::{ConnectOptions, ConnectStrategy, DynRpcApi}; +pub use crate::secret::Secret; +pub use crate::settings::WalletSettings; +pub use crate::storage::{IdT, Interface, PrvKeyDataId, PrvKeyDataInfo, TransactionId, TransactionRecord, WalletDescriptor}; +pub use crate::tx::{Fees, PaymentDestination, PaymentOutput, PaymentOutputs}; +pub use crate::utxo::balance::{Balance, BalanceStrings}; +pub use crate::wallet::args::*; +pub use crate::wallet::Wallet; +pub use kash_addresses::{Address, Prefix as AddressPrefix}; +pub use kash_bip32::{Language, Mnemonic, WordCount}; +pub use kash_wrpc_client::{KashRpcClient, WrpcEncoding}; diff --git a/wallet/core/src/result.rs b/wallet/core/src/result.rs index 4c8cb83f5..06c934c97 100644 --- a/wallet/core/src/result.rs +++ b/wallet/core/src/result.rs @@ -1 +1,5 @@ +//! +//! [`Result`] type alias bound to the framework [`Error`](crate::error::Error) enum. +//! + pub type Result = std::result::Result; diff --git a/wallet/core/src/rpc.rs b/wallet/core/src/rpc.rs index b455befba..2532e8571 100644 --- a/wallet/core/src/rpc.rs +++ b/wallet/core/src/rpc.rs @@ -1,13 +1,19 @@ +//! +//! RPC adaptor struct use by the Wallet framework. +//! + use std::sync::Arc; pub use kash_rpc_core::api::ctl::RpcCtl; -pub type DynRpcApi = dyn kash_rpc_core::api::rpc::RpcApi; +pub use kash_rpc_core::api::rpc::RpcApi; +pub type DynRpcApi = dyn RpcApi; pub type NotificationChannel = kash_utils::channel::Channel; pub use kash_rpc_core::notify::mode::NotificationMode; +pub use kash_wrpc_client::client::{ConnectOptions, ConnectStrategy}; pub use kash_wrpc_client::WrpcEncoding; -/// RPC adaptor class that holds the [`RpcApi`](crate::api::RpcApi) -/// and [`RpcCtl`](crate::api::RpcCtl) instances. +/// RPC adaptor class that holds the [`RpcApi`] +/// and [`RpcCtl`] instances. #[derive(Clone)] pub struct Rpc { pub rpc_api: Arc, diff --git a/wallet/core/src/runtime/account/id.rs b/wallet/core/src/runtime/account/id.rs index 8872c5275..e69de29bb 100644 --- a/wallet/core/src/runtime/account/id.rs +++ b/wallet/core/src/runtime/account/id.rs @@ -1,113 +0,0 @@ -#[allow(unused_imports)] -use crate::derivation::{gen0::*, gen1::*, PubkeyDerivationManagerTrait, WalletDerivationManagerTrait}; -use crate::encryption::sha256_hash; -use crate::imports::*; -use crate::runtime::account::AccountKind; -use crate::storage::{self, PrvKeyDataId}; -use kash_hashes::Hash; -use kash_utils::as_slice::AsSlice; -use secp256k1::PublicKey; - -#[derive(BorshSerialize)] -struct AccountIdHashData> { - account_kind: AccountKind, - prv_key_data_id: Option, - ecdsa: Option, - account_index: Option, - secp256k1_public_key: Option>, - data: Option>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AccountId(pub(crate) Hash); - -impl AccountId { - pub(crate) fn from_bip32(prv_key_data_id: &PrvKeyDataId, data: &storage::account::Bip32) -> AccountId { - let hashable = AccountIdHashData { - account_kind: AccountKind::Bip32, - prv_key_data_id: Some([*prv_key_data_id]), - ecdsa: Some(data.ecdsa), - account_index: Some(data.account_index), - secp256k1_public_key: None, - data: None, - }; - AccountId(Hash::from_slice(sha256_hash(&hashable.try_to_vec().unwrap()).as_ref())) - } - - pub(crate) fn from_legacy(prv_key_data_id: &PrvKeyDataId, _data: &storage::account::Legacy) -> AccountId { - let hashable = AccountIdHashData { - account_kind: AccountKind::Legacy, - prv_key_data_id: Some([*prv_key_data_id]), - ecdsa: Some(false), - account_index: Some(0), - secp256k1_public_key: None, - data: None, - }; - AccountId(Hash::from_slice(sha256_hash(&hashable.try_to_vec().unwrap()).as_ref())) - } - - pub(crate) fn from_multisig(data: &storage::account::MultiSig) -> AccountId { - let hashable = AccountIdHashData { - account_kind: AccountKind::MultiSig, - prv_key_data_id: data.prv_key_data_ids.as_ref().cloned(), - ecdsa: Some(data.ecdsa), - account_index: Some(0), - secp256k1_public_key: None, - data: Some(data.xpub_keys.iter().flat_map(|s| s.as_bytes()).cloned().collect()), - }; - AccountId(Hash::from_slice(sha256_hash(&hashable.try_to_vec().unwrap()).as_ref())) - } - - pub(crate) fn from_keypair(prv_key_data_id: &PrvKeyDataId, data: &storage::account::Keypair) -> AccountId { - let hashable = AccountIdHashData { - account_kind: AccountKind::Keypair, - prv_key_data_id: Some([*prv_key_data_id]), - ecdsa: Some(data.ecdsa), - account_index: None, - secp256k1_public_key: Some(data.public_key.as_bytes().to_vec()), - data: None, - }; - AccountId(Hash::from_slice(sha256_hash(&hashable.try_to_vec().unwrap()).as_ref())) - } - - pub fn from_public_key(account_kind: AccountKind, public_key: &PublicKey) -> Self { - let hashable: AccountIdHashData<[PrvKeyDataId; 0]> = AccountIdHashData { - account_kind, - prv_key_data_id: None, - ecdsa: None, - account_index: None, - secp256k1_public_key: Some(public_key.serialize().to_vec()), - data: None, - }; - AccountId(Hash::from_slice(sha256_hash(&hashable.try_to_vec().unwrap()).as_ref())) - } - - pub fn from_data(account_kind: AccountKind, data: &[u8]) -> Self { - let hashable: AccountIdHashData<[PrvKeyDataId; 0]> = AccountIdHashData { - account_kind, - prv_key_data_id: None, - ecdsa: None, - account_index: None, - secp256k1_public_key: None, - data: Some(data.to_vec()), - }; - AccountId(Hash::from_slice(sha256_hash(&hashable.try_to_vec().unwrap()).as_ref())) - } - - pub fn short(&self) -> String { - let hex = self.to_hex(); - format!("[{}]", &hex[0..4]) - } -} - -impl ToHex for AccountId { - fn to_hex(&self) -> String { - format!("{}", self.0) - } -} - -impl std::fmt::Display for AccountId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} diff --git a/wallet/core/src/runtime/account/kind.rs b/wallet/core/src/runtime/account/kind.rs deleted file mode 100644 index 0c41aa93a..000000000 --- a/wallet/core/src/runtime/account/kind.rs +++ /dev/null @@ -1,63 +0,0 @@ -#[allow(unused_imports)] -use crate::derivation::{gen0::*, gen1::*, PubkeyDerivationManagerTrait, WalletDerivationManagerTrait}; -use crate::imports::*; -use crate::result::Result; -use std::hash::Hash; -use std::str::FromStr; -use workflow_core::enums::u8_try_from; - -u8_try_from! { - #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Hash)] - #[serde(rename_all = "lowercase")] - #[wasm_bindgen] - pub enum AccountKind { - Legacy, - #[default] - Bip32, - MultiSig, - Keypair, - Hardware, - Resident, - } -} - -impl std::fmt::Display for AccountKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AccountKind::Legacy => write!(f, "legacy"), - AccountKind::Bip32 => write!(f, "bip32"), - AccountKind::MultiSig => write!(f, "multisig"), - AccountKind::Keypair => write!(f, "keypair"), - AccountKind::Hardware => write!(f, "hardware"), - AccountKind::Resident => write!(f, "resident"), - } - } -} - -impl FromStr for AccountKind { - type Err = Error; - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "legacy" => Ok(AccountKind::Legacy), - "bip32" => Ok(AccountKind::Bip32), - "multisig" => Ok(AccountKind::MultiSig), - "keypair" => Ok(AccountKind::Keypair), - "hardware" => Ok(AccountKind::Hardware), - "resident" => Ok(AccountKind::Resident), - _ => Err(Error::InvalidAccountKind), - } - } -} - -impl TryFrom for AccountKind { - type Error = Error; - fn try_from(kind: JsValue) -> Result { - if let Some(kind) = kind.as_f64() { - Ok(AccountKind::try_from(kind as u8)?) - } else if let Some(kind) = kind.as_string() { - Ok(AccountKind::from_str(kind.as_str())?) - } else { - Err(Error::InvalidAccountKind) - } - } -} diff --git a/wallet/core/src/runtime/account/variants/bip32.rs b/wallet/core/src/runtime/account/variants/bip32.rs deleted file mode 100644 index 7c0645764..000000000 --- a/wallet/core/src/runtime/account/variants/bip32.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::derivation::AddressDerivationManager; -use crate::imports::*; -use crate::result::Result; -use crate::runtime::account::Inner; -use crate::runtime::account::{Account, AccountId, AccountKind, DerivationCapableAccount}; -use crate::runtime::Wallet; -use crate::storage::{self, Metadata, PrvKeyDataId, Settings}; -use crate::AddressDerivationManagerTrait; - -pub struct Bip32 { - inner: Arc, - prv_key_data_id: PrvKeyDataId, - account_index: u64, - xpub_keys: Arc>, - ecdsa: bool, - derivation: Arc, -} - -impl Bip32 { - pub async fn try_new( - wallet: &Arc, - prv_key_data_id: PrvKeyDataId, - settings: Settings, - data: storage::account::Bip32, - meta: Option>, - ) -> Result { - let id = AccountId::from_bip32(&prv_key_data_id, &data); - let inner = Arc::new(Inner::new(wallet, id, Some(settings))); - - let storage::account::Bip32 { account_index, xpub_keys, ecdsa, .. } = data; - - let address_derivation_indexes = meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or_default(); - - let derivation = - AddressDerivationManager::new(wallet, AccountKind::Bip32, &xpub_keys, ecdsa, 0, None, 1, address_derivation_indexes) - .await?; - - Ok(Self { - inner, - prv_key_data_id, //: prv_key_data_id.clone(), - account_index, //: account_index, - xpub_keys, //: data.xpub_keys.clone(), - ecdsa, - derivation, - }) - } -} - -#[async_trait] -impl Account for Bip32 { - fn inner(&self) -> &Arc { - &self.inner - } - - fn account_kind(&self) -> AccountKind { - AccountKind::Bip32 - } - - fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { - Ok(&self.prv_key_data_id) - } - - fn as_dyn_arc(self: Arc) -> Arc { - self - } - - fn receive_address(&self) -> Result
{ - self.derivation.receive_address_manager().current_address() - } - fn change_address(&self) -> Result
{ - self.derivation.change_address_manager().current_address() - } - - fn as_storable(&self) -> Result { - let settings = self.context().settings.clone().unwrap_or_default(); - let bip32 = storage::Bip32::new(self.account_index, self.xpub_keys.clone(), self.ecdsa); - let account = storage::Account::new(*self.id(), Some(self.prv_key_data_id), settings, storage::AccountData::Bip32(bip32)); - Ok(account) - } - - fn metadata(&self) -> Result> { - let metadata = Metadata::new(self.inner.id, self.derivation.address_derivation_meta()); - Ok(Some(metadata)) - } - - fn as_derivation_capable(self: Arc) -> Result> { - Ok(self.clone()) - } -} - -impl DerivationCapableAccount for Bip32 { - fn derivation(&self) -> Arc { - self.derivation.clone() - } -} diff --git a/wallet/core/src/runtime/account/variants/keypair.rs b/wallet/core/src/runtime/account/variants/keypair.rs deleted file mode 100644 index 27ed3fbc7..000000000 --- a/wallet/core/src/runtime/account/variants/keypair.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::imports::*; -use crate::result::Result; -use crate::runtime::account::{Account, AccountId, AccountKind, Inner}; -use crate::runtime::Wallet; -use crate::storage::{self, Metadata, PrvKeyDataId, Settings}; -use kash_addresses::Version; -use secp256k1::PublicKey; - -pub struct Keypair { - inner: Arc, - prv_key_data_id: PrvKeyDataId, - public_key: PublicKey, - ecdsa: bool, -} - -impl Keypair { - pub async fn try_new( - wallet: &Arc, - prv_key_data_id: PrvKeyDataId, - settings: Settings, - data: storage::account::Keypair, - _meta: Option>, - ) -> Result { - let id = AccountId::from_keypair(&prv_key_data_id, &data); - let inner = Arc::new(Inner::new(wallet, id, Some(settings))); - - let storage::account::Keypair { public_key, ecdsa, .. } = data; - Ok(Self { inner, prv_key_data_id, public_key: PublicKey::from_str(public_key.as_str())?, ecdsa }) - } -} - -#[async_trait] -impl Account for Keypair { - fn inner(&self) -> &Arc { - &self.inner - } - - fn account_kind(&self) -> AccountKind { - AccountKind::Keypair - } - - fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { - Ok(&self.prv_key_data_id) - } - - fn as_dyn_arc(self: Arc) -> Arc { - self - } - - fn receive_address(&self) -> Result
{ - let (xonly_public_key, _) = self.public_key.x_only_public_key(); - Ok(Address::new(self.inner().wallet.network_id()?.into(), Version::PubKey, &xonly_public_key.serialize())) - } - - fn change_address(&self) -> Result
{ - let (xonly_public_key, _) = self.public_key.x_only_public_key(); - Ok(Address::new(self.inner().wallet.network_id()?.into(), Version::PubKey, &xonly_public_key.serialize())) - } - - fn as_storable(&self) -> Result { - let settings = self.context().settings.clone().unwrap_or_default(); - let keypair = storage::Keypair::new(self.public_key, self.ecdsa); - let account = storage::Account::new(*self.id(), Some(self.prv_key_data_id), settings, storage::AccountData::Keypair(keypair)); - Ok(account) - } - - fn metadata(&self) -> Result> { - Ok(None) - } -} diff --git a/wallet/core/src/runtime/account/variants/legacy.rs b/wallet/core/src/runtime/account/variants/legacy.rs deleted file mode 100644 index ede3d2af9..000000000 --- a/wallet/core/src/runtime/account/variants/legacy.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::derivation::AddressDerivationMeta; -use crate::imports::*; -use crate::result::Result; -use crate::runtime::account::{Account, AccountId, AccountKind, DerivationCapableAccount, Inner}; -use crate::runtime::Wallet; -use crate::secret::Secret; -use crate::storage::{self, Metadata, PrvKeyDataId, Settings}; -use crate::AddressDerivationManager; -use crate::AddressDerivationManagerTrait; -use kash_bip32::{ExtendedPrivateKey, Prefix, SecretKey}; - -pub struct Legacy { - inner: Arc, - prv_key_data_id: PrvKeyDataId, - derivation: Arc, -} - -impl Legacy { - pub async fn try_new( - wallet: &Arc, - prv_key_data_id: PrvKeyDataId, - settings: Settings, - data: storage::account::Legacy, - meta: Option>, - ) -> Result { - let id = AccountId::from_legacy(&prv_key_data_id, &data); - let inner = Arc::new(Inner::new(wallet, id, Some(settings))); - - let address_derivation_indexes = - meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or(AddressDerivationMeta::new(0, 0)); - let account_index = 0; - let derivation = - AddressDerivationManager::create_legacy_pubkey_managers(wallet, account_index, address_derivation_indexes.clone(), data)?; - - Ok(Self { inner, prv_key_data_id, derivation }) - } - - pub async fn initialize_derivation( - &self, - wallet_secret: Secret, - payment_secret: Option<&Secret>, - index: Option, - ) -> Result<()> { - let prv_key_data = self - .inner - .wallet - .get_prv_key_data(wallet_secret, &self.prv_key_data_id) - .await? - .ok_or(Error::Custom(format!("Prv key data is missing for {}", self.prv_key_data_id.to_hex())))?; - let mnemonic = prv_key_data - .as_mnemonic(payment_secret)? - .ok_or(Error::Custom(format!("Could not convert Prv key data into mnemonic for {}", self.prv_key_data_id.to_hex())))?; - - let seed = mnemonic.to_seed(""); - let xprv = ExtendedPrivateKey::::new(seed).unwrap(); - let xprv = xprv.to_string(Prefix::XPRV).to_string(); - - for derivator in &self.derivation.derivators { - derivator.initialize(xprv.clone(), index)?; - } - - Ok(()) - } -} - -#[async_trait] -impl Account for Legacy { - fn inner(&self) -> &Arc { - &self.inner - } - - fn account_kind(&self) -> AccountKind { - AccountKind::Legacy - } - - fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { - Ok(&self.prv_key_data_id) - } - - fn as_dyn_arc(self: Arc) -> Arc { - self - } - - fn receive_address(&self) -> Result
{ - self.derivation.receive_address_manager().current_address() - } - - fn change_address(&self) -> Result
{ - self.derivation.change_address_manager().current_address() - } - - fn as_storable(&self) -> Result { - let settings = self.context().settings.clone().unwrap_or_default(); - let legacy = storage::Legacy::new(); - let account = storage::Account::new(*self.id(), Some(self.prv_key_data_id), settings, storage::AccountData::Legacy(legacy)); - Ok(account) - } - - fn metadata(&self) -> Result> { - let metadata = Metadata::new(self.inner.id, self.derivation.address_derivation_meta()); - Ok(Some(metadata)) - } - - fn as_derivation_capable(self: Arc) -> Result> { - Ok(self.clone()) - } - - async fn initialize_private_data( - self: Arc, - secret: Secret, - payment_secret: Option<&Secret>, - index: Option, - ) -> Result<()> { - self.initialize_derivation(secret, payment_secret, index).await?; - Ok(()) - } - - async fn clear_private_data(self: Arc) -> Result<()> { - for derivator in &self.derivation.derivators { - derivator.uninitialize()?; - } - Ok(()) - } -} - -impl DerivationCapableAccount for Legacy { - fn derivation(&self) -> Arc { - self.derivation.clone() - } -} diff --git a/wallet/core/src/runtime/account/variants/mod.rs b/wallet/core/src/runtime/account/variants/mod.rs deleted file mode 100644 index bc95ca3bd..000000000 --- a/wallet/core/src/runtime/account/variants/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod bip32; -pub mod keypair; -pub mod legacy; -pub mod multisig; -pub mod resident; - -pub use bip32::*; -pub use keypair::*; -pub use legacy::*; -pub use multisig::*; -pub use resident::*; diff --git a/wallet/core/src/runtime/account/variants/multisig.rs b/wallet/core/src/runtime/account/variants/multisig.rs deleted file mode 100644 index 5f0ab1b98..000000000 --- a/wallet/core/src/runtime/account/variants/multisig.rs +++ /dev/null @@ -1,107 +0,0 @@ -#![allow(dead_code)] - -use crate::result::Result; -use crate::runtime::account::Inner; -use crate::runtime::account::{Account, AccountId, AccountKind, DerivationCapableAccount}; -use crate::runtime::Wallet; -use crate::storage::{self, Metadata, PrvKeyDataId, Settings}; -use crate::AddressDerivationManager; -use crate::{imports::*, AddressDerivationManagerTrait}; - -pub struct MultiSig { - inner: Arc, - pub xpub_keys: Arc>, - cosigner_index: Option, - pub minimum_signatures: u16, - ecdsa: bool, - derivation: Arc, - pub prv_key_data_ids: Option>>, -} - -impl MultiSig { - pub async fn try_new( - wallet: &Arc, - settings: Settings, - data: storage::account::MultiSig, - meta: Option>, - ) -> Result { - let id = AccountId::from_multisig(&data); - let inner = Arc::new(Inner::new(wallet, id, Some(settings))); - - let storage::account::MultiSig { xpub_keys, prv_key_data_ids, cosigner_index, minimum_signatures, ecdsa, .. } = data; - - let address_derivation_indexes = meta.and_then(|meta| meta.address_derivation_indexes()).unwrap_or_default(); - - let derivation = AddressDerivationManager::new( - wallet, - AccountKind::MultiSig, - &xpub_keys, - ecdsa, - 0, - cosigner_index.map(|v| v as u32), - minimum_signatures, - address_derivation_indexes, - ) - .await?; - - Ok(Self { inner, xpub_keys, cosigner_index, minimum_signatures, ecdsa, derivation, prv_key_data_ids }) - } -} - -#[async_trait] -impl Account for MultiSig { - fn inner(&self) -> &Arc { - &self.inner - } - - fn account_kind(&self) -> AccountKind { - AccountKind::MultiSig - } - - fn prv_key_data_id(&self) -> Result<&PrvKeyDataId> { - Err(Error::AccountKindFeature) - } - - fn as_dyn_arc(self: Arc) -> Arc { - self - } - - fn receive_address(&self) -> Result
{ - self.derivation.receive_address_manager().current_address() - } - - fn change_address(&self) -> Result
{ - self.derivation.change_address_manager().current_address() - } - - fn as_storable(&self) -> Result { - let settings = self.context().settings.clone().unwrap_or_default(); - - let multisig = storage::MultiSig::new( - self.xpub_keys.clone(), - self.prv_key_data_ids.clone(), - self.cosigner_index, - self.minimum_signatures, - self.ecdsa, - ); - - let account = storage::Account::new(*self.id(), None, settings, storage::AccountData::MultiSig(multisig)); - - Ok(account) - } - - fn metadata(&self) -> Result> { - let metadata = Metadata::new(self.inner.id, self.derivation.address_derivation_meta()); - Ok(Some(metadata)) - } - - fn as_derivation_capable(self: Arc) -> Result> { - Ok(self.clone()) - } -} - -impl DerivationCapableAccount for MultiSig { - fn derivation(&self) -> Arc { - self.derivation.clone() - } -} diff --git a/wallet/core/src/runtime/mod.rs b/wallet/core/src/runtime/mod.rs deleted file mode 100644 index 44155c875..000000000 --- a/wallet/core/src/runtime/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod account; -pub mod balance; -pub mod maps; -pub mod sync; -pub mod wallet; - -pub use account::{try_from_storage, Account, AccountId, AccountKind, Bip32, Keypair, Legacy, MultiSig}; -pub use balance::{AtomicBalance, Balance, BalanceStrings}; -pub use maps::ActiveAccountMap; -pub use sync::SyncMonitor; -pub use wallet::{AccountCreateArgs, PrvKeyDataCreateArgs, Wallet, WalletCreateArgs}; diff --git a/wallet/core/src/runtime/wallet.rs b/wallet/core/src/runtime/wallet.rs deleted file mode 100644 index f223152f8..000000000 --- a/wallet/core/src/runtime/wallet.rs +++ /dev/null @@ -1,1148 +0,0 @@ -use crate::imports::*; -use crate::result::Result; -use crate::runtime::{account::ScanNotifier, try_from_storage, Account, AccountId, ActiveAccountMap}; -use crate::secret::Secret; -use crate::settings::{SettingsStore, WalletSettings}; -use crate::storage::interface::{AccessContext, CreateArgs, OpenArgs}; -use crate::storage::local::interface::LocalStore; -use crate::storage::local::Storage; -use crate::storage::{ - self, make_filename, AccessContextT, AccountData, AccountKind, Hint, Interface, PrvKeyData, PrvKeyDataId, PrvKeyDataInfo, -}; -use crate::utxo::UtxoProcessor; -#[allow(unused_imports)] -use crate::{derivation::gen0, derivation::gen0::import::*, derivation::gen1, derivation::gen1::import::*}; -use futures::future::join_all; -use futures::stream::StreamExt; -use futures::{select, FutureExt, Stream}; -use kash_bip32::{Language, Mnemonic}; -use kash_notify::{ - listener::ListenerId, - scope::{Scope, VirtualDaaScoreChangedScope}, -}; -use kash_rpc_core::notify::mode::NotificationMode; -use kash_wallet_core::storage::MultiSig; -use kash_wrpc_client::{KashRpcClient, WrpcEncoding}; -use std::sync::Arc; -use workflow_core::task::spawn; -use workflow_log::log_error; -use zeroize::Zeroize; - -const CACHE_ADDRESS_OFFSET: u32 = 2000; - -pub struct WalletCreateArgs { - pub title: Option, - pub filename: Option, - pub user_hint: Option, - pub wallet_secret: Secret, - pub overwrite_wallet_storage: bool, -} - -impl WalletCreateArgs { - pub fn new( - title: Option, - filename: Option, - user_hint: Option, - secret: Secret, - overwrite_wallet_storage: bool, - ) -> Self { - Self { title, filename, user_hint, wallet_secret: secret, overwrite_wallet_storage } - } -} - -impl From for CreateArgs { - fn from(args: WalletCreateArgs) -> Self { - CreateArgs::new(args.title, args.filename, args.user_hint, args.overwrite_wallet_storage) - } -} - -pub struct PrvKeyDataCreateArgs { - pub name: Option, - pub wallet_secret: Secret, - pub payment_secret: Option, - pub mnemonic: Option, -} - -impl PrvKeyDataCreateArgs { - pub fn new(name: Option, wallet_secret: Secret, payment_secret: Option) -> Self { - Self { name, wallet_secret, payment_secret, mnemonic: None } - } - - pub fn new_with_mnemonic(name: Option, wallet_secret: Secret, payment_secret: Option, mnemonic: String) -> Self { - Self { name, wallet_secret, payment_secret, mnemonic: Some(mnemonic) } - } -} - -impl Zeroize for PrvKeyDataCreateArgs { - fn zeroize(&mut self) { - self.mnemonic.zeroize(); - } -} - -#[derive(Clone, Debug)] -pub struct MultisigCreateArgs { - pub prv_key_data_ids: Vec, - pub name: Option, - pub title: Option, - pub wallet_secret: Secret, - pub additional_xpub_keys: Vec, - pub minimum_signatures: u16, -} - -#[derive(Clone)] -pub struct AccountCreateArgs { - pub name: Option, - pub title: Option, - pub account_kind: storage::AccountKind, - pub wallet_secret: Secret, - pub payment_secret: Option, -} - -impl AccountCreateArgs { - pub fn new( - name: Option, - title: Option, - account_kind: storage::AccountKind, - wallet_secret: Secret, - payment_secret: Option, - ) -> Self { - Self { name, title, account_kind, wallet_secret, payment_secret } - } -} - -pub struct Inner { - active_accounts: ActiveAccountMap, - legacy_accounts: ActiveAccountMap, - listener_id: Mutex>, - task_ctl: DuplexChannel, - selected_account: Mutex>>, - store: Arc, - settings: SettingsStore, - utxo_processor: Arc, - multiplexer: Multiplexer>, -} - -/// `Wallet` data structure -#[derive(Clone)] -pub struct Wallet { - inner: Arc, -} - -impl Wallet { - pub fn local_store() -> Result> { - Ok(Arc::new(LocalStore::try_new(false)?)) - } - - pub fn resident_store() -> Result> { - Ok(Arc::new(LocalStore::try_new(true)?)) - } - - pub fn try_new(storage: Arc, network_id: Option) -> Result { - Wallet::try_with_wrpc(storage, network_id) - } - - pub fn try_with_wrpc(store: Arc, network_id: Option) -> Result { - let rpc_client = - Arc::new(KashRpcClient::new_with_args(WrpcEncoding::Borsh, NotificationMode::MultiListeners, "wrpc://127.0.0.1:17110")?); - let rpc_ctl = rpc_client.ctl().clone(); - let rpc_api: Arc = rpc_client; - let rpc = Rpc::new(rpc_api, rpc_ctl); - Self::try_with_rpc(Some(rpc), store, network_id) - } - - pub fn try_with_rpc(rpc: Option, store: Arc, network_id: Option) -> Result { - let multiplexer = Multiplexer::>::new(); - let utxo_processor = Arc::new(UtxoProcessor::new(rpc.clone(), network_id, Some(multiplexer.clone()))); - - let wallet = Wallet { - inner: Arc::new(Inner { - multiplexer, - store, - active_accounts: ActiveAccountMap::default(), - legacy_accounts: ActiveAccountMap::default(), - listener_id: Mutex::new(None), - task_ctl: DuplexChannel::oneshot(), - selected_account: Mutex::new(None), - settings: SettingsStore::new_with_storage(Storage::default_settings_store()), - utxo_processor, - }), - }; - - Ok(wallet) - } - - pub fn inner(&self) -> &Arc { - &self.inner - } - - pub fn utxo_processor(&self) -> &Arc { - &self.inner.utxo_processor - } - - pub fn name(&self) -> Option { - self.store().name() - } - - pub fn store(&self) -> &Arc { - &self.inner.store - } - - pub fn active_accounts(&self) -> &ActiveAccountMap { - &self.inner.active_accounts - } - pub fn legacy_accounts(&self) -> &ActiveAccountMap { - &self.inner.legacy_accounts - } - - pub async fn reset(self: &Arc, clear_legacy_cache: bool) -> Result<()> { - self.utxo_processor().clear().await?; - - self.select(None).await?; - - let accounts = self.active_accounts().collect(); - let futures = accounts.into_iter().map(|account| account.stop()); - join_all(futures).await.into_iter().collect::>>()?; - - if clear_legacy_cache { - self.legacy_accounts().clear(); - } - - Ok(()) - } - - pub async fn reload(self: &Arc) -> Result<()> { - self.reset(false).await?; - - if self.is_open() { - self.notify(Events::WalletReload).await?; - } - - Ok(()) - } - - pub async fn close(self: &Arc) -> Result<()> { - self.reset(true).await?; - self.store().close().await?; - self.notify(Events::WalletClose).await?; - - Ok(()) - } - - cfg_if! { - if #[cfg(not(feature = "multi-user"))] { - - fn default_active_account(&self) -> Option> { - self.active_accounts().first() - } - - /// For end-user wallets only - selects an account only if there - /// is only a single account currently active in the wallet. - /// Can be used to automatically select the default account. - pub async fn autoselect_default_account_if_single(self: &Arc) -> Result<()> { - if self.active_accounts().len() == 1 { - self.select(self.default_active_account().as_ref()).await?; - } - Ok(()) - } - - /// For end-user wallets only - activates all accounts in the wallet - /// storage. - pub async fn activate_all_stored_accounts(self: &Arc) -> Result>> { - self.accounts(None).await?.try_collect::>().await - } - - /// Select an account as 'active'. Supply `None` to remove active selection. - pub async fn select(self: &Arc, account: Option<&Arc>) -> Result<()> { - *self.inner.selected_account.lock().unwrap() = account.cloned(); - if let Some(account) = account { - // log_info!("selecting account: {}", account.name_or_id()); - account.clone().start().await?; - self.notify(Events::AccountSelection{ id : Some(*account.id()) }).await?; - } else { - self.notify(Events::AccountSelection{ id : None }).await?; - } - Ok(()) - } - - /// Get currently selected account - pub fn account(&self) -> Result> { - self.inner.selected_account.lock().unwrap().clone().ok_or_else(|| Error::AccountSelection) - } - - - - } - } - - /// Loads a wallet from storage. Accounts are not activated by this call. - async fn load_impl(self: &Arc, secret: Secret, name: Option) -> Result<()> { - let name = name.or_else(|| self.settings().get(WalletSettings::Wallet)); - let name = Some(make_filename(&name, &None)); - let ctx: Arc = Arc::new(AccessContext::new(secret)); - self.store().open(&ctx, OpenArgs::new(name)).await?; - - // reset current state only after we have successfully opened another wallet - self.reset(true).await?; - - let hint = self.store().get_user_hint().await?; - self.notify(Events::WalletHint { hint }).await?; - self.notify(Events::WalletOpen).await?; - - Ok(()) - } - - /// Loads a wallet from storage. Accounts are not activated by this call. - pub async fn load(self: &Arc, secret: Secret, name: Option) -> Result<()> { - // This is a wrapper of load_impl() that catches errors and notifies the UI - if let Err(err) = self.load_impl(secret, name).await { - self.notify(Events::WalletError { message: err.to_string() }).await?; - Err(err) - } else { - Ok(()) - } - } - - /// Loads a wallet from storage. Accounts are activated by this call. - pub async fn load_and_activate(self: &Arc, secret: Secret, name: Option) -> Result<()> { - let name = name.or_else(|| self.settings().get(WalletSettings::Wallet)); - let name = Some(make_filename(&name, &None)); - let ctx: Arc = Arc::new(AccessContext::new(secret.clone())); - self.store().open(&ctx, OpenArgs::new(name)).await?; - - // reset current state only after we have successfully opened another wallet - self.reset(true).await?; - - self.initialize_all_stored_accounts(secret).await?; - let hint = self.store().get_user_hint().await?; - self.notify(Events::WalletHint { hint }).await?; - self.notify(Events::WalletOpen).await?; - Ok(()) - } - - async fn initialize_all_stored_accounts(self: &Arc, secret: Secret) -> Result<()> { - self.initialized_accounts(None, secret).await?.try_collect::>().await?; - Ok(()) - } - - pub async fn get_prv_key_data(&self, wallet_secret: Secret, id: &PrvKeyDataId) -> Result> { - let ctx: Arc = Arc::new(AccessContext::new(wallet_secret)); - self.inner.store.as_prv_key_data_store()?.load_key_data(&ctx, id).await - } - - pub async fn get_prv_key_info(&self, account: &Arc) -> Result>> { - self.inner.store.as_prv_key_data_store()?.load_key_info(account.prv_key_data_id()?).await - } - - pub async fn is_account_key_encrypted(&self, account: &Arc) -> Result> { - Ok(self.get_prv_key_info(account).await?.map(|info| info.is_encrypted())) - } - - pub fn wrpc_client(&self) -> Option> { - self.rpc_api().clone().downcast_arc::().ok() - } - - pub fn rpc_api(&self) -> Arc { - self.utxo_processor().rpc_api() - } - - pub fn rpc_ctl(&self) -> RpcCtl { - self.utxo_processor().rpc_ctl() - } - - pub fn has_rpc(&self) -> bool { - self.utxo_processor().has_rpc() - } - - pub async fn bind_rpc(self: &Arc, rpc: Option) -> Result<()> { - self.utxo_processor().bind_rpc(rpc).await?; - Ok(()) - } - - pub fn multiplexer(&self) -> &Multiplexer> { - &self.inner.multiplexer - } - - pub fn settings(&self) -> &SettingsStore { - &self.inner.settings - } - - pub fn current_daa_score(&self) -> Option { - self.utxo_processor().current_daa_score() - } - - pub async fn load_settings(&self) -> Result<()> { - self.settings().try_load().await?; - - let settings = self.settings(); - - if let Some(network_type) = settings.get(WalletSettings::Network) { - self.set_network_id(network_type).unwrap_or_else(|_| log_error!("Unable to select network type: `{}`", network_type)); - } - - if let Some(url) = settings.get::(WalletSettings::Server) { - if let Some(wrpc_client) = self.wrpc_client() { - wrpc_client.set_url(url.as_str()).unwrap_or_else(|_| log_error!("Unable to set rpc url: `{}`", url)); - } - } - - Ok(()) - } - - // intended for starting async management tasks - pub async fn start(self: &Arc) -> Result<()> { - // self.load_settings().await.unwrap_or_else(|_| log_error!("Unable to load settings, discarding...")); - - // internal event loop - self.start_task().await?; - self.utxo_processor().start().await?; - // rpc services (notifier) - if let Some(rpc_client) = self.wrpc_client() { - rpc_client.start().await?; - } - - Ok(()) - } - - // intended for stopping async management task - pub async fn stop(&self) -> Result<()> { - self.utxo_processor().stop().await?; - self.stop_task().await?; - Ok(()) - } - - pub fn listener_id(&self) -> ListenerId { - self.inner.listener_id.lock().unwrap().expect("missing wallet.inner.listener_id in Wallet::listener_id()") - } - - pub async fn get_info(&self) -> Result { - let v = self.rpc_api().get_info().await?; - Ok(format!("{v:#?}").replace('\n', "\r\n")) - } - - pub async fn subscribe_daa_score(&self) -> Result<()> { - self.rpc_api().start_notify(self.listener_id(), Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; - Ok(()) - } - - pub async fn unsubscribe_daa_score(&self) -> Result<()> { - self.rpc_api().stop_notify(self.listener_id(), Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; - Ok(()) - } - - pub async fn ping(&self) -> bool { - self.rpc_api().ping().await.is_ok() - } - - pub async fn broadcast(&self) -> Result<()> { - Ok(()) - } - - pub fn set_network_id(&self, network_id: NetworkId) -> Result<()> { - if self.is_connected() { - return Err(Error::NetworkTypeConnected); - } - self.utxo_processor().set_network_id(network_id); - Ok(()) - } - - pub fn network_id(&self) -> Result { - self.utxo_processor().network_id() - } - - pub fn address_prefix(&self) -> Result { - Ok(self.network_id()?.into()) - } - - pub fn default_port(&self) -> Result> { - let network_type = self.network_id()?; - if let Some(wrpc_client) = self.wrpc_client() { - let port = match wrpc_client.encoding() { - WrpcEncoding::Borsh => network_type.default_borsh_rpc_port(), - WrpcEncoding::SerdeJson => network_type.default_json_rpc_port(), - }; - Ok(Some(port)) - } else { - Ok(None) - } - } - - // pub async fn create_private_key_impl(self: &Arc, wallet_secret: Secret, payment_secret: Option, save : ) -> Result { - // let store = Store::new(storage::DEFAULT_WALLET_FILE)?; - // let mnemonic = Mnemonic::create_random()?; - // let wallet = storage::local::Wallet::try_load(&store).await?; - // let mut payload = wallet.payload.decrypt::(wallet_secret).unwrap(); - // payload.as_mut().add_prv_key_data(mnemonic.clone(), payment_secret)?; - // Ok(mnemonic) - // } - - // pub async fn create_private_key(self: &Arc, wallet_secret: Secret, payment_secret: Option) -> Result { - // let mnemonic = Mnemonic::create_random()?; - // self.store.as_prv_key_data_store().store_key_data(&self. - // // let store = Store::default(); - // // let mnemonic = Mnemonic::create_random()?; - // // let wallet = storage::local::Wallet::try_load(&store).await?; - // // let mut payload = wallet.payload.decrypt::(wallet_secret).unwrap(); - // // payload.as_mut().add_prv_key_data(mnemonic.clone(), payment_secret)?; - // Ok(mnemonic) - // } - - pub async fn create_multisig_account(self: &Arc, args: MultisigCreateArgs) -> Result> { - let account_storage = self.inner.store.clone().as_account_store()?; - let ctx: Arc = Arc::new(AccessContext::new(args.wallet_secret)); - - let settings = storage::Settings { is_visible: false, name: args.name, title: args.title }; - let mut xpub_keys = args.additional_xpub_keys; - - let account: Arc = if args.prv_key_data_ids.is_not_empty() { - let mut generated_xpubs = Vec::with_capacity(args.prv_key_data_ids.len()); - let mut prv_key_data_ids = Vec::with_capacity(args.prv_key_data_ids.len()); - for prv_key_data_id in args.prv_key_data_ids { - let prv_key_data = self - .inner - .store - .as_prv_key_data_store()? - .load_key_data(&ctx, &prv_key_data_id) - .await? - .ok_or(Error::PrivateKeyNotFound(prv_key_data_id.to_hex()))?; - let xpub_key = prv_key_data.create_xpub(None, AccountKind::MultiSig, 0).await?; // todo it can be done concurrently - let xpub_prefix = kash_bip32::Prefix::XPUB; - generated_xpubs.push(xpub_key.to_string(Some(xpub_prefix))); - prv_key_data_ids.push(prv_key_data_id); - } - - generated_xpubs.sort_unstable(); - xpub_keys.extend_from_slice(generated_xpubs.as_slice()); - xpub_keys.sort_unstable(); - let min_cosigner_index = xpub_keys.binary_search(generated_xpubs.first().unwrap()).unwrap() as u8; - - Arc::new( - runtime::MultiSig::try_new( - self, - settings, - MultiSig::new( - Arc::new(xpub_keys), - Some(Arc::new(prv_key_data_ids)), - Some(min_cosigner_index), - args.minimum_signatures, - false, - ), - None, - ) - .await?, - ) - } else { - Arc::new( - runtime::MultiSig::try_new( - self, - settings, - MultiSig::new(Arc::new(xpub_keys), None, None, args.minimum_signatures, false), - None, - ) - .await?, - ) - }; - - let stored_account = account.as_storable()?; - - account_storage.store_single(&stored_account, None).await?; - self.inner.store.clone().commit(&ctx).await?; - account.clone().start().await?; - - Ok(account) - } - - pub async fn create_bip32_account( - self: &Arc, - prv_key_data_id: PrvKeyDataId, - args: AccountCreateArgs, - ) -> Result> { - let account_storage = self.inner.store.clone().as_account_store()?; - let account_index = account_storage.clone().len(Some(prv_key_data_id)).await? as u64; - - let ctx: Arc = Arc::new(AccessContext::new(args.wallet_secret)); - let prv_key_data = self - .inner - .store - .as_prv_key_data_store()? - .load_key_data(&ctx, &prv_key_data_id) - .await? - .ok_or(Error::PrivateKeyNotFound(prv_key_data_id.to_hex()))?; - let xpub_key = prv_key_data.create_xpub(args.payment_secret.as_ref(), args.account_kind, account_index).await?; - let xpub_prefix = kash_bip32::Prefix::XPUB; - let xpub_keys = Arc::new(vec![xpub_key.to_string(Some(xpub_prefix))]); - - let bip32 = storage::Bip32::new(account_index, xpub_keys, false); - - let settings = storage::Settings { is_visible: false, name: None, title: None }; - let account: Arc = Arc::new(runtime::Bip32::try_new(self, prv_key_data.id, settings, bip32, None).await?); - let stored_account = account.as_storable()?; - - account_storage.store_single(&stored_account, None).await?; - self.inner.store.clone().commit(&ctx).await?; - account.clone().start().await?; - - Ok(account) - } - - pub async fn create_wallet(self: &Arc, args: WalletCreateArgs) -> Result> { - self.reset(true).await?; - let ctx: Arc = Arc::new(AccessContext::new(args.wallet_secret.clone())); - self.inner.store.create(&ctx, args.into()).await?; - let descriptor = self.inner.store.descriptor()?; - self.inner.store.commit(&ctx).await?; - Ok(descriptor) - } - - pub async fn create_prv_key_data(self: &Arc, args: PrvKeyDataCreateArgs) -> Result<(PrvKeyDataId, Mnemonic)> { - let ctx: Arc = Arc::new(AccessContext::new(args.wallet_secret.clone())); - let mnemonic = if let Some(mnemonic) = args.mnemonic.as_ref() { - let mnemonic = mnemonic.to_string(); - Mnemonic::new(mnemonic, Language::English)? - } else { - Mnemonic::create_random()? - }; - let prv_key_data = PrvKeyData::try_from((mnemonic.clone(), args.payment_secret.as_ref()))?; - let prv_key_data_id = prv_key_data.id; - let prv_key_data_store = self.inner.store.as_prv_key_data_store()?; - prv_key_data_store.store(&ctx, prv_key_data).await?; - self.inner.store.commit(&ctx).await?; - Ok((prv_key_data_id, mnemonic)) - } - - pub async fn create_wallet_with_account( - self: &Arc, - wallet_args: WalletCreateArgs, - account_args: AccountCreateArgs, - ) -> Result<(Mnemonic, Option, Arc)> { - self.reset(true).await?; - - let ctx: Arc = Arc::new(AccessContext::new(account_args.wallet_secret)); - - self.inner.store.create(&ctx, wallet_args.into()).await?; - let descriptor = self.inner.store.descriptor()?; - let xpub_prefix = kash_bip32::Prefix::XPUB; - let mnemonic = Mnemonic::create_random()?; - let account_index = 0; - let prv_key_data = PrvKeyData::try_from((mnemonic.clone(), account_args.payment_secret.as_ref()))?; - let xpub_key = - prv_key_data.create_xpub(account_args.payment_secret.as_ref(), account_args.account_kind, account_index).await?; - let xpub_keys = Arc::new(vec![xpub_key.to_string(Some(xpub_prefix))]); - - let bip32 = storage::Bip32::new(account_index, xpub_keys, false); - - let settings = storage::Settings { is_visible: false, name: None, title: None }; - let account: Arc = Arc::new(runtime::Bip32::try_new(self, prv_key_data.id, settings, bip32, None).await?); - let stored_account = account.as_storable()?; - - let prv_key_data_store = self.inner.store.as_prv_key_data_store()?; - prv_key_data_store.store(&ctx, prv_key_data).await?; - let account_store = self.inner.store.as_account_store()?; - account_store.store_single(&stored_account, None).await?; - self.inner.store.commit(&ctx).await?; - - self.select(Some(&account)).await?; - Ok((mnemonic, descriptor, account)) - } - - pub async fn get_account_by_id(self: &Arc, account_id: &AccountId) -> Result>> { - if let Some(account) = self.active_accounts().get(account_id) { - Ok(Some(account.clone())) - } else { - let account_storage = self.inner.store.as_account_store()?; - let stored = account_storage.load_single(account_id).await?; - if let Some((stored_account, stored_metadata)) = stored { - let account = try_from_storage(self, stored_account, stored_metadata).await?; - Ok(Some(account)) - } else { - Ok(None) - } - } - } - - pub async fn notify(&self, event: Events) -> Result<()> { - self.multiplexer() - .try_broadcast(Box::new(event)) - .map_err(|_| Error::Custom("multiplexer channel error during update_balance".to_string()))?; - Ok(()) - } - - pub fn is_synced(&self) -> bool { - self.utxo_processor().is_synced() - } - - pub fn is_connected(&self) -> bool { - self.utxo_processor().is_connected() - } - - async fn handle_event(self: &Arc, event: Box) -> Result<()> { - match &*event { - Events::Pending { record, is_outgoing } | Events::Maturity { record, is_outgoing } => { - // if `is_outgoint` is set, this means that this pending and maturity - // event is for the change UTXOs of the outgoing transaction. - if !is_outgoing { - self.store().as_transaction_record_store()?.store(&[record]).await?; - } - } - - Events::Reorg { record } | Events::External { record } | Events::Outgoing { record } => { - self.store().as_transaction_record_store()?.store(&[record]).await?; - } - Events::SyncState { sync_state } => { - if sync_state.is_synced() && self.is_open() { - self.reload().await?; - } - } - _ => {} - } - - Ok(()) - } - - async fn start_task(self: &Arc) -> Result<()> { - let this = self.clone(); - let task_ctl_receiver = self.inner.task_ctl.request.receiver.clone(); - let task_ctl_sender = self.inner.task_ctl.response.sender.clone(); - let events = self.multiplexer().channel(); - - spawn(async move { - loop { - select! { - _ = task_ctl_receiver.recv().fuse() => { - break; - }, - - msg = events.receiver.recv().fuse() => { - match msg { - Ok(event) => { - this.handle_event(event).await.unwrap_or_else(|e| log_error!("Wallet::handle_event() error: {}", e)); - }, - Err(err) => { - log_error!("Wallet: error while receiving multiplexer message: {err}"); - log_error!("Suspending Wallet processing..."); - - break; - } - } - }, - } - } - - task_ctl_sender.send(()).await.unwrap(); - }); - Ok(()) - } - - async fn stop_task(&self) -> Result<()> { - self.inner.task_ctl.signal(()).await.expect("Wallet::stop_task() `signal` error"); - Ok(()) - } - - pub fn is_open(&self) -> bool { - self.inner.store.is_open() - } - - pub fn descriptor(&self) -> Result> { - self.inner.store.descriptor() - } - - pub async fn exists(&self, name: Option<&str>) -> Result { - self.inner.store.exists(name).await - } - - pub async fn keys(&self) -> Result>>> { - self.inner.store.as_prv_key_data_store()?.iter().await - } - - pub async fn find_accounts_by_name_or_id(&self, pat: &str) -> Result>> { - let active_accounts = self.active_accounts().inner().values().cloned().collect::>(); - let matches = active_accounts - .into_iter() - .filter(|account| { - account.name().map(|name| name.starts_with(pat)).unwrap_or(false) || account.id().to_hex().starts_with(pat) - }) - .collect::>(); - Ok(matches) - } - - pub async fn accounts(self: &Arc, filter: Option) -> Result>>> { - let iter = self.inner.store.as_account_store().unwrap().iter(filter).await.unwrap(); - let wallet = self.clone(); - - let stream = iter.then(move |stored| { - let wallet = wallet.clone(); - - async move { - let (stored_account, stored_metadata) = stored.unwrap(); - if let Some(account) = wallet.legacy_accounts().get(&stored_account.id) { - if !wallet.active_accounts().contains(account.id()) { - account.clone().start().await?; - } - Ok(account) - } else if let Some(account) = wallet.active_accounts().get(&stored_account.id) { - Ok(account) - } else { - let account = try_from_storage(&wallet, stored_account, stored_metadata).await?; - account.clone().start().await?; - Ok(account) - } - } - }); - - Ok(Box::pin(stream)) - } - - pub async fn initialized_accounts( - self: &Arc, - filter: Option, - secret: Secret, - ) -> Result>>> { - let iter = self.inner.store.as_account_store().unwrap().iter(filter).await.unwrap(); - let wallet = self.clone(); - - let stream = iter.then(move |stored| { - let wallet = wallet.clone(); - let secret = secret.clone(); - - async move { - let (stored_account, stored_metadata) = stored.unwrap(); - if let Some(account) = wallet.active_accounts().get(&stored_account.id) { - Ok(account) - } else { - let is_legacy = matches!(stored_account.data, AccountData::Legacy { .. }); - let account = try_from_storage(&wallet, stored_account, stored_metadata).await?; - - if is_legacy { - account.clone().initialize_private_data(secret, None, None).await?; - wallet.legacy_accounts().insert(account.clone()); - } - - account.clone().start().await?; - - if is_legacy { - let derivation = account.clone().as_derivation_capable()?.derivation(); - let m = derivation.receive_address_manager(); - m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - let m = derivation.change_address_manager(); - m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - account.clone().clear_private_data().await?; - } - - Ok(account) - } - } - }); - - Ok(Box::pin(stream)) - } - - pub async fn import_gen0_keydata( - self: &Arc, - import_secret: Secret, - wallet_secret: Secret, - payment_secret: Option<&Secret>, - notifier: Option, - ) -> Result> { - let notifier = notifier.as_ref(); - let keydata = load_v0_keydata(&import_secret).await?; - - let ctx: Arc = Arc::new(AccessContext::new(wallet_secret.clone())); - - let mnemonic = Mnemonic::new(keydata.mnemonic.trim(), Language::English)?; - let prv_key_data = PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret)?; - let prv_key_data_store = self.inner.store.as_prv_key_data_store()?; - if prv_key_data_store.load_key_data(&ctx, &prv_key_data.id).await?.is_some() { - return Err(Error::PrivateKeyAlreadyExists(prv_key_data.id.to_hex())); - } - - let data = storage::Legacy::new(); - let settings = storage::Settings::default(); - let account = Arc::new(runtime::account::Legacy::try_new(self, prv_key_data.id, settings, data, None).await?); - - // activate account (add it to wallet active account list) - self.active_accounts().insert(account.clone().as_dyn_arc()); - self.legacy_accounts().insert(account.clone().as_dyn_arc()); - - let account_store = self.inner.store.as_account_store()?; - let stored_account = account.as_storable()?; - // store private key and account - self.inner.store.batch().await?; - prv_key_data_store.store(&ctx, prv_key_data).await?; - account_store.store_single(&stored_account, None).await?; - self.inner.store.flush(&ctx).await?; - - account.clone().initialize_private_data(wallet_secret, payment_secret, None).await?; - - if self.is_connected() { - if let Some(notifier) = notifier { - notifier(0, 0, None); - } - account.clone().scan(Some(100), Some(5000)).await?; - } - - let derivation = account.clone().as_derivation_capable()?.derivation(); - let m = derivation.receive_address_manager(); - m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - let m = derivation.change_address_manager(); - m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - account.clone().clear_private_data().await?; - - account.clone().clear_private_data().await?; - - Ok(account) - } - - pub async fn import_gen1_keydata(self: &Arc, secret: Secret) -> Result<()> { - let _keydata = load_v1_keydata(&secret).await?; - - Ok(()) - } - - pub async fn import_with_mnemonic( - self: &Arc, - wallet_secret: Secret, - payment_secret: Option<&Secret>, - mnemonic: Mnemonic, - account_kind: AccountKind, - ) -> Result> { - let prv_key_data = storage::PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret)?; - let prv_key_data_store = self.store().as_prv_key_data_store()?; - let ctx: Arc = Arc::new(AccessContext::new(wallet_secret.clone())); - if prv_key_data_store.load_key_data(&ctx, &prv_key_data.id).await?.is_some() { - return Err(Error::PrivateKeyAlreadyExists(prv_key_data.id.to_hex())); - } - let mut is_legacy = false; - let account: Arc = match account_kind { - AccountKind::Bip32 => { - let account_index = 0; - let xpub_key = prv_key_data.create_xpub(payment_secret, account_kind, account_index).await?; - let xpub_keys = Arc::new(vec![xpub_key.to_string(Some(kash_bip32::Prefix::KPUB))]); - let ecdsa = false; - // --- - - let data = storage::Bip32::new(account_index, xpub_keys, ecdsa); - let settings = storage::Settings::default(); - Arc::new(runtime::account::Bip32::try_new(self, prv_key_data.id, settings, data, None).await?) - // account - } - AccountKind::Legacy => { - is_legacy = true; - let data = storage::Legacy::new(); - let settings = storage::Settings::default(); - Arc::new(runtime::account::Legacy::try_new(self, prv_key_data.id, settings, data, None).await?) - } - _ => { - return Err(Error::AccountKindFeature); - } - }; - - let stored_account = account.as_storable()?; - let account_store = self.inner.store.as_account_store()?; - self.inner.store.batch().await?; - self.store().as_prv_key_data_store()?.store(&ctx, prv_key_data).await?; - account_store.store_single(&stored_account, None).await?; - self.inner.store.flush(&ctx).await?; - - if is_legacy { - account.clone().initialize_private_data(wallet_secret, None, None).await?; - self.legacy_accounts().insert(account.clone()); - } - account.clone().start().await?; - if is_legacy { - let derivation = account.clone().as_derivation_capable()?.derivation(); - let m = derivation.receive_address_manager(); - m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - let m = derivation.change_address_manager(); - m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; - account.clone().clear_private_data().await?; - } - - Ok(account) - } - - pub async fn import_multisig_with_mnemonic( - self: &Arc, - wallet_secret: Secret, - mnemonics_secrets: Vec<(Mnemonic, Option)>, - minimum_signatures: u16, - mut additional_xpub_keys: Vec, - ) -> Result> { - let ctx: Arc = Arc::new(AccessContext::new(wallet_secret)); - - let mut generated_xpubs = Vec::with_capacity(mnemonics_secrets.len()); - let mut prv_key_data_ids = Vec::with_capacity(mnemonics_secrets.len()); - let prv_key_data_store = self.store().as_prv_key_data_store()?; - - for (mnemonic, payment_secret) in mnemonics_secrets { - let prv_key_data = storage::PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret.as_ref())?; - if prv_key_data_store.load_key_data(&ctx, &prv_key_data.id).await?.is_some() { - return Err(Error::PrivateKeyAlreadyExists(prv_key_data.id.to_hex())); - } - let xpub_key = prv_key_data.create_xpub(payment_secret.as_ref(), AccountKind::MultiSig, 0).await?; // todo it can be done concurrently - let xpub_prefix = kash_bip32::Prefix::XPUB; - generated_xpubs.push(xpub_key.to_string(Some(xpub_prefix))); - prv_key_data_ids.push(prv_key_data.id); - prv_key_data_store.store(&ctx, prv_key_data).await?; - } - - generated_xpubs.sort_unstable(); - additional_xpub_keys.extend_from_slice(generated_xpubs.as_slice()); - let mut xpub_keys = additional_xpub_keys; - xpub_keys.sort_unstable(); - let min_cosigner_index = xpub_keys.binary_search(generated_xpubs.first().unwrap()).unwrap() as u8; - - let account: Arc = Arc::new( - runtime::MultiSig::try_new( - self, - storage::Settings::default(), - MultiSig::new( - Arc::new(xpub_keys), - Some(Arc::new(prv_key_data_ids)), - Some(min_cosigner_index), - minimum_signatures, - false, - ), - None, - ) - .await?, - ); - - let stored_account = account.as_storable()?; - self.inner.store.clone().as_account_store()?.store_single(&stored_account, None).await?; - self.inner.store.clone().commit(&ctx).await?; - account.clone().start().await?; - - Ok(account) - } -} - -#[cfg(not(target_arch = "wasm32"))] -#[cfg(test)] -mod test { - use std::{str::FromStr, thread::sleep, time}; - - use super::*; - use crate::utxo::{UtxoContext, UtxoContextBinding, UtxoIterator}; - use kash_addresses::{Address, Prefix, Version}; - use kash_bip32::{ChildNumber, ExtendedPrivateKey, SecretKey}; - use kash_consensus_core::asset_type::AssetType; - use kash_consensus_core::subnets::SUBNETWORK_ID_NATIVE; - use kash_consensus_core::tx::TransactionAction; - use kash_consensus_wasm::{sign_transaction, SignableTransaction, Transaction, TransactionInput, TransactionOutput}; - use kash_txscript::pay_to_address_script; - use workflow_rpc::client::ConnectOptions; - - async fn create_utxos_context_with_addresses( - rpc: Arc, - addresses: Vec
, - current_daa_score: u64, - core: &UtxoProcessor, - ) -> Result { - let utxos = rpc.get_utxos_by_addresses(addresses).await?; - let utxo_context = UtxoContext::new(core, UtxoContextBinding::default()); - let entries = utxos.into_iter().map(|entry| entry.into()).collect::>(); - for entry in entries.into_iter() { - utxo_context.insert(entry, current_daa_score).await?; - } - Ok(utxo_context) - } - - #[allow(dead_code)] - // #[tokio::test] - async fn wallet_test() -> Result<()> { - println!("Creating wallet..."); - let resident_store = Wallet::resident_store()?; - let wallet = Arc::new(Wallet::try_new(resident_store, None)?); - - let rpc_api = wallet.rpc_api(); - let utxo_processor = wallet.utxo_processor(); - - let wrpc_client = wallet.wrpc_client().expect("Unable to obtain wRPC client"); - - let info = rpc_api.get_block_dag_info().await?; - let current_daa_score = info.virtual_daa_score; - - let _connect_result = wrpc_client.connect(ConnectOptions::fallback()).await; - //println!("connect_result: {_connect_result:?}"); - - let _result = wallet.start().await; - //println!("wallet.task(): {_result:?}"); - let result = wallet.get_info().await; - println!("wallet.get_info(): {result:#?}"); - - let address = Address::try_from("kashtest:qz7ulu4c25dh7fzec9zjyrmlhnkzrg4wmf89q7gzr3gfrsj3uz6xjceef60sd")?; - - let utxo_context = - self::create_utxos_context_with_addresses(rpc_api.clone(), vec![address.clone()], current_daa_score, utxo_processor) - .await?; - - let utxo_set_balance = utxo_context.calculate_balance().await; - println!("get_utxos_by_addresses: {utxo_set_balance:?}"); - - let to_address = Address::try_from("kashtest:qpakxqlesqywgkq7rg4wyhjd93kmw7trkl3gpa3vd5flyt59a43yyn8vu0w8c")?; - let mut iter = UtxoIterator::new(&utxo_context); - let utxo = iter.next().unwrap(); - let utxo = (*utxo.utxo).clone(); - let selected_entries = vec![utxo]; - - let entries = &selected_entries; - - let inputs = selected_entries - .iter() - .enumerate() - .map(|(sequence, utxo)| TransactionInput::new(utxo.outpoint.clone(), vec![], sequence as u64, 0)) - .collect::>(); - - let tx = Transaction::new( - 0, - inputs, - vec![TransactionOutput::new(1000, &pay_to_address_script(&to_address), AssetType::KSH)], - TransactionAction::TransferKSH, - 0, - SUBNETWORK_ID_NATIVE, - 0, - vec![], - )?; - - let mtx = SignableTransaction::new(tx, (*entries).clone().into()); - - let derivation_path = - gen1::WalletDerivationManager::build_derivate_path(false, 0, None, Some(kash_bip32::AddressType::Receive))?; - - let xprv = "kprv5y2qurMHCsXYrNfU3GCihuwG3vMqFji7PZXajMEqyBkNh9UZUJgoHYBLTKu1eM4MvUtomcXPQ3Sw9HZ5ebbM4byoUciHo1zrPJBQfqpLorQ"; - - let xkey = ExtendedPrivateKey::::from_str(xprv)?.derive_path(derivation_path)?; - - let xkey = xkey.derive_child(ChildNumber::new(0, false)?)?; - - // address test - let address_test = Address::new(Prefix::Testnet, Version::PubKey, &xkey.public_key().to_bytes()[1..]); - let address_str: String = address_test.clone().into(); - assert_eq!(address, address_test, "Address don't match"); - println!("address: {address_str}"); - - let private_keys = vec![xkey.to_bytes()]; - - println!("mtx: {mtx:?}"); - - let mtx = sign_transaction(mtx, private_keys, true)?; - - let utxo_context = - self::create_utxos_context_with_addresses(rpc_api.clone(), vec![to_address.clone()], current_daa_score, utxo_processor) - .await?; - let to_balance = utxo_context.calculate_balance().await; - println!("to address balance before tx submit: {to_balance:?}"); - - let result = rpc_api.submit_transaction(mtx.into(), false).await?; - - println!("tx submit result, {:?}", result); - println!("sleep for 5s..."); - sleep(time::Duration::from_millis(5000)); - let utxo_context = - self::create_utxos_context_with_addresses(rpc_api.clone(), vec![to_address.clone()], current_daa_score, utxo_processor) - .await?; - let to_balance = utxo_context.calculate_balance().await; - println!("to address balance after tx submit: {to_balance:?}"); - - Ok(()) - } -} diff --git a/wallet/core/src/secret.rs b/wallet/core/src/secret.rs index 3e6d079bb..9401fa3b1 100644 --- a/wallet/core/src/secret.rs +++ b/wallet/core/src/secret.rs @@ -1,6 +1,12 @@ +//! +//! Secret container for sensitive data. Performs zeroization on drop. +//! + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use serde::{Deserialize, Serialize}; use zeroize::Zeroize; -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] pub struct Secret(Vec); impl Secret { diff --git a/wallet/core/src/serializer.rs b/wallet/core/src/serializer.rs new file mode 100644 index 000000000..75cb32a37 --- /dev/null +++ b/wallet/core/src/serializer.rs @@ -0,0 +1,64 @@ +//! +//! Helpers for binary serialization and deserialization used by the storage subsystem. +//! + +use crate::imports::*; + +#[derive(Debug, Clone)] +pub struct StorageHeader { + pub magic: u32, + pub version: u32, +} + +impl StorageHeader { + pub fn new(magic: u32, version: u32) -> Self { + Self { magic, version } + } + + pub fn try_magic(self, magic: u32) -> IoResult { + if self.magic != magic { + Err(IoError::new( + IoErrorKind::Other, + format!( + "Deserializer magic value error: expected '0x{}' received '0x{}'", + magic.to_le_bytes().as_slice().to_hex(), + self.magic.to_le_bytes().as_slice().to_hex() + ), + )) + } else { + Ok(self) + } + } + + pub fn try_version(self, version: u32) -> IoResult { + if self.version > version { + Err(IoError::new( + IoErrorKind::Other, + format!( + "Deserializer data has a newer version than the current version: expected version at most '{}' received '{}' (your data may have been generated on a newer version of the software)", + version, + self.version + ), + )) + } else { + Ok(self) + } + } +} + +impl BorshSerialize for StorageHeader { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + BorshSerialize::serialize(&(self.magic, self.version), writer) + } +} + +impl BorshDeserialize for StorageHeader { + fn deserialize(buf: &mut &[u8]) -> std::io::Result { + let (magic, version): (u32, u32) = BorshDeserialize::deserialize(buf)?; + Ok(Self { magic, version }) + } +} + +pub type IoError = std::io::Error; +pub type IoErrorKind = std::io::ErrorKind; +pub type IoResult = std::io::Result; diff --git a/wallet/core/src/settings.rs b/wallet/core/src/settings.rs index 1f120a932..f214b9153 100644 --- a/wallet/core/src/settings.rs +++ b/wallet/core/src/settings.rs @@ -1,3 +1,7 @@ +//! +//! Multi-platform storage for wallet and application settings. +//! + use crate::imports::*; use crate::result::Result; use crate::storage::local::Storage; @@ -167,7 +171,7 @@ where } pub fn application_folder() -> Result { - Ok(fs::resolve_path(storage::local::DEFAULT_STORAGE_FOLDER)?) + Ok(fs::resolve_path(storage::local::default_storage_folder())?) } pub async fn ensure_application_folder() -> Result<()> { diff --git a/wallet/core/src/storage/account.rs b/wallet/core/src/storage/account.rs index 8db3c4870..da0c2df32 100644 --- a/wallet/core/src/storage/account.rs +++ b/wallet/core/src/storage/account.rs @@ -1,157 +1,153 @@ +//! +//! Storage wrapper for account data. +//! + use crate::imports::*; -use crate::storage::{AccountId, AccountKind, PrvKeyDataId}; -use secp256k1::PublicKey; + +const ACCOUNT_SETTINGS_VERSION: u32 = 0; #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] -pub struct Settings { - pub is_visible: bool, +pub struct AccountSettings { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, -} - -const LEGACY_ACCOUNT_VERSION: u16 = 0; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub struct Legacy { - pub version: u16, + pub meta: Option>, } -impl Legacy { - pub fn new() -> Self { - Self { version: LEGACY_ACCOUNT_VERSION } - } -} +impl BorshSerialize for AccountSettings { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + BorshSerialize::serialize(&ACCOUNT_SETTINGS_VERSION, writer)?; + BorshSerialize::serialize(&self.name, writer)?; + BorshSerialize::serialize(&self.meta, writer)?; -impl Default for Legacy { - fn default() -> Self { - Self::new() + Ok(()) } } -const BIP32_ACCOUNT_VERSION: u16 = 0; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub struct Bip32 { - #[serde(default)] - pub version: u16, +impl BorshDeserialize for AccountSettings { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let _version: u32 = BorshDeserialize::deserialize(buf)?; + let name = BorshDeserialize::deserialize(buf)?; + let meta = BorshDeserialize::deserialize(buf)?; - pub account_index: u64, - pub xpub_keys: Arc>, - pub ecdsa: bool, -} - -impl Bip32 { - pub fn new(account_index: u64, xpub_keys: Arc>, ecdsa: bool) -> Self { - Self { version: BIP32_ACCOUNT_VERSION, account_index, xpub_keys, ecdsa } + Ok(Self { name, meta }) } } -const MULTISIG_ACCOUNT_VERSION: u16 = 0; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub struct MultiSig { - #[serde(default)] - pub version: u16, - pub xpub_keys: Arc>, - pub prv_key_data_ids: Option>>, - pub cosigner_index: Option, - pub minimum_signatures: u16, - pub ecdsa: bool, -} +/// A [`Storable`] variant used explicitly for [`Account`] payload storage. +pub trait AccountStorable: Storable {} -impl MultiSig { - pub fn new( - xpub_keys: Arc>, - prv_key_data_ids: Option>>, - cosigner_index: Option, - minimum_signatures: u16, - ecdsa: bool, - ) -> Self { - Self { version: MULTISIG_ACCOUNT_VERSION, xpub_keys, prv_key_data_ids, cosigner_index, minimum_signatures, ecdsa } +#[derive(Clone, Serialize, Deserialize)] +pub struct AccountStorage { + pub kind: AccountKind, + pub id: AccountId, + pub storage_key: AccountStorageKey, + pub prv_key_data_ids: AssocPrvKeyDataIds, + pub settings: AccountSettings, + pub serialized: Vec, +} + +impl AccountStorage { + const STORAGE_MAGIC: u32 = 0x4153414b; + const STORAGE_VERSION: u32 = 0; + + pub fn try_new( + kind: AccountKind, + id: &AccountId, + storage_key: &AccountStorageKey, + prv_key_data_ids: AssocPrvKeyDataIds, + settings: AccountSettings, + serialized: A, + ) -> Result + where + A: AccountStorable, + { + Ok(Self { id: *id, storage_key: *storage_key, kind, prv_key_data_ids, settings, serialized: serialized.try_to_vec()? }) } -} -const KEYPAIR_ACCOUNT_VERSION: u16 = 0; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub struct Keypair { - #[serde(default)] - pub version: u16, - - pub public_key: String, - pub ecdsa: bool, -} - -impl Keypair { - pub fn new(public_key: PublicKey, ecdsa: bool) -> Self { - Self { version: KEYPAIR_ACCOUNT_VERSION, public_key: public_key.to_string(), ecdsa } + pub fn id(&self) -> &AccountId { + &self.id } -} -const HARDWARE_ACCOUNT_VERSION: u16 = 0; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub struct Hardware { - #[serde(default)] - pub version: u16, + pub fn storage_key(&self) -> &AccountStorageKey { + &self.storage_key + } - pub descriptor: String, + pub fn serialized(&self) -> &[u8] { + &self.serialized + } } -impl Hardware { - pub fn new(descriptor: &str) -> Self { - Self { version: HARDWARE_ACCOUNT_VERSION, descriptor: descriptor.to_string() } +impl std::fmt::Debug for AccountStorage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AccountStorage") + .field("kind", &self.kind) + .field("id", &self.id) + .field("storage_key", &self.storage_key) + .field("prv_key_data_ids", &self.prv_key_data_ids) + .field("settings", &self.settings) + .field("serialized", &self.serialized.to_hex()) + .finish() } } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -#[serde(rename_all = "lowercase")] -pub enum AccountData { - Legacy(Legacy), - Bip32(Bip32), - MultiSig(MultiSig), - Keypair(Keypair), - Hardware(Hardware), -} +impl BorshSerialize for AccountStorage { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.kind, writer)?; + BorshSerialize::serialize(&self.id, writer)?; + BorshSerialize::serialize(&self.storage_key, writer)?; + BorshSerialize::serialize(&self.prv_key_data_ids, writer)?; + BorshSerialize::serialize(&self.settings, writer)?; + BorshSerialize::serialize(&self.serialized, writer)?; -impl AccountData { - pub fn account_kind(&self) -> AccountKind { - match self { - AccountData::Legacy { .. } => AccountKind::Legacy, - AccountData::Bip32 { .. } => AccountKind::Bip32, - AccountData::MultiSig { .. } => AccountKind::MultiSig, - AccountData::Hardware { .. } => AccountKind::Hardware, - AccountData::Keypair { .. } => AccountKind::Keypair, - } + Ok(()) } } -const ACCOUNT_VERSION: u16 = 0; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Account { - #[serde(default)] - pub version: u16, +impl BorshDeserialize for AccountStorage { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; - pub id: AccountId, - pub prv_key_data_id: Option, - pub settings: Settings, - pub data: AccountData, -} + let kind = BorshDeserialize::deserialize(buf)?; + let id = BorshDeserialize::deserialize(buf)?; + let storage_key = BorshDeserialize::deserialize(buf)?; + let prv_key_data_ids = BorshDeserialize::deserialize(buf)?; + let settings = BorshDeserialize::deserialize(buf)?; + let serialized = BorshDeserialize::deserialize(buf)?; -impl Account { - pub fn new(id: AccountId, prv_key_data_id: Option, settings: Settings, data: AccountData) -> Self { - Self { version: ACCOUNT_VERSION, id, prv_key_data_id, settings, data } - } - - pub fn data(&self) -> &AccountData { - &self.data + Ok(Self { kind, id, storage_key, prv_key_data_ids, settings, serialized }) } +} - pub fn is_legacy(&self) -> bool { - matches!(self.data, AccountData::Legacy { .. }) +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_account_storage_wrapper() -> Result<()> { + let (id, storage_key) = make_account_hashes(from_data(&BIP32_ACCOUNT_KIND.into(), &[0x00, 0x01, 0x02, 0x03])); + let prv_key_data_id = PrvKeyDataId::new(0xcafe); + let storable = bip32::Payload::new(0, ExtendedPublicKeys::default(), false); + let storable_in = AccountStorage::try_new( + BIP32_ACCOUNT_KIND.into(), + &id, + &storage_key, + prv_key_data_id.into(), + AccountSettings::default(), + storable, + )?; + let guard = StorageGuard::new(&storable_in); + let storable_out = guard.validate()?; + + assert_eq!(storable_in.kind, storable_out.kind); + assert_eq!(storable_in.id, storable_out.id); + assert_eq!(storable_in.storage_key, storable_out.storage_key); + assert_eq!(storable_in.serialized, storable_out.serialized); + + Ok(()) } } diff --git a/wallet/core/src/storage/address.rs b/wallet/core/src/storage/address.rs index f5a5db85a..640aac84a 100644 --- a/wallet/core/src/storage/address.rs +++ b/wallet/core/src/storage/address.rs @@ -1,6 +1,11 @@ +//! +//! Wallet address book. +//! + use crate::imports::*; -#[derive(Debug, Clone, Serialize, Deserialize)] +// TODO +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct AddressBookEntry { pub alias: String, pub title: String, diff --git a/wallet/core/src/storage/binding.rs b/wallet/core/src/storage/binding.rs index c2fc06cef..45eac4a7e 100644 --- a/wallet/core/src/storage/binding.rs +++ b/wallet/core/src/storage/binding.rs @@ -1,8 +1,11 @@ +//! +//! Id references used to associate transactions with Account or UtxoContext ids. +//! + use crate::imports::*; -use crate::runtime::{Account, AccountId}; use crate::utxo::{UtxoContextBinding as UtxoProcessorBinding, UtxoContextId}; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "kebab-case")] #[serde(tag = "type", content = "id")] pub enum Binding { @@ -34,3 +37,9 @@ impl Binding { } } } + +impl AsRef for Binding { + fn as_ref(&self) -> &Binding { + self + } +} diff --git a/wallet/core/src/storage/hint.rs b/wallet/core/src/storage/hint.rs index c0d8b30c2..83921be4f 100644 --- a/wallet/core/src/storage/hint.rs +++ b/wallet/core/src/storage/hint.rs @@ -1,7 +1,14 @@ +//! +//! User hint is a string that can be stored in the wallet +//! and presented to the user when the wallet opens to help +//! prevent phishing attacks. +//! + use crate::imports::*; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use std::fmt::{Display, Formatter}; -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] pub struct Hint { pub text: String, } diff --git a/wallet/core/src/storage/id.rs b/wallet/core/src/storage/id.rs index 3c61590cb..0b87abf0e 100644 --- a/wallet/core/src/storage/id.rs +++ b/wallet/core/src/storage/id.rs @@ -1,10 +1,15 @@ +//! +//! General-purpose Id traits used by storage data collections. +//! + use kash_consensus_core::tx::TransactionId; use kash_utils::hex::ToHex; use std::cmp::Eq; use std::fmt::Debug; use std::hash::Hash; -use crate::storage::{Account, AccountId, PrvKeyData, PrvKeyDataId, PrvKeyDataInfo, TransactionRecord}; +use crate::deterministic::AccountId; +use crate::storage::{AccountStorage, PrvKeyData, PrvKeyDataId, PrvKeyDataInfo, TransactionRecord}; pub trait IdT { type Id: Eq + Hash + Debug + ToHex; @@ -25,7 +30,7 @@ impl IdT for PrvKeyDataInfo { } } -impl IdT for Account { +impl IdT for AccountStorage { type Id = AccountId; fn id(&self) -> &AccountId { &self.id diff --git a/wallet/core/src/storage/interface.rs b/wallet/core/src/storage/interface.rs index 8e6afb780..c46dc9f20 100644 --- a/wallet/core/src/storage/interface.rs +++ b/wallet/core/src/storage/interface.rs @@ -1,54 +1,24 @@ +//! +//! Wallet storage subsystem traits. +//! + use crate::imports::*; use crate::result::Result; use crate::secret::Secret; -use crate::storage::*; use async_trait::async_trait; use downcast::{downcast_sync, AnySync}; -use zeroize::Zeroize; - -/// AccessContextT is a trait that wraps a wallet secret -/// (or possibly other parameters in the future) -/// needed for accessing stored wallet data. -#[async_trait] -pub trait AccessContextT: Send + Sync { - async fn wallet_secret(&self) -> Secret; -} - -/// AccessContext is a wrapper for wallet secret that implements -/// the [`AccessContextT`] trait. -#[derive(Clone)] -pub struct AccessContext { - pub(crate) wallet_secret: Secret, -} -impl AccessContext { - pub fn new(wallet_secret: Secret) -> Self { - Self { wallet_secret } - } -} - -#[async_trait] -impl AccessContextT for AccessContext { - async fn wallet_secret(&self) -> Secret { - self.wallet_secret.clone() - } +#[derive(Debug, Clone)] +pub struct WalletExportOptions { + pub include_transactions: bool, } -impl Zeroize for AccessContext { - fn zeroize(&mut self) { - self.wallet_secret.zeroize() - } -} - -impl Drop for AccessContext { - fn drop(&mut self) { - self.zeroize() - } -} - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] +#[wasm_bindgen(inspectable)] pub struct WalletDescriptor { + #[wasm_bindgen(getter_with_clone)] pub title: Option, + #[wasm_bindgen(getter_with_clone)] pub filename: String, } @@ -58,28 +28,49 @@ impl WalletDescriptor { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "kind", content = "meta")] +pub enum StorageDescriptor { + Resident, + Internal(String), + Other(String), +} + +impl std::fmt::Display for StorageDescriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StorageDescriptor::Resident => write!(f, "memory(resident)"), + StorageDescriptor::Internal(path) => write!(f, "{path}"), + StorageDescriptor::Other(other) => write!(f, "{other}"), + } + } +} + pub type StorageStream = Pin> + Send>>; #[async_trait] pub trait PrvKeyDataStore: Send + Sync { async fn iter(&self) -> Result>>; async fn load_key_info(&self, id: &PrvKeyDataId) -> Result>>; - async fn load_key_data(&self, ctx: &Arc, id: &PrvKeyDataId) -> Result>; - async fn store(&self, ctx: &Arc, data: PrvKeyData) -> Result<()>; - async fn remove(&self, ctx: &Arc, id: &PrvKeyDataId) -> Result<()>; + async fn load_key_data(&self, wallet_secret: &Secret, id: &PrvKeyDataId) -> Result>; + async fn store(&self, wallet_secret: &Secret, data: PrvKeyData) -> Result<()>; + async fn remove(&self, wallet_secret: &Secret, id: &PrvKeyDataId) -> Result<()>; } #[async_trait] pub trait AccountStore: Send + Sync { - async fn iter(&self, prv_key_data_id_filter: Option) - -> Result, Option>)>>; + async fn iter( + &self, + prv_key_data_id_filter: Option, + ) -> Result, Option>)>>; async fn len(&self, prv_key_data_id_filter: Option) -> Result; - async fn load_single(&self, ids: &AccountId) -> Result, Option>)>>; - // async fn load_multiple(&self, ids: &[AccountId]) -> Result,Option>)>>; - async fn store_single(&self, account: &Account, metadata: Option<&Metadata>) -> Result<()>; - async fn store_multiple(&self, data: &[(&Account, Option<&Metadata>)]) -> Result<()>; + async fn load_single(&self, ids: &AccountId) -> Result, Option>)>>; + async fn load_multiple(&self, ids: &[AccountId]) -> Result, Option>)>>; + async fn store_single(&self, account: &AccountStorage, metadata: Option<&AccountMetadata>) -> Result<()>; + async fn store_multiple(&self, data: Vec<(AccountStorage, Option)>) -> Result<()>; async fn remove(&self, id: &[&AccountId]) -> Result<()>; - async fn update_metadata(&self, metadata: &[&Metadata]) -> Result<()>; + async fn update_metadata(&self, metadata: Vec) -> Result<()>; } #[async_trait] @@ -92,10 +83,23 @@ pub trait AddressBookStore: Send + Sync { } } +pub struct TransactionRangeResult { + pub transactions: Vec>, + pub total: u64, +} + #[async_trait] pub trait TransactionRecordStore: Send + Sync { async fn transaction_id_iter(&self, binding: &Binding, network_id: &NetworkId) -> Result>>; - // async fn transaction_iter(&self, binding: &Binding, network_id: &NetworkId) -> Result>; + async fn transaction_data_iter(&self, binding: &Binding, network_id: &NetworkId) -> Result>>; + async fn load_range( + &self, + binding: &Binding, + network_id: &NetworkId, + filter: Option>, + range: std::ops::Range, + ) -> Result; + async fn load_single(&self, binding: &Binding, network_id: &NetworkId, id: &TransactionId) -> Result>; async fn load_multiple( &self, @@ -103,23 +107,44 @@ pub trait TransactionRecordStore: Send + Sync { network_id: &NetworkId, ids: &[TransactionId], ) -> Result>>; + async fn store(&self, transaction_records: &[&TransactionRecord]) -> Result<()>; async fn remove(&self, binding: &Binding, network_id: &NetworkId, ids: &[&TransactionId]) -> Result<()>; - async fn store_transaction_metadata(&self, id: TransactionId, metadata: TransactionMetadata) -> Result<()>; + async fn store_transaction_note( + &self, + binding: &Binding, + network_id: &NetworkId, + id: TransactionId, + note: Option, + ) -> Result<()>; + async fn store_transaction_metadata( + &self, + binding: &Binding, + network_id: &NetworkId, + id: TransactionId, + metadata: Option, + ) -> Result<()>; } #[derive(Debug)] pub struct CreateArgs { pub title: Option, pub filename: Option, + pub encryption_kind: EncryptionKind, pub user_hint: Option, pub overwrite_wallet: bool, } impl CreateArgs { - pub fn new(title: Option, filename: Option, user_hint: Option, overwrite_wallet: bool) -> Self { - Self { title, filename, user_hint, overwrite_wallet } + pub fn new( + title: Option, + filename: Option, + encryption_kind: EncryptionKind, + user_hint: Option, + overwrite_wallet: bool, + ) -> Self { + Self { title, filename, encryption_kind, user_hint, overwrite_wallet } } } @@ -143,32 +168,47 @@ pub trait Interface: Send + Sync + AnySync { fn is_open(&self) -> bool; /// return storage information string (file location) - fn descriptor(&self) -> Result>; + fn location(&self) -> Result; /// returns the name of the currently open wallet or none - fn name(&self) -> Option; + fn descriptor(&self) -> Option; + + /// encryption used by the currently open wallet + fn encryption_kind(&self) -> Result; + + /// rename the currently open wallet (title or the filename) + async fn rename(&self, wallet_secret: &Secret, title: Option<&str>, filename: Option<&str>) -> Result<()>; + + /// change the secret of the currently open wallet + async fn change_secret(&self, old_wallet_secret: &Secret, new_wallet_secret: &Secret) -> Result<()>; /// checks if the wallet storage is present async fn exists(&self, name: Option<&str>) -> Result; /// initialize wallet storage - async fn create(&self, ctx: &Arc, args: CreateArgs) -> Result<()>; + async fn create(&self, wallet_secret: &Secret, args: CreateArgs) -> Result; /// establish an open state (load wallet data cache, connect to the database etc.) - async fn open(&self, ctx: &Arc, args: OpenArgs) -> Result<()>; + async fn open(&self, wallet_secret: &Secret, args: OpenArgs) -> Result<()>; /// suspend commit operations until flush() is called async fn batch(&self) -> Result<()>; /// flush resumes commit operations previously suspended by `suspend()` - async fn flush(&self, ctx: &Arc) -> Result<()>; + async fn flush(&self, wallet_secret: &Secret) -> Result<()>; /// commit any changes changes to storage - async fn commit(&self, ctx: &Arc) -> Result<()>; + async fn commit(&self, wallet_secret: &Secret) -> Result<()>; /// stop the storage subsystem async fn close(&self) -> Result<()>; + /// export the wallet data + async fn wallet_export(&self, wallet_secret: &Secret, options: WalletExportOptions) -> Result>; + + /// import the wallet data + async fn wallet_import(&self, wallet_secret: &Secret, serialized_wallet_storage: &[u8]) -> Result; + // ~~~ // phishing hint (user-created text string identifying authenticity of the wallet) diff --git a/wallet/core/src/storage/keydata.rs b/wallet/core/src/storage/keydata.rs deleted file mode 100644 index e9ab4dfa7..000000000 --- a/wallet/core/src/storage/keydata.rs +++ /dev/null @@ -1,391 +0,0 @@ -use crate::derivation::create_xpub_from_xprv; -use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; -use faster_hex::{hex_decode, hex_string}; -use kash_bip32::{ExtendedPrivateKey, ExtendedPublicKey, Language, Mnemonic}; -use secp256k1::SecretKey; -use serde::Serializer; -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -#[allow(unused_imports)] -use workflow_core::runtime; -use xxhash_rust::xxh3::xxh3_64; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -use crate::storage::{AccountKind, Encryptable}; - -#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, BorshSerialize, Ord, PartialOrd)] -pub struct KeyDataId(pub(crate) [u8; 8]); - -impl KeyDataId { - pub fn new(id: u64) -> Self { - KeyDataId(id.to_le_bytes()) - } - - pub fn new_from_slice(vec: &[u8]) -> Self { - Self(<[u8; 8]>::try_from(<&[u8]>::clone(&vec)).expect("Error: invalid slice size for id")) - } -} - -impl ToHex for KeyDataId { - fn to_hex(&self) -> String { - self.0.to_vec().to_hex() - } -} - -impl FromHex for KeyDataId { - type Error = Error; - fn from_hex(hex_str: &str) -> Result { - let mut data = vec![0u8; hex_str.len() / 2]; - hex_decode(hex_str.as_bytes(), &mut data)?; - Ok(Self::new_from_slice(&data)) - } -} - -impl std::fmt::Debug for KeyDataId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "KeyDataId ( {:?} )", self.0) - } -} - -impl std::fmt::Display for KeyDataId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "KeyDataId ( {:?} )", self.0) - } -} - -impl Serialize for KeyDataId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&hex_string(&self.0)) - } -} - -impl<'de> Deserialize<'de> for KeyDataId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = ::deserialize(deserializer)?; - let mut data = vec![0u8; s.len() / 2]; - hex_decode(s.as_bytes(), &mut data).map_err(serde::de::Error::custom)?; - Ok(Self::new_from_slice(&data)) - } -} - -impl Zeroize for KeyDataId { - fn zeroize(&mut self) { - self.0.zeroize(); - } -} - -pub type PrvKeyDataId = KeyDataId; -pub type PrvKeyDataMap = HashMap; - -/// Indicates key capabilities in the context of Kash -/// core (kash-wallet) or legacy (KDX/PWA) wallets. -/// The setting is based on the type of key import. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum KeyCaps { - // 24 word mnemonic, bip39 seed accounts - MultipleAccounts, - // 12 word mnemonic (legacy) - SingleAccount, -} - -impl KeyCaps { - pub fn from_mnemonic_phrase(phrase: &str) -> Self { - let data = phrase.split_whitespace().collect::>(); - if data.len() == 12 { - KeyCaps::SingleAccount - } else { - KeyCaps::MultipleAccounts - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -#[serde(tag = "key-variant", content = "key-data")] -pub enum PrvKeyVariant { - // 12 (legacy) or 24 word Bip39 mnemonic - Mnemonic(String), - // Bip39 seed (generated from mnemonic) - Bip39Seed(String), - // Extended Private Key (XPrv) - ExtendedPrivateKey(String), - // SecretKey - SecretKey(String), -} - -impl PrvKeyVariant { - pub fn from_mnemonic(mnemonic: Mnemonic) -> Self { - PrvKeyVariant::Mnemonic(mnemonic.phrase_string()) - } - - pub fn from_secret_key(secret_key: SecretKey) -> Self { - PrvKeyVariant::SecretKey(secret_key.secret_bytes().to_vec().to_hex()) - } - - pub fn get_string(&self) -> Zeroizing { - match self { - PrvKeyVariant::Mnemonic(s) => Zeroizing::new(s.clone()), - PrvKeyVariant::Bip39Seed(s) => Zeroizing::new(s.clone()), - PrvKeyVariant::ExtendedPrivateKey(s) => Zeroizing::new(s.clone()), - PrvKeyVariant::SecretKey(s) => Zeroizing::new(s.clone()), - } - } - - pub fn id(&self) -> KeyDataId { - let s = self.get_string(); - PrvKeyDataId::new(xxh3_64(s.as_bytes())) - } - - // pub fn get_caps(&self) -> KeyCaps { - // match self { - // PrvKeyVariant::Mnemonic(phrase) => { KeyCaps::from_mnemonic_phrase(phrase) }, - // PrvKeyVariant::Bip39Seed(_) => { KeyCaps::MultipleAccounts }, - // PrvKeyVariant::ExtendedPrivateKey(_) => { KeyCaps::SingleAccount }, - // } - // } - - // pub fn get_as_bytes(&self) -> Zeroizing<&[u8]> { - // match self { - // PrvKeyVariant::Mnemonic(s) => Zeroizing::new(s.clone()), - // PrvKeyVariant::ExtendedPrivateKey(s) => Zeroizing::new(s.clone()), - // PrvKeyVariant::Seed(s) => Zeroizing::new(s.clone()), - // } - // } -} - -impl Zeroize for PrvKeyVariant { - fn zeroize(&mut self) { - match self { - PrvKeyVariant::Mnemonic(s) => s.zeroize(), - PrvKeyVariant::Bip39Seed(s) => s.zeroize(), - PrvKeyVariant::ExtendedPrivateKey(s) => s.zeroize(), - PrvKeyVariant::SecretKey(s) => s.zeroize(), - } - } -} -impl Drop for PrvKeyVariant { - fn drop(&mut self) { - self.zeroize() - } -} - -impl ZeroizeOnDrop for PrvKeyVariant {} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PrvKeyDataPayload { - prv_key_variant: PrvKeyVariant, -} - -impl PrvKeyDataPayload { - pub fn try_new_with_mnemonic(mnemonic: Mnemonic) -> Result { - Ok(Self { prv_key_variant: PrvKeyVariant::from_mnemonic(mnemonic) }) - } - - pub fn try_new_with_secret_key(secret_key: SecretKey) -> Result { - Ok(Self { prv_key_variant: PrvKeyVariant::from_secret_key(secret_key) }) - } - - pub fn get_xprv(&self, payment_secret: Option<&Secret>) -> Result> { - let payment_secret = payment_secret.map(|s| std::str::from_utf8(s.as_ref())).transpose()?; - - match &self.prv_key_variant { - PrvKeyVariant::Mnemonic(mnemonic) => { - let mnemonic = Mnemonic::new(mnemonic, Language::English)?; - let xkey = ExtendedPrivateKey::::new(mnemonic.to_seed(payment_secret.unwrap_or_default()))?; - Ok(xkey) - } - PrvKeyVariant::Bip39Seed(seed) => { - let seed = Zeroizing::new(Vec::from_hex(seed.as_ref())?); - let xkey = ExtendedPrivateKey::::new(seed)?; - Ok(xkey) - } - PrvKeyVariant::ExtendedPrivateKey(extended_private_key) => { - let xkey: ExtendedPrivateKey = extended_private_key.parse()?; - Ok(xkey) - } - PrvKeyVariant::SecretKey(_) => Err(Error::XPrvSupport), - } - } - - pub fn as_mnemonic(&self) -> Result> { - match &self.prv_key_variant { - PrvKeyVariant::Mnemonic(mnemonic) => Ok(Some(Mnemonic::new(mnemonic.clone(), Language::English)?)), - _ => Ok(None), - } - } - - pub fn as_variant(&self) -> Zeroizing { - Zeroizing::new(self.prv_key_variant.clone()) - } - - pub fn as_secret_key(&self) -> Result> { - match &self.prv_key_variant { - PrvKeyVariant::SecretKey(private_key) => Ok(Some(SecretKey::from_str(private_key)?)), - _ => Ok(None), - } - } - - pub fn id(&self) -> PrvKeyDataId { - self.prv_key_variant.id() - } -} - -impl Zeroize for PrvKeyDataPayload { - fn zeroize(&mut self) { - self.prv_key_variant.zeroize(); - } -} - -impl Drop for PrvKeyDataPayload { - fn drop(&mut self) { - self.zeroize() - } -} - -impl ZeroizeOnDrop for PrvKeyDataPayload {} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PrvKeyData { - pub id: PrvKeyDataId, - pub name: Option, - pub key_caps: KeyCaps, - pub payload: Encryptable, -} - -impl PrvKeyData { - pub async fn create_xpub( - &self, - payment_secret: Option<&Secret>, - account_kind: AccountKind, - account_index: u64, - ) -> Result> { - let payload = self.payload.decrypt(payment_secret)?; - let xprv = payload.get_xprv(payment_secret)?; - create_xpub_from_xprv(xprv, account_kind, account_index).await - } - - pub fn get_xprv(&self, payment_secret: Option<&Secret>) -> Result> { - let payload = self.payload.decrypt(payment_secret)?; - payload.get_xprv(payment_secret) - } - - pub fn as_mnemonic(&self, payment_secret: Option<&Secret>) -> Result> { - let payload = self.payload.decrypt(payment_secret)?; - payload.as_mnemonic() - } - - pub fn as_variant(&self, payment_secret: Option<&Secret>) -> Result> { - let payload = self.payload.decrypt(payment_secret)?; - Ok(payload.as_variant()) - } -} - -impl TryFrom<(Mnemonic, Option<&Secret>)> for PrvKeyData { - type Error = Error; - fn try_from((mnemonic, payment_secret): (Mnemonic, Option<&Secret>)) -> Result { - let account_caps = KeyCaps::from_mnemonic_phrase(mnemonic.phrase()); - let key_data_payload = PrvKeyDataPayload::try_new_with_mnemonic(mnemonic)?; - let key_data_payload_id = key_data_payload.id(); - let key_data_payload = Encryptable::Plain(key_data_payload); - - let mut prv_key_data = PrvKeyData::new(key_data_payload_id, None, account_caps, key_data_payload); - if let Some(payment_secret) = payment_secret { - prv_key_data.encrypt(payment_secret)?; - } - - Ok(prv_key_data) - } -} - -impl Zeroize for PrvKeyData { - fn zeroize(&mut self) { - self.id.zeroize(); - self.name.zeroize(); - self.payload.zeroize(); - } -} - -impl Drop for PrvKeyData { - fn drop(&mut self) { - self.zeroize(); - } -} - -impl PrvKeyData { - pub fn new(id: PrvKeyDataId, name: Option, key_caps: KeyCaps, payload: Encryptable) -> Self { - Self { id, payload, key_caps, name } - } - - pub fn try_new_from_mnemonic(mnemonic: Mnemonic, payment_secret: Option<&Secret>) -> Result { - let key_caps = KeyCaps::from_mnemonic_phrase(mnemonic.phrase()); - let payload = PrvKeyDataPayload::try_new_with_mnemonic(mnemonic)?; - let mut prv_key_data = Self { id: payload.id(), payload: Encryptable::Plain(payload), key_caps, name: None }; - if let Some(payment_secret) = payment_secret { - prv_key_data.encrypt(payment_secret)?; - } - - Ok(prv_key_data) - } - - pub fn try_new_from_secret_key(secret_key: SecretKey, payment_secret: Option<&Secret>) -> Result { - let key_caps = KeyCaps::SingleAccount; - let payload = PrvKeyDataPayload::try_new_with_secret_key(secret_key)?; - let mut prv_key_data = Self { id: payload.id(), payload: Encryptable::Plain(payload), key_caps, name: None }; - if let Some(payment_secret) = payment_secret { - prv_key_data.encrypt(payment_secret)?; - } - - Ok(prv_key_data) - } - - pub fn encrypt(&mut self, secret: &Secret) -> Result<()> { - self.payload = self.payload.into_encrypted(secret)?; - Ok(()) - } -} - -#[derive(Clone, Debug)] -pub struct PrvKeyDataInfo { - pub id: PrvKeyDataId, - pub name: Option, - pub key_caps: KeyCaps, - pub is_encrypted: bool, -} - -impl From<&PrvKeyData> for PrvKeyDataInfo { - fn from(data: &PrvKeyData) -> Self { - Self::new(data.id, data.name.clone(), data.key_caps.clone(), data.payload.is_encrypted()) - } -} - -impl PrvKeyDataInfo { - pub fn new(id: PrvKeyDataId, name: Option, key_caps: KeyCaps, is_encrypted: bool) -> Self { - Self { id, name, key_caps, is_encrypted } - } - - pub fn is_encrypted(&self) -> bool { - self.is_encrypted - } -} - -impl Display for PrvKeyDataInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if let Some(name) = &self.name { - write!(f, "{} ({})", name, self.id.to_hex())?; - } else { - write!(f, "{}", self.id.to_hex())?; - } - Ok(()) - } -} diff --git a/wallet/core/src/storage/keydata/assoc.rs b/wallet/core/src/storage/keydata/assoc.rs new file mode 100644 index 000000000..a8f38bbef --- /dev/null +++ b/wallet/core/src/storage/keydata/assoc.rs @@ -0,0 +1,112 @@ +use crate::imports::*; +use itertools::Either; +use std::iter::{empty, once, Empty, Once}; + +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "lowercase")] +#[serde(tag = "type", content = "data")] +pub enum AssocPrvKeyDataIds { + None, + Single(PrvKeyDataId), + Multiple(Arc>), +} + +impl IntoIterator for &AssocPrvKeyDataIds { + type Item = PrvKeyDataId; + type IntoIter = Either, Once>, std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + match self { + AssocPrvKeyDataIds::None => Either::Left(Either::Left(empty())), + AssocPrvKeyDataIds::Single(id) => Either::Left(Either::Right(once(*id))), + AssocPrvKeyDataIds::Multiple(ids) => Either::Right((**ids).clone().into_iter()), + } + } +} + +impl From for AssocPrvKeyDataIds { + fn from(value: PrvKeyDataId) -> Self { + AssocPrvKeyDataIds::Single(value) + } +} + +impl TryFrom>>> for AssocPrvKeyDataIds { + type Error = Error; + + fn try_from(value: Option>>) -> Result { + match value { + None => Ok(AssocPrvKeyDataIds::None), + Some(ids) => { + if ids.is_empty() { + return Err(Error::AssocPrvKeyDataIdsEmpty); + } + Ok(AssocPrvKeyDataIds::Multiple(ids)) + } + } + } +} + +impl TryFrom for PrvKeyDataId { + type Error = Error; + + fn try_from(value: AssocPrvKeyDataIds) -> Result { + match value { + AssocPrvKeyDataIds::Single(id) => Ok(id), + _ => Err(Error::AssocPrvKeyDataIds("Single".to_string(), value)), + } + } +} + +impl TryFrom for Arc> { + type Error = Error; + + fn try_from(value: AssocPrvKeyDataIds) -> Result { + match value { + AssocPrvKeyDataIds::Multiple(ids) => Ok(ids), + _ => Err(Error::AssocPrvKeyDataIds("Multiple".to_string(), value)), + } + } +} + +impl TryFrom for Option>> { + type Error = Error; + + fn try_from(value: AssocPrvKeyDataIds) -> Result { + match value { + AssocPrvKeyDataIds::None => Ok(None), + AssocPrvKeyDataIds::Multiple(ids) => Ok(Some(ids)), + _ => Err(Error::AssocPrvKeyDataIds("None or Multiple".to_string(), value)), + } + } +} + +impl AssocPrvKeyDataIds { + pub fn contains(&self, id: &PrvKeyDataId) -> bool { + match self { + AssocPrvKeyDataIds::None => false, + AssocPrvKeyDataIds::Single(single) => single == id, + AssocPrvKeyDataIds::Multiple(multiple) => multiple.contains(id), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_assoc_prv_key_data_ids() -> Result<()> { + let id = PrvKeyDataId::new(0x1ee7c0de); + let vec = vec![PrvKeyDataId::new(0x1ee7c0de), PrvKeyDataId::new(0xbaadc0de), PrvKeyDataId::new(0xba5ec0de)]; + + let iter = AssocPrvKeyDataIds::Single(id).into_iter(); + iter.for_each(|id| assert_eq!(id, id)); + + let iter = AssocPrvKeyDataIds::Multiple(vec.clone().into()).into_iter(); + for (idx, id) in iter.enumerate() { + assert_eq!(id, vec[idx]); + } + + Ok(()) + } +} diff --git a/wallet/core/src/storage/keydata/data.rs b/wallet/core/src/storage/keydata/data.rs new file mode 100644 index 000000000..aa0fff63b --- /dev/null +++ b/wallet/core/src/storage/keydata/data.rs @@ -0,0 +1,316 @@ +//! +//! Private key storage and encryption. +//! + +use crate::derivation::create_xpub_from_xprv; +use crate::imports::*; +use kash_bip32::{ExtendedPrivateKey, ExtendedPublicKey, Language, Mnemonic}; +use kash_utils::hex::ToHex; +use secp256k1::SecretKey; +use xxhash_rust::xxh3::xxh3_64; + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum PrvKeyDataVariantKind { + Mnemonic, + Bip39Seed, + ExtendedPrivateKey, + SecretKey, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "key-variant", content = "key-data")] +pub enum PrvKeyDataVariant { + // 12 or 24 word bip39 mnemonic + Mnemonic(String), + // Bip39 seed (generated from mnemonic) + Bip39Seed(String), + // Extended Private Key (XPrv) + ExtendedPrivateKey(String), + // secp256k1::SecretKey + SecretKey(String), +} + +impl BorshSerialize for PrvKeyDataVariant { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::MAGIC, Self::VERSION).serialize(writer)?; + let kind = self.kind(); + let string = self.get_string(); + BorshSerialize::serialize(&kind, writer)?; + BorshSerialize::serialize(string.as_str(), writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for PrvKeyDataVariant { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = StorageHeader::deserialize(buf)?.try_magic(Self::MAGIC)?.try_version(Self::VERSION)?; + + let kind: PrvKeyDataVariantKind = BorshDeserialize::deserialize(buf)?; + let string: String = BorshDeserialize::deserialize(buf)?; + + match kind { + PrvKeyDataVariantKind::Mnemonic => Ok(Self::Mnemonic(string)), + PrvKeyDataVariantKind::Bip39Seed => Ok(Self::Bip39Seed(string)), + PrvKeyDataVariantKind::ExtendedPrivateKey => Ok(Self::ExtendedPrivateKey(string)), + PrvKeyDataVariantKind::SecretKey => Ok(Self::SecretKey(string)), + } + } +} + +impl PrvKeyDataVariant { + const MAGIC: u32 = 0x5652504b; + const VERSION: u32 = 0; + + pub fn kind(&self) -> PrvKeyDataVariantKind { + match self { + PrvKeyDataVariant::Mnemonic(_) => PrvKeyDataVariantKind::Mnemonic, + PrvKeyDataVariant::Bip39Seed(_) => PrvKeyDataVariantKind::Bip39Seed, + PrvKeyDataVariant::ExtendedPrivateKey(_) => PrvKeyDataVariantKind::ExtendedPrivateKey, + PrvKeyDataVariant::SecretKey(_) => PrvKeyDataVariantKind::SecretKey, + } + } + + pub fn from_mnemonic(mnemonic: Mnemonic) -> Self { + PrvKeyDataVariant::Mnemonic(mnemonic.phrase_string()) + } + + pub fn from_secret_key(secret_key: SecretKey) -> Self { + PrvKeyDataVariant::SecretKey(secret_key.secret_bytes().to_vec().to_hex()) + } + + pub fn get_string(&self) -> Zeroizing { + match self { + PrvKeyDataVariant::Mnemonic(s) => Zeroizing::new(s.clone()), + PrvKeyDataVariant::Bip39Seed(s) => Zeroizing::new(s.clone()), + PrvKeyDataVariant::ExtendedPrivateKey(s) => Zeroizing::new(s.clone()), + PrvKeyDataVariant::SecretKey(s) => Zeroizing::new(s.clone()), + } + } + + pub fn id(&self) -> PrvKeyDataId { + let s = PrvKeyDataVariant::get_string(self); //self.get_string(); + PrvKeyDataId::new(xxh3_64(s.as_bytes())) + } +} + +impl Zeroize for PrvKeyDataVariant { + fn zeroize(&mut self) { + match self { + PrvKeyDataVariant::Mnemonic(s) => s.zeroize(), + PrvKeyDataVariant::Bip39Seed(s) => s.zeroize(), + PrvKeyDataVariant::ExtendedPrivateKey(s) => s.zeroize(), + PrvKeyDataVariant::SecretKey(s) => s.zeroize(), + } + } +} +impl Drop for PrvKeyDataVariant { + fn drop(&mut self) { + self.zeroize() + } +} + +impl ZeroizeOnDrop for PrvKeyDataVariant {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyDataPayload { + prv_key_variant: PrvKeyDataVariant, +} + +impl PrvKeyDataPayload { + pub fn try_new_with_mnemonic(mnemonic: Mnemonic) -> Result { + Ok(Self { prv_key_variant: PrvKeyDataVariant::from_mnemonic(mnemonic) }) + } + + pub fn try_new_with_secret_key(secret_key: SecretKey) -> Result { + Ok(Self { prv_key_variant: PrvKeyDataVariant::from_secret_key(secret_key) }) + } + + pub fn get_xprv(&self, payment_secret: Option<&Secret>) -> Result> { + let payment_secret = payment_secret.map(|s| std::str::from_utf8(s.as_ref())).transpose()?; + + match &self.prv_key_variant { + PrvKeyDataVariant::Mnemonic(mnemonic) => { + let mnemonic = Mnemonic::new(mnemonic, Language::English)?; + let xkey = ExtendedPrivateKey::::new(mnemonic.to_seed(payment_secret.unwrap_or_default()))?; + Ok(xkey) + } + PrvKeyDataVariant::Bip39Seed(seed) => { + let seed = Zeroizing::new(Vec::from_hex(seed.as_ref())?); + let xkey = ExtendedPrivateKey::::new(seed)?; + Ok(xkey) + } + PrvKeyDataVariant::ExtendedPrivateKey(extended_private_key) => { + let xkey: ExtendedPrivateKey = extended_private_key.parse()?; + Ok(xkey) + } + PrvKeyDataVariant::SecretKey(_) => Err(Error::XPrvSupport), + } + } + + pub fn as_mnemonic(&self) -> Result> { + match &self.prv_key_variant { + PrvKeyDataVariant::Mnemonic(mnemonic) => Ok(Some(Mnemonic::new(mnemonic.clone(), Language::English)?)), + _ => Ok(None), + } + } + + pub fn as_variant(&self) -> Zeroizing { + Zeroizing::new(self.prv_key_variant.clone()) + } + + pub fn as_secret_key(&self) -> Result> { + match &self.prv_key_variant { + PrvKeyDataVariant::SecretKey(private_key) => Ok(Some(SecretKey::from_str(private_key)?)), + _ => Ok(None), + } + } + + pub fn id(&self) -> PrvKeyDataId { + self.prv_key_variant.id() + } +} + +impl Zeroize for PrvKeyDataPayload { + fn zeroize(&mut self) { + self.prv_key_variant.zeroize(); + } +} + +impl Drop for PrvKeyDataPayload { + fn drop(&mut self) { + self.zeroize() + } +} + +impl ZeroizeOnDrop for PrvKeyDataPayload {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrvKeyData { + pub id: PrvKeyDataId, + pub name: Option, + pub payload: Encryptable, +} + +impl PrvKeyData { + pub async fn create_xpub( + &self, + payment_secret: Option<&Secret>, + account_kind: AccountKind, + account_index: u64, + ) -> Result> { + let payload = self.payload.decrypt(payment_secret)?; + let xprv = payload.get_xprv(payment_secret)?; + create_xpub_from_xprv(xprv, account_kind, account_index).await + } + + pub fn get_xprv(&self, payment_secret: Option<&Secret>) -> Result> { + let payload = self.payload.decrypt(payment_secret)?; + payload.get_xprv(payment_secret) + } + + pub fn as_mnemonic(&self, payment_secret: Option<&Secret>) -> Result> { + let payload = self.payload.decrypt(payment_secret)?; + payload.as_mnemonic() + } + + pub fn as_variant(&self, payment_secret: Option<&Secret>) -> Result> { + let payload = self.payload.decrypt(payment_secret)?; + Ok(payload.as_variant()) + } + + pub fn try_from_mnemonic(mnemonic: Mnemonic, payment_secret: Option<&Secret>, encryption_kind: EncryptionKind) -> Result { + let key_data_payload = PrvKeyDataPayload::try_new_with_mnemonic(mnemonic)?; + let key_data_payload_id = key_data_payload.id(); + let key_data_payload = Encryptable::Plain(key_data_payload); + + let mut prv_key_data = PrvKeyData::new(key_data_payload_id, None, key_data_payload); + if let Some(payment_secret) = payment_secret { + prv_key_data.encrypt(payment_secret, encryption_kind)?; + } + + Ok(prv_key_data) + } +} + +impl AsRef for PrvKeyData { + fn as_ref(&self) -> &PrvKeyData { + self + } +} + +impl Zeroize for PrvKeyData { + fn zeroize(&mut self) { + self.id.zeroize(); + self.name.zeroize(); + self.payload.zeroize(); + } +} + +impl Drop for PrvKeyData { + fn drop(&mut self) { + self.zeroize(); + } +} + +impl PrvKeyData { + pub fn new(id: PrvKeyDataId, name: Option, payload: Encryptable) -> Self { + Self { id, payload, name } + } + + pub fn try_new_from_mnemonic( + mnemonic: Mnemonic, + payment_secret: Option<&Secret>, + encryption_kind: EncryptionKind, + ) -> Result { + let payload = PrvKeyDataPayload::try_new_with_mnemonic(mnemonic)?; + let mut prv_key_data = Self { id: payload.id(), payload: Encryptable::Plain(payload), name: None }; + if let Some(payment_secret) = payment_secret { + prv_key_data.encrypt(payment_secret, encryption_kind)?; + } + + Ok(prv_key_data) + } + + pub fn try_new_from_secret_key( + secret_key: SecretKey, + payment_secret: Option<&Secret>, + encryption_kind: EncryptionKind, + ) -> Result { + let payload = PrvKeyDataPayload::try_new_with_secret_key(secret_key)?; + let mut prv_key_data = Self { id: payload.id(), payload: Encryptable::Plain(payload), name: None }; + if let Some(payment_secret) = payment_secret { + prv_key_data.encrypt(payment_secret, encryption_kind)?; + } + + Ok(prv_key_data) + } + + pub fn encrypt(&mut self, secret: &Secret, encryption_kind: EncryptionKind) -> Result<()> { + self.payload = self.payload.into_encrypted(secret, encryption_kind)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_prv_key_data() -> Result<()> { + let storable_in = PrvKeyDataVariant::Bip39Seed("lorem ipsum".to_string()); + let guard = StorageGuard::new(&storable_in); + let storable_out = guard.validate()?; + + match &storable_out { + PrvKeyDataVariant::Bip39Seed(s) => assert_eq!(s, "lorem ipsum"), + _ => unreachable!("invalid prv key variant storage data"), + } + + Ok(()) + } +} diff --git a/wallet/core/src/storage/keydata/id.rs b/wallet/core/src/storage/keydata/id.rs new file mode 100644 index 000000000..1906abcf9 --- /dev/null +++ b/wallet/core/src/storage/keydata/id.rs @@ -0,0 +1,77 @@ +//! +//! Deterministic private key data ids. +//! + +use crate::imports::*; +use faster_hex::{hex_decode, hex_string}; +use serde::Serializer; + +#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, BorshSerialize, BorshDeserialize)] +pub struct KeyDataId(pub(crate) [u8; 8]); + +impl KeyDataId { + pub fn new(id: u64) -> Self { + KeyDataId(id.to_le_bytes()) + } + + pub fn new_from_slice(vec: &[u8]) -> Self { + Self(<[u8; 8]>::try_from(<&[u8]>::clone(&vec)).expect("Error: invalid slice size for id")) + } +} + +impl ToHex for KeyDataId { + fn to_hex(&self) -> String { + self.0.to_vec().to_hex() + } +} + +impl FromHex for KeyDataId { + type Error = Error; + fn from_hex(hex_str: &str) -> Result { + let mut data = vec![0u8; hex_str.len() / 2]; + hex_decode(hex_str.as_bytes(), &mut data)?; + Ok(Self::new_from_slice(&data)) + } +} + +impl std::fmt::Debug for KeyDataId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "KeyDataId ({})", self.0.as_slice().to_hex()) + } +} + +impl std::fmt::Display for KeyDataId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_slice().to_hex()) + } +} + +impl Serialize for KeyDataId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&hex_string(&self.0)) + } +} + +impl<'de> Deserialize<'de> for KeyDataId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + let mut data = vec![0u8; s.len() / 2]; + hex_decode(s.as_bytes(), &mut data).map_err(serde::de::Error::custom)?; + Ok(Self::new_from_slice(&data)) + } +} + +impl Zeroize for KeyDataId { + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +pub type PrvKeyDataId = KeyDataId; +pub type PrvKeyDataMap = HashMap; diff --git a/wallet/core/src/storage/keydata/info.rs b/wallet/core/src/storage/keydata/info.rs new file mode 100644 index 000000000..9a4af33ec --- /dev/null +++ b/wallet/core/src/storage/keydata/info.rs @@ -0,0 +1,52 @@ +//! +//! Private key data info (reference representation). +//! + +use crate::imports::*; +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct PrvKeyDataInfo { + pub id: PrvKeyDataId, + pub name: Option, + pub is_encrypted: bool, +} + +impl From<&PrvKeyData> for PrvKeyDataInfo { + fn from(data: &PrvKeyData) -> Self { + Self::new(data.id, data.name.clone(), data.payload.is_encrypted()) + } +} + +impl PrvKeyDataInfo { + pub fn new(id: PrvKeyDataId, name: Option, is_encrypted: bool) -> Self { + Self { id, name, is_encrypted } + } + + pub fn is_encrypted(&self) -> bool { + self.is_encrypted + } + + pub fn name_or_id(&self) -> String { + if let Some(name) = &self.name { + name.to_owned() + } else { + self.id.to_hex()[0..16].to_string() + } + } + + pub fn requires_bip39_passphrase(&self) -> bool { + self.is_encrypted + } +} + +impl Display for PrvKeyDataInfo { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Some(name) = &self.name { + write!(f, "{} ({})", name, self.id.to_hex())?; + } else { + write!(f, "{}", self.id.to_hex())?; + } + Ok(()) + } +} diff --git a/wallet/core/src/storage/keydata/mod.rs b/wallet/core/src/storage/keydata/mod.rs new file mode 100644 index 000000000..f91c9484b --- /dev/null +++ b/wallet/core/src/storage/keydata/mod.rs @@ -0,0 +1,15 @@ +//! +//! Private key management primitives. +//! + +pub mod id; +pub use id::*; + +pub mod assoc; +pub use assoc::*; + +pub mod data; +pub use data::*; + +pub mod info; +pub use info::*; diff --git a/wallet/core/src/storage/local/cache.rs b/wallet/core/src/storage/local/cache.rs index fda4228c9..8558f190f 100644 --- a/wallet/core/src/storage/local/cache.rs +++ b/wallet/core/src/storage/local/cache.rs @@ -1,25 +1,27 @@ +//! +//! Wallet data cache retained in memory during the wallet session. +//! + use crate::imports::*; use crate::result::Result; use crate::secret::Secret; -use crate::storage::local::wallet::Wallet; -use crate::storage::local::wallet::WALLET_VERSION; +use crate::storage::local::wallet::WalletStorage; use crate::storage::local::*; -use crate::storage::*; use std::collections::HashMap; pub struct Cache { pub wallet_title: Option, pub user_hint: Option, + pub encryption_kind: EncryptionKind, pub prv_key_data: Encrypted, pub prv_key_data_info: Collection, - pub accounts: Collection, - pub metadata: Collection, + pub accounts: Collection, + pub metadata: Collection, pub address_book: Vec, } -impl TryFrom<(Wallet, &Secret)> for Cache { - type Error = Error; - fn try_from((wallet, secret): (Wallet, &Secret)) -> Result { +impl Cache { + pub fn from_wallet(wallet: WalletStorage, secret: &Secret) -> Result { let payload = wallet.payload(secret)?; let prv_key_data_info = @@ -27,51 +29,56 @@ impl TryFrom<(Wallet, &Secret)> for Cache { let prv_key_data_map = payload.0.prv_key_data.into_iter().map(|pkdata| (pkdata.id, pkdata)).collect::>(); let prv_key_data: Decrypted = Decrypted::new(prv_key_data_map); - let prv_key_data = prv_key_data.encrypt(secret)?; - let accounts: Collection = payload.0.accounts.try_into()?; - let metadata: Collection = wallet.metadata.try_into()?; + let encryption_kind = wallet.encryption_kind; + let prv_key_data = prv_key_data.encrypt(secret, encryption_kind)?; + let accounts: Collection = payload.0.accounts.try_into()?; + let metadata: Collection = wallet.metadata.try_into()?; let user_hint = wallet.user_hint; let wallet_title = wallet.title; let address_book = payload.0.address_book.into_iter().collect(); - Ok(Cache { wallet_title, user_hint, prv_key_data, prv_key_data_info, accounts, metadata, address_book }) + Ok(Cache { wallet_title, user_hint, encryption_kind, prv_key_data, prv_key_data_info, accounts, metadata, address_book }) } -} -impl TryFrom<(Option, Option, Payload, &Secret)> for Cache { - type Error = Error; - fn try_from((wallet_title, user_hint, payload, secret): (Option, Option, Payload, &Secret)) -> Result { + pub fn from_payload( + wallet_title: Option, + user_hint: Option, + payload: Payload, + secret: &Secret, + encryption_kind: EncryptionKind, + ) -> Result { let prv_key_data_info = payload.prv_key_data.iter().map(|pkdata| pkdata.into()).collect::>().try_into()?; let prv_key_data_map = payload.prv_key_data.into_iter().map(|pkdata| (pkdata.id, pkdata)).collect::>(); let prv_key_data: Decrypted = Decrypted::new(prv_key_data_map); - let prv_key_data = prv_key_data.encrypt(secret)?; - let accounts: Collection = payload.accounts.try_into()?; - let metadata: Collection = Collection::default(); + let prv_key_data = prv_key_data.encrypt(secret, encryption_kind)?; + let accounts: Collection = payload.accounts.try_into()?; + let metadata: Collection = Collection::default(); let address_book = payload.address_book.into_iter().collect(); - Ok(Cache { wallet_title, user_hint, prv_key_data, prv_key_data_info, accounts, metadata, address_book }) + Ok(Cache { wallet_title, user_hint, encryption_kind, prv_key_data, prv_key_data_info, accounts, metadata, address_book }) } -} - -impl TryFrom<(&Cache, &Secret)> for Wallet { - type Error = Error; - fn try_from((cache, secret): (&Cache, &Secret)) -> Result { - let prv_key_data: Decrypted = cache.prv_key_data.decrypt(secret)?; + pub fn to_wallet( + &self, + transactions: Option>>>, + secret: &Secret, + ) -> Result { + let prv_key_data: Decrypted = self.prv_key_data.decrypt(secret)?; let prv_key_data = prv_key_data.values().cloned().collect::>(); - let accounts: Vec = (&cache.accounts).try_into()?; - let metadata: Vec = (&cache.metadata).try_into()?; - let address_book = cache.address_book.clone(); + let accounts: Vec = (&self.accounts).try_into()?; + let metadata: Vec = (&self.metadata).try_into()?; + let address_book = self.address_book.clone(); let payload = Payload::new(prv_key_data, accounts, address_book); - let payload = Decrypted::new(payload).encrypt(secret)?; + let payload = Decrypted::new(payload).encrypt(secret, self.encryption_kind)?; - Ok(Wallet { - version: WALLET_VERSION, + Ok(WalletStorage { + encryption_kind: self.encryption_kind, payload, metadata, - user_hint: cache.user_hint.clone(), - title: cache.wallet_title.clone(), + user_hint: self.user_hint.clone(), + title: self.wallet_title.clone(), + transactions, }) } } diff --git a/wallet/core/src/storage/local/collection.rs b/wallet/core/src/storage/local/collection.rs index 372dc910e..85cfb6aec 100644 --- a/wallet/core/src/storage/local/collection.rs +++ b/wallet/core/src/storage/local/collection.rs @@ -1,3 +1,7 @@ +//! +//! Ordered collections used to store wallet primitives. +//! + use crate::error::Error; use crate::result::Result; use std::collections::HashMap; @@ -77,15 +81,15 @@ where Ok(()) } - pub fn store_multiple(&mut self, data: &[&Data]) -> Result<()> { - for data in data.iter() { - let id = data.id(); - if self.map.get(id).is_some() { - self.map.remove(id); - self.vec.retain(|d| d.id() != id); + pub fn store_multiple(&mut self, data: Vec) -> Result<()> { + for data in data.into_iter() { + let id = data.id().clone(); + if self.map.get(&id).is_some() { + self.map.remove(&id); + self.vec.retain(|d| d.id() != &id); } - let data = Arc::new((*data).clone()); + let data = Arc::new(data); self.map.insert(id.clone(), data.clone()); self.vec.push(data); } @@ -110,13 +114,12 @@ where } pub fn load_multiple(&self, ids: &[Id]) -> Result>> { - Ok(ids - .iter() + ids.iter() .map(|id| match self.map.get(id).cloned() { - Some(data) => data, - None => panic!("requested id `{}` was not found in collection", id), + Some(data) => Ok(data), + None => Err(Error::KeyId(id.to_string())), }) - .collect()) + .collect::>>() } pub fn range(&self, range: std::ops::Range) -> Result>> { diff --git a/wallet/core/src/storage/local/interface.rs b/wallet/core/src/storage/local/interface.rs index 733799f63..220547ee7 100644 --- a/wallet/core/src/storage/local/interface.rs +++ b/wallet/core/src/storage/local/interface.rs @@ -1,18 +1,23 @@ +//! +//! Storage interface implementation capable of storing wallet data +//! in a local file system, web browser localstorage and chrome +//! extension storage. +//! + use crate::imports::*; use crate::result::Result; -use crate::storage::interface::AddressBookStore; -use crate::storage::interface::CreateArgs; -use crate::storage::interface::OpenArgs; -use crate::storage::interface::StorageStream; -use crate::storage::interface::WalletDescriptor; +use crate::secret::Secret; +use crate::storage::interface::{ + AddressBookStore, CreateArgs, OpenArgs, StorageDescriptor, StorageStream, WalletDescriptor, WalletExportOptions, +}; use crate::storage::local::cache::*; use crate::storage::local::streams::*; use crate::storage::local::transaction::*; -use crate::storage::local::wallet::Wallet; +use crate::storage::local::wallet::WalletStorage; use crate::storage::local::Payload; use crate::storage::local::Storage; -use crate::storage::*; use slugify_rs::slugify; +use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use workflow_core::runtime::is_web; @@ -24,25 +29,38 @@ pub fn make_filename(title: &Option, filename: &Option) -> Strin } else if let Some(title) = title { slugify!(title) } else { - super::DEFAULT_WALLET_FILE.to_string() + super::default_wallet_file().to_string() } } +#[derive(Clone)] pub enum Store { Resident, Storage(Storage), } +impl Store { + fn filename(&self) -> Option { + match self { + Store::Resident => None, + Store::Storage(storage) => Some(storage.filename_as_string()), + } + } + + pub fn is_resident(&self) -> bool { + matches!(self, Store::Resident) + } +} + pub(crate) struct LocalStoreInner { - pub cache: Arc>, - pub store: Store, + pub cache: Arc>, + pub store: RwLock>, pub transactions: Arc, pub is_modified: AtomicBool, - pub filename: String, } impl LocalStoreInner { - pub async fn try_create(ctx: &Arc, folder: &str, args: CreateArgs, is_resident: bool) -> Result { + async fn try_create(wallet_secret: &Secret, folder: &str, args: CreateArgs, is_resident: bool) -> Result { let (store, wallet_title, filename) = if is_resident { (Store::Resident, Some("Resident Wallet".to_string()), "resident".to_string()) } else { @@ -58,9 +76,9 @@ impl LocalStoreInner { (Store::Storage(storage), title, filename) }; - let secret = ctx.wallet_secret().await; let payload = Payload::default(); - let cache = Arc::new(Mutex::new(Cache::try_from((wallet_title, args.user_hint, payload, &secret))?)); + let cache = + Arc::new(RwLock::new(Cache::from_payload(wallet_title, args.user_hint, payload, wallet_secret, args.encryption_kind)?)); let is_modified = AtomicBool::new(false); let transactions: Arc = if !is_web() { Arc::new(fsio::TransactionStore::new(folder, &filename)) @@ -68,16 +86,15 @@ impl LocalStoreInner { Arc::new(indexdb::TransactionStore::new(&filename)) }; - Ok(Self { cache, store, is_modified, filename, transactions }) + Ok(Self { cache, store: RwLock::new(Arc::new(store)), is_modified, transactions }) } - pub async fn try_load(ctx: &Arc, folder: &str, args: OpenArgs) -> Result { + async fn try_load(wallet_secret: &Secret, folder: &str, args: OpenArgs) -> Result { let filename = make_filename(&None, &args.filename); let storage = Storage::try_new_with_folder(folder, &format!("{filename}.wallet"))?; - let secret = ctx.wallet_secret().await; - let wallet = Wallet::try_load(&storage).await?; - let cache = Arc::new(Mutex::new(Cache::try_from((wallet, &secret))?)); + let wallet = WalletStorage::try_load(&storage).await?; + let cache = Arc::new(RwLock::new(Cache::from_wallet(wallet, wallet_secret)?)); let is_modified = AtomicBool::new(false); let transactions: Arc = if !is_web() { @@ -86,15 +103,90 @@ impl LocalStoreInner { Arc::new(indexdb::TransactionStore::new(&filename)) }; - Ok(Self { cache, store: Store::Storage(storage), is_modified, filename, transactions }) + Ok(Self { cache, store: RwLock::new(Arc::new(Store::Storage(storage))), is_modified, transactions }) + } + + async fn try_import(wallet_secret: &Secret, folder: &str, serialized_wallet_storage: &[u8]) -> Result { + let wallet = WalletStorage::try_from_slice(serialized_wallet_storage)?; + // Try to decrypt the wallet payload with the provided + // secret. This will block import if the secret is + // not correct. + let _ = wallet.payload(wallet_secret)?; + + let filename = make_filename(&wallet.title, &None); + let storage = Storage::try_new_with_folder(folder, &format!("{filename}.wallet"))?; + if storage.exists_sync()? { + return Err(Error::WalletAlreadyExists); + } + + let cache = Arc::new(RwLock::new(Cache::from_wallet(wallet, wallet_secret)?)); + let is_modified = AtomicBool::new(false); + + let transactions: Arc = if !is_web() { + Arc::new(fsio::TransactionStore::new(folder, &filename)) + } else { + Arc::new(indexdb::TransactionStore::new(&filename)) + }; + + Ok(Self { cache, store: RwLock::new(Arc::new(Store::Storage(storage))), is_modified, transactions }) + } + + async fn try_export(&self, wallet_secret: &Secret, _options: WalletExportOptions) -> Result> { + let wallet = self.cache.read().unwrap().to_wallet(None, wallet_secret)?; + Ok(wallet.try_to_vec()?) + } + + fn storage(&self) -> Arc { + self.store.read().unwrap().clone() + } + + fn rename(&self, filename: &str) -> Result<()> { + let store = (**self.store.read().unwrap()).clone(); + let filename = make_filename(&None, &Some(filename.to_string())); + match store { + Store::Resident => Err(Error::ResidentWallet), + Store::Storage(mut storage) => { + storage.rename_sync(filename.as_str())?; + *self.store.write().unwrap() = Arc::new(Store::Storage(storage)); + Ok(()) + } + } + } + + async fn change_secret(&self, old_secret: &Secret, new_secret: &Secret) -> Result<()> { + match &*self.storage() { + Store::Resident => { + let mut cache = self.cache.write().unwrap(); + let old_prv_key_data: Decrypted = cache.prv_key_data.decrypt(old_secret)?; + let new_prv_key_data = Decrypted::new(old_prv_key_data.unwrap()).encrypt(new_secret, cache.encryption_kind)?; + cache.prv_key_data.replace(new_prv_key_data); + + Ok(()) + } + Store::Storage(ref storage) => { + let wallet = { + let mut cache = self.cache.write().unwrap(); + let old_prv_key_data: Decrypted = cache.prv_key_data.decrypt(old_secret)?; + let new_prv_key_data = Decrypted::new(old_prv_key_data.unwrap()).encrypt(new_secret, cache.encryption_kind)?; + cache.prv_key_data.replace(new_prv_key_data); + + cache.to_wallet(None, new_secret)? + }; + wallet.try_store(storage).await?; + self.set_modified(false); + Ok(()) + } + } } pub async fn update_stored_metadata(&self) -> Result<()> { - match self.store { + match &*self.storage() { Store::Resident => Ok(()), Store::Storage(ref storage) => { - let metadata: Vec = (&self.cache().metadata).try_into()?; - let mut wallet = Wallet::try_load(storage).await?; + // take current metadata, load wallet, replace metadata, store wallet + // this bypasses the cache payload and wallet encryption + let metadata: Vec = (&self.cache.read().unwrap().metadata).try_into()?; + let mut wallet = WalletStorage::try_load(storage).await?; wallet.replace_metadata(metadata); wallet.try_store(storage).await?; Ok(()) @@ -102,24 +194,23 @@ impl LocalStoreInner { } } - pub fn cache(&self) -> MutexGuard { - self.cache.lock().unwrap() - } + // pub fn cache(&self) -> &Cache { + // &self.cache + // } - // pub async fn reload(&self, ctx: &Arc) -> Result<()> { - // let secret = ctx.wallet_secret().await.expect("wallet requires an encryption secret"); - // let wallet = Wallet::try_load(&self.store).await?; - // let cache = Cache::try_from((wallet, &secret))?; - // self.cache.lock().unwrap().replace(cache); - // Ok(()) + // pub fn cache_read(&self) -> RwLockReadGuard { + // self.cache.read().unwrap() // } - pub async fn store(&self, ctx: &Arc) -> Result<()> { - match self.store { + // pub fn cache_write(&self) -> RwLockWriteGuard { + // self.cache.write().unwrap() + // } + + pub async fn store(&self, wallet_secret: &Secret) -> Result<()> { + match &*self.storage() { Store::Resident => Ok(()), Store::Storage(ref storage) => { - let secret = ctx.wallet_secret().await; //.ok_or(Error::WalletSecretRequired)?; - let wallet = Wallet::try_from((&*self.cache(), &secret))?; + let wallet = self.cache.read().unwrap().to_wallet(None, wallet_secret)?; wallet.try_store(storage).await?; self.set_modified(false); Ok(()) @@ -129,7 +220,7 @@ impl LocalStoreInner { #[inline] pub fn set_modified(&self, modified: bool) { - match self.store { + match &*self.storage() { Store::Resident => (), Store::Storage(_) => { self.is_modified.store(modified, Ordering::SeqCst); @@ -139,7 +230,7 @@ impl LocalStoreInner { #[inline] pub fn is_modified(&self) -> bool { - match self.store { + match &*self.storage() { Store::Resident => false, Store::Storage(_) => self.is_modified.load(Ordering::SeqCst), } @@ -148,6 +239,23 @@ impl LocalStoreInner { async fn close(&self) -> Result<()> { Ok(()) } + + fn descriptor(&self) -> WalletDescriptor { + let filename = self + .storage() + .filename() + .and_then(|f| PathBuf::from(f).file_stem().and_then(|f| f.to_str().map(String::from))) + .unwrap_or_else(|| "resident".to_string()); + WalletDescriptor { title: self.cache.read().unwrap().wallet_title.clone(), filename } + } + + fn location(&self) -> Result { + let store = self.storage(); + match &*store { + Store::Resident => Ok(StorageDescriptor::Resident), + Store::Storage(storage) => Ok(StorageDescriptor::Internal(storage.filename_as_string())), + } + } } impl Drop for LocalStoreInner { @@ -170,7 +278,7 @@ impl Location { impl Default for Location { fn default() -> Self { - Self { folder: super::DEFAULT_STORAGE_FOLDER.to_string() } + Self { folder: super::default_storage_folder().to_string() } } } @@ -195,6 +303,23 @@ impl LocalStore { pub fn inner(&self) -> Result> { self.inner.lock().unwrap().as_ref().cloned().ok_or(Error::WalletNotOpen) } + + fn location(&self) -> Option> { + self.location.lock().unwrap().clone() + } + + #[allow(dead_code)] + async fn wallet_export_impl(&self, wallet_secret: &Secret, _options: WalletExportOptions) -> Result> { + self.inner()?.try_export(wallet_secret, _options).await + } + + async fn wallet_import_impl(&self, wallet_secret: &Secret, serialized_wallet_storage: &[u8]) -> Result { + let location = self.location().expect("initialized wallet storage location"); + let inner = LocalStoreInner::try_import(wallet_secret, &location.folder, serialized_wallet_storage).await?; + inner.store(wallet_secret).await?; + let wallet_descriptor = inner.descriptor(); + Ok(wallet_descriptor) + } } #[async_trait] @@ -215,28 +340,58 @@ impl Interface for LocalStore { Ok(self.inner()?.transactions.clone()) } - fn name(&self) -> Option { - self.inner.lock().unwrap().as_ref().map(|inner| inner.filename.clone()) + fn descriptor(&self) -> Option { + self.inner.lock().unwrap().as_ref().map(|inner| inner.descriptor()) + } + + fn encryption_kind(&self) -> Result { + Ok(self.inner()?.cache.read().unwrap().encryption_kind) + } + + async fn rename(&self, wallet_secret: &Secret, title: Option<&str>, filename: Option<&str>) -> Result<()> { + let inner = self.inner.lock().unwrap().clone().ok_or(Error::WalletNotOpen)?; + if let Some(title) = title { + inner.cache.write().unwrap().wallet_title = Some(title.to_string()); + self.commit(wallet_secret).await?; + } + + if let Some(filename) = filename { + inner.rename(filename)?; + } + Ok(()) + } + + /// change the secret of the currently open wallet + async fn change_secret(&self, old_wallet_secret: &Secret, new_wallet_secret: &Secret) -> Result<()> { + let inner = self.inner.lock().unwrap().clone().ok_or(Error::WalletNotOpen)?; + inner.change_secret(old_wallet_secret, new_wallet_secret).await?; + Ok(()) } async fn exists(&self, name: Option<&str>) -> Result { let location = self.location.lock().unwrap().clone().unwrap(); - let store = Storage::try_new_with_folder(&location.folder, &format!("{}.wallet", name.unwrap_or(super::DEFAULT_WALLET_FILE)))?; + let store = + Storage::try_new_with_folder(&location.folder, &format!("{}.wallet", name.unwrap_or(super::default_wallet_file())))?; store.exists().await } - async fn create(&self, ctx: &Arc, args: CreateArgs) -> Result<()> { - let location = self.location.lock().unwrap().clone().unwrap(); + async fn create(&self, wallet_secret: &Secret, args: CreateArgs) -> Result { + let location = self.location().expect("initialized wallet storage location"); - let inner = Arc::new(LocalStoreInner::try_create(ctx, &location.folder, args, self.is_resident).await?); + let inner = Arc::new(LocalStoreInner::try_create(wallet_secret, &location.folder, args, self.is_resident).await?); + let descriptor = inner.descriptor(); self.inner.lock().unwrap().replace(inner); - Ok(()) + Ok(descriptor) } - async fn open(&self, ctx: &Arc, args: OpenArgs) -> Result<()> { + async fn open(&self, wallet_secret: &Secret, args: OpenArgs) -> Result<()> { + if self.inner()?.is_modified() { + panic!("LocalStore::open called while modified flag is true!"); + } + let location = self.location.lock().unwrap().clone().unwrap(); - let inner = Arc::new(LocalStoreInner::try_load(ctx, &location.folder, args).await?); + let inner = Arc::new(LocalStoreInner::try_load(wallet_secret, &location.folder, args).await?); self.inner.lock().unwrap().replace(inner); Ok(()) } @@ -272,12 +427,8 @@ impl Interface for LocalStore { self.inner.lock().unwrap().is_some() } - fn descriptor(&self) -> Result> { - let inner = self.inner()?; - match inner.store { - Store::Resident => Ok(Some("Memory resident wallet".to_string())), - Store::Storage(ref storage) => Ok(Some(storage.filename_as_string())), - } + fn location(&self) -> Result { + self.inner()?.location() } async fn batch(&self) -> Result<()> { @@ -285,19 +436,19 @@ impl Interface for LocalStore { Ok(()) } - async fn flush(&self, ctx: &Arc) -> Result<()> { + async fn flush(&self, wallet_secret: &Secret) -> Result<()> { if !self.batch.load(Ordering::SeqCst) { panic!("flush() called while not in batch mode"); } self.batch.store(false, Ordering::SeqCst); - self.commit(ctx).await?; + self.commit(wallet_secret).await?; Ok(()) } - async fn commit(&self, ctx: &Arc) -> Result<()> { + async fn commit(&self, wallet_secret: &Secret) -> Result<()> { if !self.batch.load(Ordering::SeqCst) { - self.inner()?.store(ctx).await?; + self.inner()?.store(wallet_secret).await?; } Ok(()) } @@ -318,13 +469,21 @@ impl Interface for LocalStore { } async fn get_user_hint(&self) -> Result> { - Ok(self.inner()?.cache().user_hint.clone()) + Ok(self.inner()?.cache.read().unwrap().user_hint.clone()) } async fn set_user_hint(&self, user_hint: Option) -> Result<()> { - self.inner()?.cache().user_hint = user_hint; + self.inner()?.cache.write().unwrap().user_hint = user_hint; Ok(()) } + + async fn wallet_export(&self, wallet_secret: &Secret, options: WalletExportOptions) -> Result> { + self.wallet_export_impl(wallet_secret, options).await + } + + async fn wallet_import(&self, wallet_secret: &Secret, serialized_wallet_storage: &[u8]) -> Result { + self.wallet_import_impl(wallet_secret, serialized_wallet_storage).await + } } #[async_trait] @@ -334,31 +493,32 @@ impl PrvKeyDataStore for LocalStoreInner { } async fn load_key_info(&self, prv_key_data_id: &PrvKeyDataId) -> Result>> { - Ok(self.cache().prv_key_data_info.map.get(prv_key_data_id).cloned()) + Ok(self.cache.read().unwrap().prv_key_data_info.map.get(prv_key_data_id).cloned()) } - async fn load_key_data(&self, ctx: &Arc, prv_key_data_id: &PrvKeyDataId) -> Result> { - let wallet_secret = ctx.wallet_secret().await; //.ok_or(Error::WalletSecretRequired)?; - let prv_key_data_map: Decrypted = self.cache().prv_key_data.decrypt(&wallet_secret)?; + async fn load_key_data(&self, wallet_secret: &Secret, prv_key_data_id: &PrvKeyDataId) -> Result> { + let prv_key_data_map: Decrypted = self.cache.read().unwrap().prv_key_data.decrypt(wallet_secret)?; Ok(prv_key_data_map.get(prv_key_data_id).cloned()) } - async fn store(&self, ctx: &Arc, prv_key_data: PrvKeyData) -> Result<()> { - let wallet_secret = ctx.wallet_secret().await; - let mut prv_key_data_map: Decrypted = self.cache().prv_key_data.decrypt(&wallet_secret)?; + async fn store(&self, wallet_secret: &Secret, prv_key_data: PrvKeyData) -> Result<()> { + let mut cache = self.cache.write().unwrap(); + let encryption_kind = cache.encryption_kind; + let mut prv_key_data_map: Decrypted = cache.prv_key_data.decrypt(wallet_secret)?; let prv_key_data_info = Arc::new((&prv_key_data).into()); - self.cache().prv_key_data_info.insert(prv_key_data.id, prv_key_data_info)?; + cache.prv_key_data_info.insert(prv_key_data.id, prv_key_data_info)?; prv_key_data_map.insert(prv_key_data.id, prv_key_data); - self.cache().prv_key_data.replace(prv_key_data_map.encrypt(&wallet_secret)?); + cache.prv_key_data.replace(prv_key_data_map.encrypt(wallet_secret, encryption_kind)?); self.set_modified(true); Ok(()) } - async fn remove(&self, ctx: &Arc, prv_key_data_id: &PrvKeyDataId) -> Result<()> { - let wallet_secret = ctx.wallet_secret().await; //.ok_or(Error::WalletSecretRequired)?; - let mut prv_key_data_map: Decrypted = self.cache().prv_key_data.decrypt(&wallet_secret)?; + async fn remove(&self, wallet_secret: &Secret, prv_key_data_id: &PrvKeyDataId) -> Result<()> { + let mut cache = self.cache.write().unwrap(); + let encryption_kind = cache.encryption_kind; + let mut prv_key_data_map: Decrypted = cache.prv_key_data.decrypt(wallet_secret)?; prv_key_data_map.remove(prv_key_data_id); - self.cache().prv_key_data.replace(prv_key_data_map.encrypt(&wallet_secret)?); + cache.prv_key_data.replace(prv_key_data_map.encrypt(wallet_secret, encryption_kind)?); self.set_modified(true); Ok(()) } @@ -369,21 +529,23 @@ impl AccountStore for LocalStoreInner { async fn iter( &self, prv_key_data_id_filter: Option, - ) -> Result, Option>)>> { + ) -> Result, Option>)>> { Ok(Box::pin(AccountStream::new(self.cache.clone(), prv_key_data_id_filter))) } async fn len(&self, prv_key_data_id_filter: Option) -> Result { let len = match prv_key_data_id_filter { - Some(filter) => self.cache().accounts.vec.iter().filter(|account| account.prv_key_data_id == Some(filter)).count(), - None => self.cache().accounts.vec.len(), + Some(filter) => { + self.cache.read().unwrap().accounts.vec.iter().filter(|account| account.prv_key_data_ids.contains(&filter)).count() + } + None => self.cache.read().unwrap().accounts.vec.len(), }; Ok(len) } - async fn load_single(&self, ids: &AccountId) -> Result, Option>)>> { - let cache = self.cache(); + async fn load_single(&self, ids: &AccountId) -> Result, Option>)>> { + let cache = self.cache.read().unwrap(); if let Some(account) = cache.accounts.load_single(ids)? { Ok(Some((account, cache.metadata.load_single(ids)?))) } else { @@ -391,12 +553,19 @@ impl AccountStore for LocalStoreInner { } } - // async fn load_multiple(&self, ids: &[AccountId]) -> Result>> { - // self.cache().accounts.load_multiple(ids) - // } + async fn load_multiple(&self, ids: &[AccountId]) -> Result, Option>)>> { + let cache = self.cache.read().unwrap(); + let accounts = cache.accounts.load_multiple(ids)?; + accounts + .into_iter() + .map(|account| { + cache.metadata.load_single(account.id()).map(|metadata| (account.clone(), metadata)).or_else(|_| Ok((account, None))) + }) + .collect::>>() + } - async fn store_single(&self, account: &Account, metadata: Option<&Metadata>) -> Result<()> { - let mut cache = self.cache(); + async fn store_single(&self, account: &AccountStorage, metadata: Option<&AccountMetadata>) -> Result<()> { + let mut cache = self.cache.write().unwrap(); cache.accounts.store_single(account)?; if let Some(metadata) = metadata { cache.metadata.store_single(metadata)?; @@ -405,18 +574,17 @@ impl AccountStore for LocalStoreInner { Ok(()) } - async fn store_multiple(&self, data: &[(&Account, Option<&Metadata>)]) -> Result<()> { - let mut cache = self.cache(); - let accounts = data.iter().map(|(account, _)| *account).collect::>(); - let metadata = data.iter().filter_map(|(_, metadata)| *metadata).collect::>(); - cache.accounts.store_multiple(&accounts)?; - cache.metadata.store_multiple(&metadata)?; + async fn store_multiple(&self, data: Vec<(AccountStorage, Option)>) -> Result<()> { + let mut cache = self.cache.write().unwrap(); + let (accounts, metadata): (Vec<_>, Vec<_>) = data.into_iter().unzip(); + cache.accounts.store_multiple(accounts)?; + cache.metadata.store_multiple(metadata.into_iter().flatten().collect())?; self.set_modified(true); Ok(()) } async fn remove(&self, ids: &[&AccountId]) -> Result<()> { - let mut cache = self.cache(); + let mut cache = self.cache.write().unwrap(); cache.accounts.remove(ids)?; cache.metadata.remove(ids)?; @@ -425,8 +593,8 @@ impl AccountStore for LocalStoreInner { Ok(()) } - async fn update_metadata(&self, metadata: &[&Metadata]) -> Result<()> { - self.cache().metadata.store_multiple(metadata)?; + async fn update_metadata(&self, metadata: Vec) -> Result<()> { + self.cache.write().unwrap().metadata.store_multiple(metadata)?; self.update_stored_metadata().await?; Ok(()) } @@ -440,7 +608,9 @@ impl AddressBookStore for LocalStoreInner { async fn search(&self, search: &str) -> Result>> { let matches = self - .cache() + .cache + .read() + .unwrap() .address_book .iter() .filter_map(|entry| if entry.alias.contains(search) { Some(Arc::new(entry.clone())) } else { None }) diff --git a/wallet/core/src/storage/local/mod.rs b/wallet/core/src/storage/local/mod.rs index 9e52b5b8f..c587ca5ae 100644 --- a/wallet/core/src/storage/local/mod.rs +++ b/wallet/core/src/storage/local/mod.rs @@ -1,3 +1,15 @@ +//! Local storage implementation for the wallet SDK. +//! This module provides a local storage implementation +//! that functions uniformly in native and JS environments. +//! In native and NodeJS environments, this subsystem +//! will use the native file system IO. In the browser +//! environment, if called from the web page context +//! this will use `localStorage` and if invoked in the +//! chromium extension context it will use the +//! `chrome.storage.local` API. The implementation +//! is backed by the [`workflow_store`](https://docs.rs/workflow-store/) +//! crate. + pub mod cache; pub mod collection; pub mod interface; @@ -10,8 +22,125 @@ pub mod wallet; pub use collection::Collection; pub use payload::Payload; pub use storage::Storage; -pub use wallet::Wallet; +pub use wallet::WalletStorage; + +use crate::error::Error; +use crate::result::Result; +use wasm_bindgen::prelude::*; +use workflow_store::fs::create_dir_all_sync; + +static mut DEFAULT_STORAGE_FOLDER: Option = None; +static mut DEFAULT_WALLET_FILE: Option = None; +static mut DEFAULT_SETTINGS_FILE: Option = None; + +pub fn default_storage_folder() -> &'static str { + // SAFETY: This operation is initializing a static mut variable, + // however, the actual variable is accessible only through + // this function. + unsafe { DEFAULT_STORAGE_FOLDER.get_or_insert("~/.kash".to_string()).as_str() } +} + +pub fn default_wallet_file() -> &'static str { + // SAFETY: This operation is initializing a static mut variable, + // however, the actual variable is accessible only through + // this function. + unsafe { DEFAULT_WALLET_FILE.get_or_insert("kash".to_string()).as_str() } +} + +pub fn default_settings_file() -> &'static str { + // SAFETY: This operation is initializing a static mut variable, + // however, the actual variable is accessible only through + // this function. + unsafe { DEFAULT_SETTINGS_FILE.get_or_insert("kash".to_string()).as_str() } +} + +/// Set a custom storage folder for the wallet SDK +/// subsystem. Encrypted wallet files and transaction +/// data will be stored in this folder. If not set +/// the storage folder will default to `~/.kash` +/// (note that the folder is hidden). +/// +/// This must be called before using any other wallet +/// SDK functions. +/// +/// NOTE: This function will create a folder if it +/// doesn't exist. This function will have no effect +/// if invoked in the browser environment. +/// +/// # Safety +/// +/// This function is unsafe because it is setting a static +/// mut variable, meaning this function is not thread-safe. +/// However the function must be used before any other +/// wallet operations are performed. You must not change +/// the default storage folder once the wallet has been +/// initialized. +/// +pub unsafe fn set_default_storage_folder(folder: String) -> Result<()> { + create_dir_all_sync(&folder).map_err(|err| Error::custom(format!("Failed to create storage folder: {err}")))?; + DEFAULT_STORAGE_FOLDER = Some(folder); + Ok(()) +} + +/// Set a custom storage folder for the wallet SDK +/// subsystem. Encrypted wallet files and transaction +/// data will be stored in this folder. If not set +/// the storage folder will default to `~/.kash` +/// (note that the folder is hidden). +/// +/// This must be called before using any other wallet +/// SDK functions. +/// +/// NOTE: This function will create a folder if it +/// doesn't exist. This function will have no effect +/// if invoked in the browser environment. +/// +/// @param {String} folder - the path to the storage folder +/// +#[wasm_bindgen(js_name = setDefaultStorageFolder, skip_jsdoc)] +pub fn js_set_default_storage_folder(folder: String) -> Result<()> { + // SAFETY: This is unsafe because we are setting a static mut variable + // meaning this function is not thread-safe. However the function + // must be used before any other wallet operations are performed. + unsafe { set_default_storage_folder(folder) } +} + +/// Set the name of the default wallet file name +/// or the `localStorage` key. If `Wallet::open` +/// is called without a wallet file name, this name +/// will be used. Please note that this name +/// will be suffixed with `.wallet` suffix. +/// +/// This function should be called before using any +/// other wallet SDK functions. +/// +/// # Safety +/// +/// This function is unsafe because it is setting a static +/// mut variable, meaning this function is not thread-safe. +/// +pub unsafe fn set_default_wallet_file(folder: String) -> Result<()> { + DEFAULT_WALLET_FILE = Some(folder); + Ok(()) +} -pub const DEFAULT_STORAGE_FOLDER: &str = "~/.kash/"; -pub const DEFAULT_WALLET_FILE: &str = "kash"; -pub const DEFAULT_SETTINGS_FILE: &str = "kash"; +/// Set the name of the default wallet file name +/// or the `localStorage` key. If `Wallet::open` +/// is called without a wallet file name, this name +/// will be used. Please note that this name +/// will be suffixed with `.wallet` suffix. +/// +/// This function should be called before using any +/// other wallet SDK functions. +/// +/// @param {String} - the name to the wallet file or key. +/// +#[wasm_bindgen(js_name = setDefaultWalletFile, skip_jsdoc)] +pub fn js_set_default_wallet_file(folder: String) -> Result<()> { + // SAFETY: This is unsafe because we are setting a static mut variable + // meaning this function is not thread-safe. + unsafe { + DEFAULT_WALLET_FILE = Some(folder); + } + Ok(()) +} diff --git a/wallet/core/src/storage/local/payload.rs b/wallet/core/src/storage/local/payload.rs index f2de3c22c..a7352c513 100644 --- a/wallet/core/src/storage/local/payload.rs +++ b/wallet/core/src/storage/local/payload.rs @@ -1,25 +1,28 @@ +//! +//! Encrypted wallet payload storage. +//! + use crate::imports::*; use crate::result::Result; use crate::secret::Secret; -use crate::storage::{Account, AddressBookEntry, PrvKeyData, PrvKeyDataId}; +use crate::storage::{AddressBookEntry, PrvKeyData, PrvKeyDataId}; use kash_bip32::Mnemonic; use zeroize::{Zeroize, ZeroizeOnDrop}; -pub const PAYLOAD_VERSION: [u16; 3] = [1, 0, 0]; - #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Payload { - #[serde(default)] - pub version: [u16; 3], - pub prv_key_data: Vec, - pub accounts: Vec, + pub accounts: Vec, pub address_book: Vec, + pub encrypt_transactions: Option, } impl Payload { - pub fn new(prv_key_data: Vec, accounts: Vec, address_book: Vec) -> Self { - Self { version: PAYLOAD_VERSION, prv_key_data, accounts, address_book } + const STORAGE_MAGIC: u32 = 0x41544144; + const STORAGE_VERSION: u32 = 0; + + pub fn new(prv_key_data: Vec, accounts: Vec, address_book: Vec) -> Self { + Self { prv_key_data, accounts, address_book, encrypt_transactions: None } } } @@ -32,19 +35,63 @@ impl Zeroize for Payload { } impl Payload { - pub fn add_prv_key_data(&mut self, mnemonic: Mnemonic, payment_secret: Option<&Secret>) -> Result { - let prv_key_data = PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret)?; + pub fn add_prv_key_data( + &mut self, + mnemonic: Mnemonic, + payment_secret: Option<&Secret>, + encryption_kind: EncryptionKind, + ) -> Result { + let prv_key_data = PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret, encryption_kind)?; if !self.prv_key_data.iter().any(|existing_key_data| prv_key_data.id == existing_key_data.id) { self.prv_key_data.push(prv_key_data.clone()); + Ok(prv_key_data) } else { - panic!("private key data id already exists in the wallet"); + Err(Error::custom("private key data id already exists in the wallet")) } - - Ok(prv_key_data) } pub fn find_prv_key_data(&self, id: &PrvKeyDataId) -> Option<&PrvKeyData> { self.prv_key_data.iter().find(|prv_key_data| prv_key_data.id == *id) } } + +impl BorshSerialize for Payload { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.prv_key_data, writer)?; + BorshSerialize::serialize(&self.accounts, writer)?; + BorshSerialize::serialize(&self.address_book, writer)?; + BorshSerialize::serialize(&self.encrypt_transactions, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for Payload { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + let prv_key_data = BorshDeserialize::deserialize(buf)?; + let accounts = BorshDeserialize::deserialize(buf)?; + let address_book = BorshDeserialize::deserialize(buf)?; + let encrypt_transactions = BorshDeserialize::deserialize(buf)?; + + Ok(Self { prv_key_data, accounts, address_book, encrypt_transactions }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_wallet_payload() -> Result<()> { + let storable_in = Payload::new(vec![], vec![], vec![]); + let guard = StorageGuard::new(&storable_in); + let _storable_out = guard.validate()?; + + Ok(()) + } +} diff --git a/wallet/core/src/storage/local/storage.rs b/wallet/core/src/storage/local/storage.rs index 0139238a1..2216a9fa2 100644 --- a/wallet/core/src/storage/local/storage.rs +++ b/wallet/core/src/storage/local/storage.rs @@ -1,3 +1,7 @@ +//! +//! Location (file path) representation & multi-platform helper utilities. +//! + use crate::imports::*; use crate::result::Result; use std::path::{Path, PathBuf}; @@ -21,18 +25,18 @@ impl Storage { impl Storage { pub fn default_wallet_store() -> Self { - Self::try_new(&format!("{}.wallet", super::DEFAULT_WALLET_FILE)).unwrap() + Self::try_new(&format!("{}.wallet", super::default_wallet_file())).unwrap() } pub fn default_settings_store() -> Self { - Self::try_new(&format!("{}.settings", super::DEFAULT_SETTINGS_FILE)).unwrap() + Self::try_new(&format!("{}.settings", super::default_wallet_file())).unwrap() } pub fn try_new(name: &str) -> Result { let filename = if runtime::is_web() { PathBuf::from(name) } else { - let filename = Path::new(super::DEFAULT_STORAGE_FOLDER).join(name); + let filename = Path::new(super::default_storage_folder()).join(name); fs::resolve_path(filename.to_str().unwrap())? }; @@ -50,6 +54,13 @@ impl Storage { Ok(Storage { filename }) } + pub fn rename_sync(&mut self, filename: &str) -> Result<()> { + let target_filename = Path::new(filename).to_path_buf(); + workflow_store::fs::rename_sync(self.filename(), &target_filename)?; + self.filename = target_filename; + Ok(()) + } + pub fn filename(&self) -> &PathBuf { &self.filename } @@ -68,11 +79,12 @@ impl Storage { } pub async fn ensure_dir(&self) -> Result<()> { - let file = self.filename(); - if file.exists() { + if self.exists().await? { return Ok(()); } + let file = self.filename(); + if let Some(dir) = file.parent() { fs::create_dir_all(dir).await?; } @@ -80,11 +92,11 @@ impl Storage { } pub fn ensure_dir_sync(&self) -> Result<()> { - let file = self.filename(); - if file.exists() { + if self.exists_sync()? { return Ok(()); } + let file = self.filename(); if let Some(dir) = file.parent() { fs::create_dir_all_sync(dir)?; } diff --git a/wallet/core/src/storage/local/streams.rs b/wallet/core/src/storage/local/streams.rs index a7b684533..2f3dc33e1 100644 --- a/wallet/core/src/storage/local/streams.rs +++ b/wallet/core/src/storage/local/streams.rs @@ -1,16 +1,19 @@ +//! +//! Async streams for async iteration of wallet primitives. +//! + use crate::imports::*; use crate::result::Result; use crate::storage::local::cache::Cache; -use crate::storage::*; #[derive(Clone)] struct StoreStreamInner { - cache: Arc>, + cache: Arc>, cursor: usize, } impl StoreStreamInner { - fn new(cache: Arc>) -> Self { + fn new(cache: Arc>) -> Self { Self { cache, cursor: 0 } } } @@ -26,7 +29,7 @@ pub struct PrvKeyDataInfoStream { } impl PrvKeyDataInfoStream { - pub(crate) fn new(cache: Arc>) -> Self { + pub(crate) fn new(cache: Arc>) -> Self { Self { inner: StoreStreamInner::new(cache) } } } @@ -36,7 +39,7 @@ impl Stream for PrvKeyDataInfoStream { fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { let cache = self.inner.cache.clone(); - let cache = cache.lock().unwrap(); + let cache = cache.read().unwrap(); let vec = &cache.prv_key_data_info.vec; if self.inner.cursor < vec.len() { let prv_key_data_info = vec[self.inner.cursor].clone(); @@ -54,17 +57,17 @@ pub struct AccountStream { } impl AccountStream { - pub(crate) fn new(cache: Arc>, filter: Option) -> Self { + pub(crate) fn new(cache: Arc>, filter: Option) -> Self { Self { inner: StoreStreamInner::new(cache), filter } } } impl Stream for AccountStream { - type Item = Result<(Arc, Option>)>; + type Item = Result<(Arc, Option>)>; fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { let cache = self.inner.cache.clone(); - let cache = cache.lock().unwrap(); + let cache = cache.read().unwrap(); let accounts = &cache.accounts.vec; let metadata = &cache.metadata.map; @@ -73,18 +76,11 @@ impl Stream for AccountStream { let account = accounts[self.inner.cursor].clone(); self.inner.cursor += 1; - match &account.data { - AccountData::MultiSig(MultiSig { prv_key_data_ids: Some(prv_key_data_ids), .. }) - if prv_key_data_ids.binary_search(&filter).is_ok() => - { - let meta = metadata.get(&account.id).cloned(); - return Poll::Ready(Some(Ok((account, meta)))); - } - _ if account.prv_key_data_id == Some(filter) => { - let meta = metadata.get(&account.id).cloned(); - return Poll::Ready(Some(Ok((account, meta)))); - } - _ => continue, + if account.prv_key_data_ids.contains(&filter) { + let meta = metadata.get(&account.id).cloned(); + return Poll::Ready(Some(Ok((account, meta)))); + } else { + continue; } } Poll::Ready(None) @@ -105,7 +101,7 @@ pub struct AddressBookEntryStream { } impl AddressBookEntryStream { - pub(crate) fn new(cache: Arc>) -> Self { + pub(crate) fn new(cache: Arc>) -> Self { Self { inner: StoreStreamInner::new(cache) } } } @@ -115,7 +111,7 @@ impl Stream for AddressBookEntryStream { fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { let cache = self.inner.cache.clone(); - let cache = cache.lock().unwrap(); + let cache = cache.read().unwrap(); let vec = &cache.address_book; //transaction_records.vec; if self.inner.cursor < vec.len() { diff --git a/wallet/core/src/storage/local/transaction/fsio.rs b/wallet/core/src/storage/local/transaction/fsio.rs index 1470e6fd5..685f2eb72 100644 --- a/wallet/core/src/storage/local/transaction/fsio.rs +++ b/wallet/core/src/storage/local/transaction/fsio.rs @@ -1,8 +1,14 @@ +//! +//! Local file system transaction storage (native+NodeJS fs IO). +//! + +use crate::encryption::*; use crate::imports::*; use crate::result::Result; -use crate::storage::interface::StorageStream; +use crate::secret::Secret; +use crate::storage::interface::{StorageStream, TransactionRangeResult}; +use crate::storage::TransactionRecord; use crate::storage::{Binding, TransactionRecordStore}; -use crate::storage::{TransactionMetadata, TransactionRecord}; use kash_utils::hex::ToHex; use std::{ collections::VecDeque, @@ -60,7 +66,8 @@ impl TransactionStore { let mut transactions = VecDeque::new(); match fs::readdir(folder, true).await { Ok(mut files) => { - files.sort_by_key(|f| f.metadata().unwrap().created()); + // we reverse the order of the files so that the newest files are first + files.sort_by_key(|f| std::cmp::Reverse(f.metadata().unwrap().created())); for file in files { if let Ok(id) = TransactionId::from_hex(file.file_name()) { @@ -82,10 +89,6 @@ impl TransactionStore { } } } - - pub async fn store_transaction_metadata(&self, _id: TransactionId, _metadata: TransactionMetadata) -> Result<()> { - Ok(()) - } } #[async_trait] @@ -94,14 +97,14 @@ impl TransactionRecordStore for TransactionStore { Ok(Box::pin(TransactionIdStream::try_new(self, binding, network_id).await?)) } - // async fn transaction_iter(&self, binding: &Binding, network_id: &NetworkId) -> Result> { - // Ok(Box::pin(TransactionRecordStream::try_new(&self.transactions, binding, network_id).await?)) - // } + async fn transaction_data_iter(&self, binding: &Binding, network_id: &NetworkId) -> Result>> { + Ok(Box::pin(TransactionRecordStream::try_new(self, binding, network_id).await?)) + } async fn load_single(&self, binding: &Binding, network_id: &NetworkId, id: &TransactionId) -> Result> { let folder = self.make_folder(binding, network_id); let path = folder.join(id.to_hex()); - Ok(Arc::new(fs::read_json::(&path).await?)) + Ok(Arc::new(read(&path, None).await?)) } async fn load_multiple( @@ -115,18 +118,79 @@ impl TransactionRecordStore for TransactionStore { for id in ids { let path = folder.join(&id.to_hex()); - let tx: TransactionRecord = fs::read_json(&path).await?; - transactions.push(Arc::new(tx)); + match read(&path, None).await { + Ok(tx) => { + transactions.push(Arc::new(tx)); + } + Err(err) => { + log_error!("Error loading transaction {id}: {:?}", err); + } + } } Ok(transactions) } + async fn load_range( + &self, + binding: &Binding, + network_id: &NetworkId, + filter: Option>, + range: std::ops::Range, + ) -> Result { + let folder = self.ensure_folder(binding, network_id).await?; + let ids = self.enumerate(binding, network_id).await?; + let mut transactions = vec![]; + + let total = if let Some(filter) = filter { + let mut located = 0; + + for id in ids { + let path = folder.join(&id.to_hex()); + + match read(&path, None).await { + Ok(tx) => { + if filter.contains(&tx.kind()) { + if located >= range.start && located < range.end { + transactions.push(Arc::new(tx)); + } + + located += 1; + } + } + Err(err) => { + log_error!("Error loading transaction {id}: {:?}", err); + } + } + } + + located + } else { + let iter = ids.iter().skip(range.start).take(range.len()); + + for id in iter { + let path = folder.join(&id.to_hex()); + match read(&path, None).await { + Ok(tx) => { + transactions.push(Arc::new(tx)); + } + Err(err) => { + log_error!("Error loading transaction {id}: {:?}", err); + } + } + } + + ids.len() + }; + + Ok(TransactionRangeResult { transactions, total: total as u64 }) + } + async fn store(&self, transaction_records: &[&TransactionRecord]) -> Result<()> { for tx in transaction_records { let folder = self.ensure_folder(tx.binding(), tx.network_id()).await?; let filename = folder.join(tx.id().to_hex()); - fs::write_json(&filename, tx).await?; + write(&filename, tx, None, EncryptionKind::XChaCha20Poly1305).await?; } Ok(()) @@ -142,7 +206,32 @@ impl TransactionRecordStore for TransactionStore { Ok(()) } - async fn store_transaction_metadata(&self, _id: TransactionId, _metadata: TransactionMetadata) -> Result<()> { + async fn store_transaction_note( + &self, + binding: &Binding, + network_id: &NetworkId, + id: TransactionId, + note: Option, + ) -> Result<()> { + let folder = self.make_folder(binding, network_id); + let path = folder.join(id.to_hex()); + let mut transaction = read(&path, None).await?; + transaction.note = note; + write(&path, &transaction, None, EncryptionKind::XChaCha20Poly1305).await?; + Ok(()) + } + async fn store_transaction_metadata( + &self, + binding: &Binding, + network_id: &NetworkId, + id: TransactionId, + metadata: Option, + ) -> Result<()> { + let folder = self.make_folder(binding, network_id); + let path = folder.join(id.to_hex()); + let mut transaction = read(&path, None).await?; + transaction.metadata = metadata; + write(&path, &transaction, None, EncryptionKind::XChaCha20Poly1305).await?; Ok(()) } } @@ -175,19 +264,17 @@ impl Stream for TransactionIdStream { } } -/* #[derive(Clone)] pub struct TransactionRecordStream { - store: Arc, - folder: PathBuf, transactions: VecDeque, + folder: PathBuf, } impl TransactionRecordStream { - pub(crate) async fn try_new(store: &Arc, binding: &Binding, network_id: &NetworkId) -> Result { - let folder = store.make_folder(binding, network_id)?; + pub(crate) async fn try_new(store: &TransactionStore, binding: &Binding, network_id: &NetworkId) -> Result { + let folder = store.make_folder(binding, network_id); let transactions = store.enumerate(binding, network_id).await?; - Ok(Self { store: store.clone(), folder, transactions }) + Ok(Self { transactions, folder }) } } @@ -199,13 +286,37 @@ impl Stream for TransactionRecordStream { Poll::Ready(None) } else { let id = self.transactions.pop_front().unwrap(); - let filename = id.to_hex(); - let path = self.folder.join(filename); - match fs::read_json::(&path).await { - Ok(tx) => Poll::Ready(Some(Ok(Arc::new(tx)))), - Err(e) => Poll::Ready(Some(Err(e))), + let path = self.folder.join(id.to_hex()); + match read_sync(&path, None) { + Ok(transaction_data) => Poll::Ready(Some(Ok(Arc::new(transaction_data)))), + Err(err) => Poll::Ready(Some(Err(err))), } } } + + fn size_hint(&self) -> (usize, Option) { + (self.transactions.len(), Some(self.transactions.len())) + } +} + +async fn read(path: &Path, secret: Option<&Secret>) -> Result { + let bytes = fs::read(path).await?; + let encryptable = Encryptable::::try_from_slice(bytes.as_slice())?; + Ok(encryptable.decrypt(secret)?.unwrap()) +} + +fn read_sync(path: &Path, secret: Option<&Secret>) -> Result { + let bytes = fs::read_sync(path)?; + let encryptable = Encryptable::::try_from_slice(bytes.as_slice())?; + Ok(encryptable.decrypt(secret)?.unwrap()) +} + +async fn write(path: &Path, record: &TransactionRecord, secret: Option<&Secret>, encryption_kind: EncryptionKind) -> Result<()> { + let data = if let Some(secret) = secret { + Encryptable::from(record.clone()).into_encrypted(secret, encryption_kind)? + } else { + Encryptable::from(record.clone()) + }; + fs::write_json(path, &data).await?; + Ok(()) } -*/ diff --git a/wallet/core/src/storage/local/transaction/indexdb.rs b/wallet/core/src/storage/local/transaction/indexdb.rs index 04dacad74..20ca49ac9 100644 --- a/wallet/core/src/storage/local/transaction/indexdb.rs +++ b/wallet/core/src/storage/local/transaction/indexdb.rs @@ -1,8 +1,12 @@ +//! +//! Web browser IndexedDB implementation of the transaction storage. +//! + use crate::imports::*; use crate::result::Result; -use crate::storage::interface::StorageStream; -use crate::storage::{Binding, TransactionRecordStore}; -use crate::storage::{TransactionMetadata, TransactionRecord}; +use crate::storage::interface::{StorageStream, TransactionRangeResult}; +use crate::storage::TransactionRecord; +use crate::storage::{Binding, TransactionKind, TransactionRecordStore}; pub struct Inner { known_databases: HashMap>, @@ -59,10 +63,6 @@ impl TransactionStore { } Ok(()) } - - pub async fn store_transaction_metadata(&self, _id: TransactionId, _metadata: TransactionMetadata) -> Result<()> { - Ok(()) - } } #[async_trait] @@ -71,9 +71,9 @@ impl TransactionRecordStore for TransactionStore { Ok(Box::pin(TransactionIdStream::try_new(self, binding, network_id).await?)) } - // async fn transaction_iter(&self, binding: &Binding, network_id: &NetworkId) -> Result> { - // Ok(Box::pin(TransactionRecordStream::try_new(&self.transactions, binding, network_id).await?)) - // } + async fn transaction_data_iter(&self, binding: &Binding, network_id: &NetworkId) -> Result>> { + Ok(Box::pin(TransactionRecordStream::try_new(self, binding, network_id).await?)) + } async fn load_single(&self, _binding: &Binding, _network_id: &NetworkId, _id: &TransactionId) -> Result> { Err(Error::NotImplemented) @@ -88,6 +88,17 @@ impl TransactionRecordStore for TransactionStore { Ok(vec![]) } + async fn load_range( + &self, + _binding: &Binding, + _network_id: &NetworkId, + _filter: Option>, + _range: std::ops::Range, + ) -> Result { + let result = TransactionRangeResult { transactions: vec![], total: 0 }; + Ok(result) + } + async fn store(&self, _transaction_records: &[&TransactionRecord]) -> Result<()> { Ok(()) } @@ -96,7 +107,22 @@ impl TransactionRecordStore for TransactionStore { Ok(()) } - async fn store_transaction_metadata(&self, _id: TransactionId, _metadata: TransactionMetadata) -> Result<()> { + async fn store_transaction_note( + &self, + _binding: &Binding, + _network_id: &NetworkId, + _id: TransactionId, + _note: Option, + ) -> Result<()> { + Ok(()) + } + async fn store_transaction_metadata( + &self, + _binding: &Binding, + _network_id: &NetworkId, + _id: TransactionId, + _metadata: Option, + ) -> Result<()> { Ok(()) } } @@ -118,3 +144,21 @@ impl Stream for TransactionIdStream { Poll::Ready(None) } } + +#[derive(Clone)] +pub struct TransactionRecordStream {} + +impl TransactionRecordStream { + pub(crate) async fn try_new(_store: &TransactionStore, _binding: &Binding, _network_id: &NetworkId) -> Result { + Ok(Self {}) + } +} + +impl Stream for TransactionRecordStream { + type Item = Result>; + + #[allow(unused_mut)] + fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(None) + } +} diff --git a/wallet/core/src/storage/local/transaction/mod.rs b/wallet/core/src/storage/local/transaction/mod.rs index 43477e236..1389042c2 100644 --- a/wallet/core/src/storage/local/transaction/mod.rs +++ b/wallet/core/src/storage/local/transaction/mod.rs @@ -1,2 +1,6 @@ +//! +//! Transaction storage subsystem implementations. +//! + pub mod fsio; pub mod indexdb; diff --git a/wallet/core/src/storage/local/wallet.rs b/wallet/core/src/storage/local/wallet.rs index 8a0c38e09..e23289d8b 100644 --- a/wallet/core/src/storage/local/wallet.rs +++ b/wallet/core/src/storage/local/wallet.rs @@ -1,63 +1,54 @@ +//! +//! Wallet data storage wrapper. +//! + use crate::imports::*; use crate::result::Result; use crate::secret::Secret; use crate::storage::local::Payload; use crate::storage::local::Storage; -use crate::storage::{Decrypted, Encrypted, Hint, Metadata, PrvKeyData, PrvKeyDataId}; -use serde_json::{from_str, from_value, Value}; +use crate::storage::Encryptable; +use crate::storage::TransactionRecord; +use crate::storage::{AccountMetadata, Decrypted, Encrypted, Hint, PrvKeyData, PrvKeyDataId}; use workflow_store::fs; -pub const WALLET_VERSION: [u16; 3] = [1, 0, 0]; - #[derive(Clone, Serialize, Deserialize)] -pub struct Wallet { - #[serde(default)] - pub version: [u16; 3], - +pub struct WalletStorage { #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(skip_serializing_if = "Option::is_none")] pub user_hint: Option, + pub encryption_kind: EncryptionKind, pub payload: Encrypted, - pub metadata: Vec, + pub metadata: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub transactions: Option>>>, } -impl Wallet { +impl WalletStorage { + pub const STORAGE_MAGIC: u32 = 0x5753414b; + pub const STORAGE_VERSION: u32 = 0; + pub fn try_new( title: Option, user_hint: Option, secret: &Secret, + encryption_kind: EncryptionKind, payload: Payload, - metadata: Vec, + metadata: Vec, ) -> Result { - let payload = Decrypted::new(payload).encrypt(secret)?; - Ok(Self { version: WALLET_VERSION, title, payload, metadata, user_hint }) + let payload = Decrypted::new(payload).encrypt(secret, encryption_kind)?; + Ok(Self { title, encryption_kind, payload, metadata, user_hint, transactions: None }) } pub fn payload(&self, secret: &Secret) -> Result> { self.payload.decrypt::(secret) } - pub async fn try_load(store: &Storage) -> Result { + pub async fn try_load(store: &Storage) -> Result { if fs::exists(store.filename()).await? { - let text = fs::read_to_string(store.filename()).await?; - let root = from_str::(&text)?; - - let version = root.get("version"); - let version: [u16; 3] = if let Some(version) = version { - from_value(version.clone()).map_err(|err| Error::Custom(format!("unknown wallet version `{version:?}`: {err}")))? - } else { - [0, 0, 0] - }; - - match version { - [0,0,0] => { - Err(Error::Custom("wallet version 0.0.0 used during the development is no longer supported, please recreate the wallet using your saved mnemonic".to_string())) - }, - _ => { - Ok(from_value::(root)?) - } - } + let bytes = fs::read(store.filename()).await?; + Ok(BorshDeserialize::try_from_slice(bytes.as_slice())?) } else { let name = store.filename().file_name().unwrap().to_str().unwrap(); Err(Error::NoWalletInStorage(name.to_string())) @@ -66,7 +57,18 @@ impl Wallet { pub async fn try_store(&self, store: &Storage) -> Result<()> { store.ensure_dir().await?; - fs::write_json(store.filename(), self).await?; + + cfg_if! { + if #[cfg(target_arch = "wasm32")] { + let serialized = BorshSerialize::try_to_vec(self)?; + fs::write(store.filename(), serialized.as_slice()).await?; + } else { + // make this platform-specific to avoid creating + // a buffer containing serialization + let mut file = std::fs::File::create(store.filename(), )?; + BorshSerialize::serialize(self, &mut file)?; + } + } Ok(()) } @@ -78,7 +80,72 @@ impl Wallet { Ok(keydata) } - pub fn replace_metadata(&mut self, metadata: Vec) { + pub fn replace_metadata(&mut self, metadata: Vec) { self.metadata = metadata; } } + +impl BorshSerialize for WalletStorage { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.title, writer)?; + BorshSerialize::serialize(&self.user_hint, writer)?; + BorshSerialize::serialize(&self.encryption_kind, writer)?; + BorshSerialize::serialize(&self.payload, writer)?; + BorshSerialize::serialize(&self.metadata, writer)?; + BorshSerialize::serialize(&self.transactions, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for WalletStorage { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { magic, version, .. } = StorageHeader::deserialize(buf)?; + + if magic != Self::STORAGE_MAGIC { + return Err(IoError::new( + IoErrorKind::InvalidData, + format!("This does not seem to be a kash wallet data file. Unknown file signature '0x{:x}'.", magic), + )); + } + + if version > Self::STORAGE_VERSION { + return Err(IoError::new( + IoErrorKind::InvalidData, + format!("This wallet data was generated using a new version of the software. Please upgrade your software environment. Expected at most version '{}', encountered version '{}'", Self::STORAGE_VERSION, version), + )); + } + + let title = BorshDeserialize::deserialize(buf)?; + let user_hint = BorshDeserialize::deserialize(buf)?; + let encryption_kind = BorshDeserialize::deserialize(buf)?; + let payload = BorshDeserialize::deserialize(buf)?; + let metadata = BorshDeserialize::deserialize(buf)?; + let transactions = BorshDeserialize::deserialize(buf)?; + + Ok(Self { title, user_hint, encryption_kind, payload, metadata, transactions }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_storage_wallet_storage() -> Result<()> { + let storable_in = WalletStorage::try_new( + Some("title".to_string()), + Some(Hint::new("hint".to_string())), + &Secret::from("secret"), + EncryptionKind::XChaCha20Poly1305, + Payload::new(vec![], vec![], vec![]), + vec![], + )?; + let guard = StorageGuard::new(&storable_in); + let _storable_out = guard.validate()?; + + Ok(()) + } +} diff --git a/wallet/core/src/storage/metadata.rs b/wallet/core/src/storage/metadata.rs index 284eb41f4..0eacb902d 100644 --- a/wallet/core/src/storage/metadata.rs +++ b/wallet/core/src/storage/metadata.rs @@ -1,16 +1,25 @@ +//! AccountMetadata is an associative structure that contains +//! additional information about an account. This structure +//! is not encrypted and is stored in plain text. This is meant +//! to provide an ability to perform various operations (such as +//! new address generation) without the need to re-encrypt the +//! wallet data when storing. + use crate::derivation::AddressDerivationMeta; use crate::imports::*; -use crate::storage::AccountId; use crate::storage::IdT; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Metadata { +pub struct AccountMetadata { pub id: AccountId, #[serde(skip_serializing_if = "Option::is_none")] pub indexes: Option, } -impl Metadata { +impl AccountMetadata { + const STORAGE_MAGIC: u32 = 0x4154454d; + const STORAGE_VERSION: u32 = 0; + pub fn new(id: AccountId, indexes: AddressDerivationMeta) -> Self { Self { id, indexes: Some(indexes) } } @@ -20,9 +29,31 @@ impl Metadata { } } -impl IdT for Metadata { +impl IdT for AccountMetadata { type Id = AccountId; fn id(&self) -> &AccountId { &self.id } } + +impl BorshSerialize for AccountMetadata { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.id, writer)?; + BorshSerialize::serialize(&self.indexes, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for AccountMetadata { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let id = BorshDeserialize::deserialize(buf)?; + let indexes = BorshDeserialize::deserialize(buf)?; + + Ok(Self { id, indexes }) + } +} diff --git a/wallet/core/src/storage/mod.rs b/wallet/core/src/storage/mod.rs index 017cf8a39..d6b8f4090 100644 --- a/wallet/core/src/storage/mod.rs +++ b/wallet/core/src/storage/mod.rs @@ -1,3 +1,7 @@ +//! +//! Wallet data storage subsystem. +//! + pub use crate::encryption::{Decrypted, Encryptable, Encrypted}; pub mod account; @@ -9,34 +13,38 @@ pub mod interface; pub mod keydata; pub mod local; pub mod metadata; +pub mod storable; pub mod transaction; -pub use crate::runtime::{AccountId, AccountKind}; -pub use account::{Account, AccountData, Bip32, Keypair, Legacy, MultiSig, Settings}; +pub use account::{AccountSettings, AccountStorable, AccountStorage}; pub use address::AddressBookEntry; pub use binding::Binding; pub use hint::Hint; pub use id::IdT; -pub use interface::{AccessContextT, AccountStore, Interface, PrvKeyDataStore, TransactionRecordStore, WalletDescriptor}; -pub use keydata::{KeyCaps, PrvKeyData, PrvKeyDataId, PrvKeyDataInfo, PrvKeyDataMap, PrvKeyDataPayload}; +pub use interface::{ + AccountStore, Interface, PrvKeyDataStore, StorageDescriptor, TransactionRecordStore, WalletDescriptor, WalletExportOptions, +}; +pub use keydata::{AssocPrvKeyDataIds, PrvKeyData, PrvKeyDataId, PrvKeyDataInfo, PrvKeyDataMap, PrvKeyDataPayload}; pub use local::interface::make_filename; -pub use metadata::Metadata; -pub use transaction::{TransactionMetadata, TransactionRecord, TransactionType}; +pub use metadata::AccountMetadata; +pub use storable::Storable; +pub use transaction::{TransactionData, TransactionId, TransactionKind, TransactionRecord}; #[cfg(test)] mod tests { use super::*; + use crate::account::variants::bip32::*; + use crate::imports::*; use crate::result::Result; use crate::secret::Secret; use crate::storage::local::Payload; - use crate::storage::local::Wallet; + use crate::storage::local::WalletStorage; use kash_bip32::{Language, Mnemonic}; - use std::sync::Arc; #[tokio::test] - async fn test_wallet_store_wallet_store_load() -> Result<()> { - // This test creates a fake instance of keydata, stored account + async fn test_storage_wallet_store_load() -> Result<()> { + // This test creates a simulated instance of keydata, stored account // instance and a wallet instance that owns them. It then tests // loading of account references and a wallet instance and confirms // that the serialized data is as expected. @@ -50,43 +58,55 @@ mod tests { let mnemonic1s = "caution guide valley easily latin already visual fancy fork car switch runway vicious polar surprise fence boil light nut invite fiction visa hamster coyote".to_string(); let mnemonic2s = "fiber boy desk trip pitch snake table awkward endorse car learn forest solid ticket enemy pink gesture wealth iron chaos clock gather honey farm".to_string(); + println!("generating keys..."); let mnemonic1 = Mnemonic::new(mnemonic1s.clone(), Language::English)?; - let prv_key_data1 = PrvKeyData::try_new_from_mnemonic(mnemonic1.clone(), Some(&payment_secret))?; + let prv_key_data1 = + PrvKeyData::try_new_from_mnemonic(mnemonic1.clone(), Some(&payment_secret), EncryptionKind::XChaCha20Poly1305)?; + let pub_key_data1 = prv_key_data1.create_xpub(Some(&payment_secret), BIP32_ACCOUNT_KIND.into(), 0).await?; let mnemonic2 = Mnemonic::new(mnemonic2s.clone(), Language::English)?; - let prv_key_data2 = PrvKeyData::try_new_from_mnemonic(mnemonic2.clone(), Some(&payment_secret))?; + let prv_key_data2 = + PrvKeyData::try_new_from_mnemonic(mnemonic2.clone(), Some(&payment_secret), EncryptionKind::XChaCha20Poly1305)?; + let pub_key_data2 = prv_key_data2.create_xpub(Some(&payment_secret), BIP32_ACCOUNT_KIND.into(), 0).await?; - let pub_key_data1 = Arc::new(vec!["abc".to_string()]); - let pub_key_data2 = Arc::new(vec!["xyz".to_string()]); - println!("keydata1 id: {:?}", prv_key_data1.id); + // println!("keydata1 id: {:?}", prv_key_data1.id); //assert_eq!(prv_key_data.id.0, [79, 36, 5, 159, 220, 113, 179, 22]); payload.prv_key_data.push(prv_key_data1.clone()); payload.prv_key_data.push(prv_key_data2.clone()); - let settings = Settings { name: Some("Wallet-A".to_string()), title: Some("Wallet A".to_string()), is_visible: false }; - let bip32 = Bip32::new(0, pub_key_data1.clone(), false); - let id = AccountId::from_bip32(&prv_key_data1.id, &bip32); - let account1 = Account::new(id, Some(prv_key_data1.id), settings, AccountData::Bip32(bip32)); + println!("generating accounts..."); + let settings = AccountSettings { name: Some("Wallet-A".to_string()), ..Default::default() }; + let storable = bip32::Payload::new(0, vec![pub_key_data1.clone()].into(), false); + let (id, storage_key) = make_account_hashes(from_bip32(&prv_key_data1.id, &storable)); + let account1 = + AccountStorage::try_new(BIP32_ACCOUNT_KIND.into(), &id, &storage_key, prv_key_data1.id.into(), settings, storable)?; + payload.accounts.push(account1); - let settings = Settings { name: Some("Wallet-B".to_string()), title: Some("Wallet B".to_string()), is_visible: false }; - let bip32 = Bip32::new(0, pub_key_data2.clone(), false); - let id = AccountId::from_bip32(&prv_key_data2.id, &bip32); - let account2 = Account::new(id, Some(prv_key_data2.id), settings, AccountData::Bip32(bip32)); + let settings = AccountSettings { name: Some("Wallet-B".to_string()), ..Default::default() }; + let storable = bip32::Payload::new(0, vec![pub_key_data2.clone()].into(), false); + let (id, storage_key) = make_account_hashes(from_bip32(&prv_key_data2.id, &storable)); + let account2 = + AccountStorage::try_new(BIP32_ACCOUNT_KIND.into(), &id, &storage_key, prv_key_data2.id.into(), settings, storable)?; + payload.accounts.push(account2); let payload_json = serde_json::to_string(&payload).unwrap(); // let settings = WalletSettings::new(account_id); - let w1 = Wallet::try_new(None, None, &wallet_secret, payload, vec![])?; + println!("creating wallet 1..."); + let w1 = WalletStorage::try_new(None, None, &wallet_secret, EncryptionKind::XChaCha20Poly1305, payload, vec![])?; w1.try_store(&store).await?; // Wallet::try_store_payload(&store, &wallet_secret, payload).await?; - let w2 = Wallet::try_load(&store).await?; + println!("loading wallet 2..."); + let w2 = WalletStorage::try_load(&store).await?; + println!("decrypting wallet..."); let w2payload = w2.payload.decrypt::(&wallet_secret).unwrap(); + println!("wallet decrypted..."); println!("\n---\nwallet.metadata (plain): {:#?}\n\n", w2.metadata); // let w2payload_json = serde_json::to_string(w2payload.as_ref()).unwrap(); - println!("\n---nwallet.payload (decrypted): {:#?}\n\n", w2payload.as_ref()); + println!("\n---\nwallet.payload (decrypted): {:#?}\n\n", w2payload.as_ref()); // purge the store store.purge().await?; diff --git a/wallet/core/src/storage/storable.rs b/wallet/core/src/storage/storable.rs new file mode 100644 index 000000000..a93e118f4 --- /dev/null +++ b/wallet/core/src/storage/storable.rs @@ -0,0 +1,9 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +pub trait Storable: Sized + BorshSerialize + BorshDeserialize { + // a unique number used for binary + // serialization data alignment check + const STORAGE_MAGIC: u32; + // AccountStorage binary serialization version + const STORAGE_VERSION: u32; +} diff --git a/wallet/core/src/storage/transaction.rs b/wallet/core/src/storage/transaction.rs deleted file mode 100644 index 6cc72c8b1..000000000 --- a/wallet/core/src/storage/transaction.rs +++ /dev/null @@ -1,437 +0,0 @@ -use crate::imports::*; -use crate::runtime::Wallet; -use crate::storage::Binding; -use crate::tx::{PendingTransaction, PendingTransactionInner}; -use crate::utxo::{UtxoContext, UtxoEntryReference}; -use kash_addresses::Address; -use kash_consensus_core::tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint}; -use separator::Separatable; -use serde::{Deserialize, Serialize}; -use workflow_log::style; - -const TRANSACTION_VERSION: u16 = 1; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum TransactionType { - /// Incoming transaction - Incoming, - /// Transaction created by the runtime - Outgoing, - /// Outgoing transaction observed by the runtime - External, - /// Internal batch (sweep) transaction - Batch, - /// Reorg transaction (caused by UTXO reorg during mining) - Reorg, -} - -impl TransactionType { - pub fn style(&self, s: &str) -> String { - match self { - TransactionType::Incoming => style(s).green().to_string(), - TransactionType::Outgoing => style(s).red().to_string(), - TransactionType::External => style(s).red().to_string(), - TransactionType::Batch => style(s).blue().to_string(), - TransactionType::Reorg => style(s).blue().to_string(), - } - } - pub fn style_with_sign(&self, s: &str, history: bool) -> String { - match self { - TransactionType::Incoming => style("+".to_string() + s).green().to_string(), - TransactionType::Outgoing => style("-".to_string() + s).red().to_string(), - TransactionType::External => style("-".to_string() + s).red().to_string(), - TransactionType::Batch => style("".to_string() + s).dim().to_string(), - TransactionType::Reorg => { - if history { - style("".to_string() + s).dim() - } else { - style("-".to_string() + s).red() - } - } - .to_string(), - } - } -} - -impl TransactionType { - pub fn sign(&self) -> String { - match self { - TransactionType::Incoming => "+", - TransactionType::Outgoing => "-", - TransactionType::External => "-", - TransactionType::Batch => "", - TransactionType::Reorg => "-", - } - .to_string() - } -} - -impl std::fmt::Display for TransactionType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - TransactionType::Incoming => "incoming", - TransactionType::Outgoing => "outgoing", - TransactionType::External => "external", - TransactionType::Batch => "batch", - TransactionType::Reorg => "reorg", - }; - write!(f, "{s}") - } -} - -/// [`UtxoRecord`] represents an incoming transaction UTXO entry -/// stored within [`TransactionRecord`]. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UtxoRecord { - pub address: Option
, - pub index: TransactionIndexType, - pub amount: u64, - #[serde(rename = "scriptPubKey")] - pub script_public_key: ScriptPublicKey, - #[serde(rename = "isCoinbase")] - pub is_coinbase: bool, -} - -impl From for UtxoRecord { - fn from(utxo: UtxoEntryReference) -> Self { - let UtxoEntryReference { utxo } = utxo; - UtxoRecord { - index: utxo.outpoint.get_index(), - address: utxo.address.clone(), - amount: utxo.entry.amount, - script_public_key: utxo.entry.script_public_key.clone(), - is_coinbase: utxo.entry.is_coinbase, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum TransactionMetadata { - Custom(String), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", content = "transaction")] -#[serde(rename_all = "lowercase")] -pub enum TransactionData { - Reorg { - #[serde(rename = "utxoEntries")] - utxo_entries: Vec, - #[serde(rename = "value")] - aggregate_input_value: u64, - }, - Incoming { - #[serde(rename = "utxoEntries")] - utxo_entries: Vec, - #[serde(rename = "value")] - aggregate_input_value: u64, - }, - External { - #[serde(rename = "utxoEntries")] - utxo_entries: Vec, - #[serde(rename = "value")] - aggregate_input_value: u64, - }, - Outgoing { - #[serde(rename = "isFinal")] - is_final: bool, - fees: u64, - #[serde(rename = "inputValue")] - aggregate_input_value: u64, - #[serde(rename = "outputValue")] - aggregate_output_value: u64, - transaction: Transaction, - #[serde(rename = "paymentValue")] - payment_value: Option, - #[serde(rename = "changeValue")] - change_value: u64, - }, -} - -impl TransactionData { - pub fn transaction_type(&self) -> TransactionType { - match self { - TransactionData::Reorg { .. } => TransactionType::Reorg, - TransactionData::Incoming { .. } => TransactionType::Incoming, - TransactionData::External { .. } => TransactionType::External, - TransactionData::Outgoing { is_final, .. } => { - if *is_final { - TransactionType::Outgoing - } else { - TransactionType::Batch - } - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TransactionRecord { - version: u16, - id: TransactionId, - #[serde(skip_serializing_if = "Option::is_none")] - unixtime: Option, - binding: Binding, - #[serde(rename = "blockDaaScore")] - block_daa_score: u64, - #[serde(rename = "network")] - network_id: NetworkId, - #[serde(rename = "data")] - transaction_data: TransactionData, - #[serde(skip_serializing_if = "Option::is_none")] - metadata: Option, -} - -impl TransactionRecord { - pub fn id(&self) -> &TransactionId { - &self.id - } - - pub fn unixtime(&self) -> Option { - self.unixtime - } - - pub fn binding(&self) -> &Binding { - &self.binding - } - - pub fn block_daa_score(&self) -> u64 { - self.block_daa_score - } - - pub fn transaction_type(&self) -> TransactionType { - self.transaction_data.transaction_type() - } - - pub fn network_id(&self) -> &NetworkId { - &self.network_id - } - - pub fn is_coinbase(&self) -> bool { - match &self.transaction_data { - TransactionData::Incoming { utxo_entries, .. } => utxo_entries.iter().any(|entry| entry.is_coinbase), - _ => false, - } - } -} - -impl TransactionRecord { - pub fn new_incoming( - utxo_context: &UtxoContext, - transaction_type: TransactionType, - id: TransactionId, - utxos: Vec, - ) -> Self { - let binding = Binding::from(utxo_context.binding()); - let block_daa_score = utxos[0].utxo.entry.block_daa_score; - let utxo_entries = utxos.into_iter().map(UtxoRecord::from).collect::>(); - let aggregate_input_value = utxo_entries.iter().map(|utxo| utxo.amount).sum::(); - - let transaction_data = match transaction_type { - TransactionType::Incoming => TransactionData::Incoming { utxo_entries, aggregate_input_value }, - TransactionType::Reorg => TransactionData::Reorg { utxo_entries, aggregate_input_value }, - _ => panic!("TransactionRecord::new_incoming() - invalid transaction type"), - }; - - TransactionRecord { - version: TRANSACTION_VERSION, - id, - unixtime: None, - binding, - transaction_data, - block_daa_score, - network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), - metadata: None, - } - } - - pub fn new_external(utxo_context: &UtxoContext, id: TransactionId, utxos: Vec) -> Self { - let binding = Binding::from(utxo_context.binding()); - let block_daa_score = utxos[0].utxo.entry.block_daa_score; - let utxo_entries = utxos.into_iter().map(UtxoRecord::from).collect::>(); - let aggregate_input_value = utxo_entries.iter().map(|utxo| utxo.amount).sum::(); - - let transaction_data = TransactionData::External { utxo_entries, aggregate_input_value }; - - TransactionRecord { - version: TRANSACTION_VERSION, - id, - unixtime: None, - binding, - transaction_data, - block_daa_score, - network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), - metadata: None, - } - } - - pub fn new_outgoing(utxo_context: &UtxoContext, pending_tx: &PendingTransaction) -> Self { - let binding = Binding::from(utxo_context.binding()); - let block_daa_score = - utxo_context.processor().current_daa_score().expect("TransactionRecord::new_outgoing() - missing daa score"); - - let PendingTransactionInner { - signable_tx, - kind, - fees, - aggregate_input_value, - aggregate_output_value, - payment_value, - change_output_value, - .. - } = &*pending_tx.inner; - - let transaction = signable_tx.lock().unwrap().tx.clone(); - let id = transaction.id(); - - let transaction_data = TransactionData::Outgoing { - is_final: kind.is_final(), - fees: *fees, - aggregate_input_value: *aggregate_input_value, - aggregate_output_value: *aggregate_output_value, - transaction, - payment_value: *payment_value, - change_value: *change_output_value, - }; - - TransactionRecord { - version: TRANSACTION_VERSION, - id, - unixtime: None, - binding, - transaction_data, - block_daa_score, - network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), - metadata: None, - } - } - - pub async fn format(&self, wallet: &Arc, include_utxos: bool) -> Vec { - self.format_with_args(wallet, None, None, include_utxos, false, None).await - } - - pub async fn format_with_state(&self, wallet: &Arc, state: Option<&str>, include_utxos: bool) -> Vec { - self.format_with_args(wallet, state, None, include_utxos, false, None).await - } - - pub async fn format_with_args( - &self, - wallet: &Arc, - state: Option<&str>, - current_daa_score: Option, - include_utxos: bool, - history: bool, - account: Option>, - ) -> Vec { - let TransactionRecord { id, binding, block_daa_score, transaction_data, .. } = self; - - let name = match binding { - Binding::Custom(id) => style(id.short()).cyan(), - Binding::Account(account_id) => { - let account = if let Some(account) = account { - Some(account) - } else { - wallet.get_account_by_id(account_id).await.ok().flatten() - }; - - if let Some(account) = account { - style(account.name_with_id()).cyan() - } else { - style(account_id.short() + " ??").magenta() - } - } - }; - - let transaction_type = transaction_data.transaction_type(); - let kind = transaction_type.style(&transaction_type.to_string()); - - let maturity = current_daa_score - .map(|score| { - // TODO - refactor @ high BPS processing - let maturity = if self.is_coinbase() { - crate::utxo::UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA.load(Ordering::SeqCst) - } else { - crate::utxo::UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA.load(Ordering::SeqCst) - }; - - if score < self.block_daa_score() + maturity { - style("pending").dim().to_string() - } else { - style("confirmed").dim().to_string() - } - }) - .unwrap_or_default(); - - let block_daa_score = block_daa_score.separated_string(); - let state = state.unwrap_or(&maturity); - let mut lines = vec![format!("{name} {id} @{block_daa_score} DAA - {kind} {state}")]; - - let suffix = utils::kash_suffix(&self.network_id.network_type); - - match transaction_data { - TransactionData::Reorg { utxo_entries, aggregate_input_value } - | TransactionData::Incoming { utxo_entries, aggregate_input_value } - | TransactionData::External { utxo_entries, aggregate_input_value } => { - let aggregate_input_value = - transaction_type.style_with_sign(utils::sompi_to_kash_string(*aggregate_input_value).as_str(), history); - lines.push(format!("{:>4}UTXOs: {} Total: {}", "", utxo_entries.len(), aggregate_input_value)); - if include_utxos { - for utxo_entry in utxo_entries { - let address = - style(utxo_entry.address.as_ref().map(|addr| addr.to_string()).unwrap_or_else(|| "n/a".to_string())) - .blue(); - let index = utxo_entry.index; - let is_coinbase = if utxo_entry.is_coinbase { - style(format!("coinbase utxo [{index}]")).dim() - } else { - style(format!("standard utxo [{index}]")).dim() - }; - let amount = - transaction_type.style_with_sign(utils::sompi_to_kash_string(utxo_entry.amount).as_str(), history); - - lines.push(format!("{:>4}{address}", "")); - lines.push(format!("{:>4}{amount} {suffix} {is_coinbase}", "")); - } - } - } - TransactionData::Outgoing { fees, aggregate_input_value, transaction, payment_value, change_value, .. } => { - if let Some(payment_value) = payment_value { - lines.push(format!( - "{:>4}Payment: {} Used: {} Fees: {} Change: {} UTXOs: [{}↠{}]", - "", - style(utils::sompi_to_kash_string(*payment_value)).red(), - style(utils::sompi_to_kash_string(*aggregate_input_value)).blue(), - style(utils::sompi_to_kash_string(*fees)).red(), - style(utils::sompi_to_kash_string(*change_value)).green(), - transaction.inputs.len(), - transaction.outputs.len(), - )); - } else { - lines.push(format!( - "{:>4}Sweep: {} Fees: {} Change: {} UTXOs: [{}↠{}]", - "", - style(utils::sompi_to_kash_string(*aggregate_input_value)).blue(), - style(utils::sompi_to_kash_string(*fees)).red(), - style(utils::sompi_to_kash_string(*change_value)).green(), - transaction.inputs.len(), - transaction.outputs.len(), - )); - } - - if include_utxos { - for input in transaction.inputs.iter() { - let TransactionInput { previous_outpoint, signature_script: _, sequence, sig_op_count } = input; - let TransactionOutpoint { transaction_id, index } = previous_outpoint; - - lines.push(format!("{:>4}{sequence:>2}: {transaction_id}:{index} SigOps: {sig_op_count}", "")); - // lines.push(format!("{:>4}{:>2} Sig Ops: {sig_op_count}", "", "")); - // lines.push(format!("{:>4}{:>2} Script: {}", "", "", signature_script.to_hex())); - } - } - } - } - - lines - } -} diff --git a/wallet/core/src/storage/transaction/data.rs b/wallet/core/src/storage/transaction/data.rs new file mode 100644 index 000000000..32f8dfb6d --- /dev/null +++ b/wallet/core/src/storage/transaction/data.rs @@ -0,0 +1,394 @@ +//! +//! Wallet transaction data variants. +//! + +use super::UtxoRecord; +use crate::imports::*; +use kash_consensus_core::tx::Transaction; +pub use kash_consensus_core::tx::TransactionId; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "transaction")] +// the reason the struct is renamed kebab-case and then +// each field is renamed to camelCase is to force the +// enum tags to be lower case. +#[serde(rename_all = "kebab-case")] +pub enum TransactionData { + Reorg { + #[serde(rename = "utxoEntries")] + utxo_entries: Vec, + #[serde(rename = "value")] + aggregate_input_value: u64, + }, + Incoming { + #[serde(rename = "utxoEntries")] + utxo_entries: Vec, + #[serde(rename = "value")] + aggregate_input_value: u64, + }, + Stasis { + #[serde(rename = "utxoEntries")] + utxo_entries: Vec, + #[serde(rename = "value")] + aggregate_input_value: u64, + }, + External { + #[serde(rename = "utxoEntries")] + utxo_entries: Vec, + #[serde(rename = "value")] + aggregate_input_value: u64, + }, + Batch { + fees: u64, + #[serde(rename = "inputValue")] + aggregate_input_value: u64, + #[serde(rename = "outputValue")] + aggregate_output_value: u64, + transaction: Transaction, + #[serde(rename = "paymentValue")] + payment_value: Option, + #[serde(rename = "changeValue")] + change_value: u64, + #[serde(rename = "acceptedDaaScore")] + accepted_daa_score: Option, + #[serde(rename = "utxoEntries")] + #[serde(default)] + utxo_entries: Vec, + }, + Outgoing { + fees: u64, + #[serde(rename = "inputValue")] + aggregate_input_value: u64, + #[serde(rename = "outputValue")] + aggregate_output_value: u64, + transaction: Transaction, + #[serde(rename = "paymentValue")] + payment_value: Option, + #[serde(rename = "changeValue")] + change_value: u64, + #[serde(rename = "acceptedDaaScore")] + accepted_daa_score: Option, + #[serde(rename = "utxoEntries")] + #[serde(default)] + utxo_entries: Vec, + }, + TransferIncoming { + fees: u64, + #[serde(rename = "inputValue")] + aggregate_input_value: u64, + #[serde(rename = "outputValue")] + aggregate_output_value: u64, + transaction: Transaction, + #[serde(rename = "paymentValue")] + payment_value: Option, + #[serde(rename = "changeValue")] + change_value: u64, + #[serde(rename = "acceptedDaaScore")] + accepted_daa_score: Option, + #[serde(rename = "utxoEntries")] + utxo_entries: Vec, + }, + TransferOutgoing { + fees: u64, + #[serde(rename = "inputValue")] + aggregate_input_value: u64, + #[serde(rename = "outputValue")] + aggregate_output_value: u64, + transaction: Transaction, + #[serde(rename = "paymentValue")] + payment_value: Option, + #[serde(rename = "changeValue")] + change_value: u64, + #[serde(rename = "acceptedDaaScore")] + accepted_daa_score: Option, + #[serde(rename = "utxoEntries")] + utxo_entries: Vec, + }, + Change { + #[serde(rename = "inputValue")] + aggregate_input_value: u64, + #[serde(rename = "outputValue")] + aggregate_output_value: u64, + transaction: Transaction, + #[serde(rename = "paymentValue")] + payment_value: Option, + #[serde(rename = "changeValue")] + change_value: u64, + #[serde(rename = "acceptedDaaScore")] + accepted_daa_score: Option, + #[serde(rename = "utxoEntries")] + utxo_entries: Vec, + }, +} + +impl TransactionData { + const STORAGE_MAGIC: u32 = 0x54445854; + const STORAGE_VERSION: u32 = 0; + + pub fn kind(&self) -> TransactionKind { + match self { + TransactionData::Reorg { .. } => TransactionKind::Reorg, + TransactionData::Stasis { .. } => TransactionKind::Stasis, + TransactionData::Incoming { .. } => TransactionKind::Incoming, + TransactionData::External { .. } => TransactionKind::External, + TransactionData::Outgoing { .. } => TransactionKind::Outgoing, + TransactionData::Batch { .. } => TransactionKind::Batch, + TransactionData::TransferIncoming { .. } => TransactionKind::TransferIncoming, + TransactionData::TransferOutgoing { .. } => TransactionKind::TransferOutgoing, + TransactionData::Change { .. } => TransactionKind::Change, + } + } +} + +impl BorshSerialize for TransactionData { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + + let kind = self.kind(); + BorshSerialize::serialize(&kind, writer)?; + + match self { + TransactionData::Reorg { utxo_entries, aggregate_input_value } => { + BorshSerialize::serialize(utxo_entries, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + } + TransactionData::Incoming { utxo_entries, aggregate_input_value } => { + BorshSerialize::serialize(utxo_entries, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + } + TransactionData::Stasis { utxo_entries, aggregate_input_value } => { + BorshSerialize::serialize(utxo_entries, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + } + TransactionData::External { utxo_entries, aggregate_input_value } => { + BorshSerialize::serialize(utxo_entries, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + } + TransactionData::Batch { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + } => { + BorshSerialize::serialize(fees, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + BorshSerialize::serialize(aggregate_output_value, writer)?; + BorshSerialize::serialize(transaction, writer)?; + BorshSerialize::serialize(payment_value, writer)?; + BorshSerialize::serialize(change_value, writer)?; + BorshSerialize::serialize(accepted_daa_score, writer)?; + BorshSerialize::serialize(utxo_entries, writer)?; + } + TransactionData::Outgoing { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + } => { + BorshSerialize::serialize(fees, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + BorshSerialize::serialize(aggregate_output_value, writer)?; + BorshSerialize::serialize(transaction, writer)?; + BorshSerialize::serialize(payment_value, writer)?; + BorshSerialize::serialize(change_value, writer)?; + BorshSerialize::serialize(accepted_daa_score, writer)?; + BorshSerialize::serialize(utxo_entries, writer)?; + } + TransactionData::TransferIncoming { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + } => { + BorshSerialize::serialize(fees, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + BorshSerialize::serialize(aggregate_output_value, writer)?; + BorshSerialize::serialize(transaction, writer)?; + BorshSerialize::serialize(payment_value, writer)?; + BorshSerialize::serialize(change_value, writer)?; + BorshSerialize::serialize(accepted_daa_score, writer)?; + BorshSerialize::serialize(utxo_entries, writer)?; + } + TransactionData::TransferOutgoing { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + } => { + BorshSerialize::serialize(fees, writer)?; + BorshSerialize::serialize(aggregate_input_value, writer)?; + BorshSerialize::serialize(aggregate_output_value, writer)?; + BorshSerialize::serialize(transaction, writer)?; + BorshSerialize::serialize(payment_value, writer)?; + BorshSerialize::serialize(change_value, writer)?; + BorshSerialize::serialize(accepted_daa_score, writer)?; + BorshSerialize::serialize(utxo_entries, writer)?; + } + TransactionData::Change { + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + } => { + BorshSerialize::serialize(aggregate_input_value, writer)?; + BorshSerialize::serialize(aggregate_output_value, writer)?; + BorshSerialize::serialize(transaction, writer)?; + BorshSerialize::serialize(payment_value, writer)?; + BorshSerialize::serialize(change_value, writer)?; + BorshSerialize::serialize(accepted_daa_score, writer)?; + BorshSerialize::serialize(utxo_entries, writer)?; + } + } + + Ok(()) + } +} + +impl BorshDeserialize for TransactionData { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let kind: TransactionKind = BorshDeserialize::deserialize(buf)?; + + match kind { + TransactionKind::Reorg => { + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::Reorg { utxo_entries, aggregate_input_value }) + } + TransactionKind::Incoming => { + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::Incoming { utxo_entries, aggregate_input_value }) + } + TransactionKind::Stasis => { + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::Stasis { utxo_entries, aggregate_input_value }) + } + TransactionKind::External => { + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::External { utxo_entries, aggregate_input_value }) + } + TransactionKind::Batch => { + let fees: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; + let transaction: Transaction = BorshDeserialize::deserialize(buf)?; + let payment_value: Option = BorshDeserialize::deserialize(buf)?; + let change_value: u64 = BorshDeserialize::deserialize(buf)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::Batch { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + }) + } + TransactionKind::Outgoing => { + let fees: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; + let transaction: Transaction = BorshDeserialize::deserialize(buf)?; + let payment_value: Option = BorshDeserialize::deserialize(buf)?; + let change_value: u64 = BorshDeserialize::deserialize(buf)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::Outgoing { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + }) + } + TransactionKind::TransferIncoming => { + let fees: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; + let transaction: Transaction = BorshDeserialize::deserialize(buf)?; + let payment_value: Option = BorshDeserialize::deserialize(buf)?; + let change_value: u64 = BorshDeserialize::deserialize(buf)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::TransferIncoming { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + }) + } + TransactionKind::TransferOutgoing => { + let fees: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; + let transaction: Transaction = BorshDeserialize::deserialize(buf)?; + let payment_value: Option = BorshDeserialize::deserialize(buf)?; + let change_value: u64 = BorshDeserialize::deserialize(buf)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::TransferOutgoing { + fees, + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + }) + } + TransactionKind::Change => { + let aggregate_input_value: u64 = BorshDeserialize::deserialize(buf)?; + let aggregate_output_value: u64 = BorshDeserialize::deserialize(buf)?; + let transaction: Transaction = BorshDeserialize::deserialize(buf)?; + let payment_value: Option = BorshDeserialize::deserialize(buf)?; + let change_value: u64 = BorshDeserialize::deserialize(buf)?; + let accepted_daa_score: Option = BorshDeserialize::deserialize(buf)?; + let utxo_entries: Vec = BorshDeserialize::deserialize(buf)?; + Ok(TransactionData::Change { + aggregate_input_value, + aggregate_output_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + utxo_entries, + }) + } + } + } +} diff --git a/wallet/core/src/storage/transaction/kind.rs b/wallet/core/src/storage/transaction/kind.rs new file mode 100644 index 000000000..3e5aff558 --- /dev/null +++ b/wallet/core/src/storage/transaction/kind.rs @@ -0,0 +1,85 @@ +//! +//! Wallet transaction record types. +//! + +use crate::imports::*; +pub use kash_consensus_core::tx::TransactionId; + +// Do not change the order of the variants in this enum. +seal! { 0x93c6, { + #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Eq, PartialEq)] + #[serde(rename_all = "kebab-case")] + pub enum TransactionKind { + /// Reorg transaction (caused by UTXO reorg). + /// NOTE: These transactions should be ignored by clients + /// if the transaction has not reached Pending maturity. + Reorg, + /// Stasis transaction (caused by a reorg during coinbase UTXO stasis). + /// NOTE: These types of transactions should be ignored by clients. + Stasis, + /// Internal batch (sweep) transaction. Generated as a part + /// of Outgoing or Transfer transactions if the number of + /// UTXOs needed for transaction is greater than the transaction + /// mass limit. + Batch, + /// Change transaction. Generated as a part of the Outgoing + /// or Transfer transactions. + /// NOTE: These types of transactions should be ignored by clients + Change, + /// A regular incoming transaction comprised of one or more UTXOs. + Incoming, + /// An outgoing transaction created by the wallet framework. + /// If transaction creation results in multiple sweep transactions, + /// this is the final transaction in the transaction tree. + Outgoing, + /// Externally triggered *Outgoing* transaction observed by + /// the wallet runtime. This only occurs when another wallet + /// issues an outgoing transaction from addresses monitored + /// by this instance of the wallet (for example a copy of + /// the wallet or an account). + External, + /// Incoming transfer transaction. A transfer between multiple + /// accounts managed by the wallet runtime. + TransferIncoming, + /// Outgoing transfer transaction. A transfer between multiple + /// accounts managed by the wallet runtime. + TransferOutgoing, + } + } +} + +impl TransactionKind {} + +impl TransactionKind { + pub fn sign(&self) -> String { + match self { + TransactionKind::Incoming => "+", + TransactionKind::Outgoing => "-", + TransactionKind::External => "-", + TransactionKind::Batch => "", + TransactionKind::Reorg => "-", + TransactionKind::Stasis => "", + TransactionKind::TransferIncoming => "", + TransactionKind::TransferOutgoing => "", + TransactionKind::Change => "", + } + .to_string() + } +} + +impl std::fmt::Display for TransactionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + TransactionKind::Incoming => "incoming", + TransactionKind::Outgoing => "outgoing", + TransactionKind::External => "external", + TransactionKind::Batch => "batch", + TransactionKind::Reorg => "reorg", + TransactionKind::Stasis => "stasis", + TransactionKind::TransferIncoming => "transfer-incoming", + TransactionKind::TransferOutgoing => "transfer-outgoing", + TransactionKind::Change => "change", + }; + write!(f, "{s}") + } +} diff --git a/wallet/core/src/storage/transaction/mod.rs b/wallet/core/src/storage/transaction/mod.rs new file mode 100644 index 000000000..b6d75f4d7 --- /dev/null +++ b/wallet/core/src/storage/transaction/mod.rs @@ -0,0 +1,13 @@ +//! +//! Wallet transaction records. +//! + +pub mod data; +pub mod kind; +pub mod record; +pub mod utxo; + +pub use data::*; +pub use kind::*; +pub use record::*; +pub use utxo::*; diff --git a/wallet/core/src/storage/transaction/record.rs b/wallet/core/src/storage/transaction/record.rs new file mode 100644 index 000000000..b7d190021 --- /dev/null +++ b/wallet/core/src/storage/transaction/record.rs @@ -0,0 +1,518 @@ +//! +//! Wallet transaction record implementation. +//! + +use super::*; +use crate::imports::*; +use crate::storage::Binding; +use crate::tx::PendingTransactionInner; +use workflow_core::time::{unixtime_as_millis_u64, unixtime_to_locale_string}; + +pub use kash_consensus_core::tx::TransactionId; +use zeroize::Zeroize; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionRecord { + pub id: TransactionId, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "unixtimeMsec")] + pub unixtime_msec: Option, + pub value: u64, + pub binding: Binding, + #[serde(rename = "blockDaaScore")] + pub block_daa_score: u64, + #[serde(rename = "network")] + pub network_id: NetworkId, + #[serde(rename = "data")] + pub transaction_data: TransactionData, + #[serde(skip_serializing_if = "Option::is_none")] + pub note: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +impl TransactionRecord { + const STORAGE_MAGIC: u32 = 0x5854414b; + const STORAGE_VERSION: u32 = 0; + + pub fn id(&self) -> &TransactionId { + &self.id + } + + pub fn unixtime_msec(&self) -> Option { + self.unixtime_msec + } + + pub fn unixtime_as_locale_string(&self) -> Option { + self.unixtime_msec.map(unixtime_to_locale_string) + } + + pub fn unixtime_or_daa_as_string(&self) -> String { + if let Some(unixtime) = self.unixtime_msec { + unixtime_to_locale_string(unixtime) + } else { + self.block_daa_score.separated_string() + } + } + + pub fn set_unixtime(&mut self, unixtime: u64) { + self.unixtime_msec = Some(unixtime); + } + + pub fn binding(&self) -> &Binding { + &self.binding + } + + pub fn block_daa_score(&self) -> u64 { + self.block_daa_score + } + + pub fn maturity(&self, current_daa_score: u64) -> Maturity { + // TODO - refactor @ high BPS processing + let maturity = if self.is_coinbase() { + crate::utxo::UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA.load(Ordering::SeqCst) + } else { + crate::utxo::UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA.load(Ordering::SeqCst) + }; + + if current_daa_score < self.block_daa_score() + maturity { + Maturity::Pending + } else { + Maturity::Confirmed + } + } + + pub fn kind(&self) -> TransactionKind { + self.transaction_data.kind() + } + + pub fn network_id(&self) -> &NetworkId { + &self.network_id + } + + pub fn is_coinbase(&self) -> bool { + match &self.transaction_data { + TransactionData::Incoming { utxo_entries, .. } => utxo_entries.iter().any(|entry| entry.is_coinbase), + _ => false, + } + } + + pub fn is_outgoing(&self) -> bool { + matches!(&self.transaction_data, TransactionData::Outgoing { .. }) + } + + pub fn is_change(&self) -> bool { + matches!(&self.transaction_data, TransactionData::Change { .. }) + } + + pub fn is_batch(&self) -> bool { + matches!(&self.transaction_data, TransactionData::Batch { .. }) + } + + pub fn is_transfer(&self) -> bool { + matches!(&self.transaction_data, TransactionData::TransferIncoming { .. } | TransactionData::TransferOutgoing { .. }) + } + + pub fn transaction_data(&self) -> &TransactionData { + &self.transaction_data + } + + // Transaction maturity ignores the stasis period and provides + // a progress value based on the pending period. It is assumed + // that transactions in stasis are not visible to the user. + pub fn maturity_progress(&self, current_daa_score: u64) -> Option { + let maturity = if self.is_coinbase() { + crate::utxo::UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA.load(Ordering::SeqCst) + } else { + crate::utxo::UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA.load(Ordering::SeqCst) + }; + + if current_daa_score < self.block_daa_score + maturity { + Some((current_daa_score - self.block_daa_score) as f64 / maturity as f64) + } else { + None + } + } + + pub fn aggregate_input_value(&self) -> u64 { + match &self.transaction_data { + TransactionData::Reorg { aggregate_input_value, .. } + | TransactionData::Stasis { aggregate_input_value, .. } + | TransactionData::Incoming { aggregate_input_value, .. } + | TransactionData::External { aggregate_input_value, .. } + | TransactionData::Outgoing { aggregate_input_value, .. } + | TransactionData::Batch { aggregate_input_value, .. } + | TransactionData::TransferIncoming { aggregate_input_value, .. } + | TransactionData::TransferOutgoing { aggregate_input_value, .. } + | TransactionData::Change { aggregate_input_value, .. } => *aggregate_input_value, + } + } + + pub fn value(&self) -> u64 { + self.value + } +} + +impl TransactionRecord { + pub fn new_incoming(utxo_context: &UtxoContext, id: TransactionId, utxos: &[UtxoEntryReference]) -> Self { + Self::new_incoming_impl(utxo_context, TransactionKind::Incoming, id, utxos) + } + + pub fn new_reorg(utxo_context: &UtxoContext, id: TransactionId, utxos: &[UtxoEntryReference]) -> Self { + Self::new_incoming_impl(utxo_context, TransactionKind::Reorg, id, utxos) + } + + pub fn new_stasis(utxo_context: &UtxoContext, id: TransactionId, utxos: &[UtxoEntryReference]) -> Self { + Self::new_incoming_impl(utxo_context, TransactionKind::Stasis, id, utxos) + } + + fn new_incoming_impl( + utxo_context: &UtxoContext, + transaction_type: TransactionKind, + id: TransactionId, + utxos: &[UtxoEntryReference], + ) -> Self { + let binding = Binding::from(utxo_context.binding()); + let block_daa_score = utxos[0].utxo.entry.block_daa_score; + let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); + let aggregate_input_value = utxo_entries.iter().map(|utxo| utxo.amount).sum::(); + + let unixtime = unixtime_as_millis_u64(); + + let transaction_data = match transaction_type { + TransactionKind::Incoming => TransactionData::Incoming { utxo_entries, aggregate_input_value }, + TransactionKind::Reorg => TransactionData::Reorg { utxo_entries, aggregate_input_value }, + TransactionKind::Stasis => TransactionData::Stasis { utxo_entries, aggregate_input_value }, + kind => panic!("TransactionRecord::new_incoming() - invalid transaction type: {kind:?}"), + }; + + TransactionRecord { + id, + unixtime_msec: Some(unixtime), + value: aggregate_input_value, + binding, + transaction_data, + block_daa_score, + network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), + metadata: None, + note: None, + } + } + + /// Transaction that was not issued by this instance of the wallet + /// but belongs to this address set. This is an "external" transaction + /// that occurs during the lifetime of this wallet. + pub fn new_external(utxo_context: &UtxoContext, id: TransactionId, utxos: &[UtxoEntryReference]) -> Self { + let binding = Binding::from(utxo_context.binding()); + let block_daa_score = utxos[0].utxo.entry.block_daa_score; + let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); + let aggregate_input_value = utxo_entries.iter().map(|utxo| utxo.amount).sum::(); + + let transaction_data = TransactionData::External { utxo_entries, aggregate_input_value }; + let unixtime = unixtime_as_millis_u64(); + + TransactionRecord { + id, + unixtime_msec: Some(unixtime), + value: aggregate_input_value, + binding, + transaction_data, + block_daa_score, + network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), + metadata: None, + note: None, + } + } + + pub fn new_outgoing( + utxo_context: &UtxoContext, + outgoing_tx: &OutgoingTransaction, + accepted_daa_score: Option, + ) -> Result { + let binding = Binding::from(utxo_context.binding()); + let block_daa_score = + utxo_context.processor().current_daa_score().ok_or(Error::MissingDaaScore("TransactionRecord::new_outgoing()"))?; + + let utxo_entries = outgoing_tx.utxo_entries().into_iter().map(UtxoRecord::from).collect::>(); + + let unixtime = unixtime_as_millis_u64(); + + let PendingTransactionInner { + signable_tx, + fees, + aggregate_input_value, + aggregate_output_value, + payment_value, + change_output_value, + .. + } = &*outgoing_tx.pending_transaction().inner; + + let transaction = signable_tx.lock().unwrap().tx.clone(); + let id = transaction.id(); + + let transaction_data = TransactionData::Outgoing { + fees: *fees, + aggregate_input_value: *aggregate_input_value, + aggregate_output_value: *aggregate_output_value, + transaction, + payment_value: *payment_value, + change_value: *change_output_value, + accepted_daa_score, + utxo_entries, + }; + + Ok(TransactionRecord { + id, + unixtime_msec: Some(unixtime), + value: payment_value.unwrap_or(*aggregate_input_value), + binding, + transaction_data, + block_daa_score, + network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), + metadata: None, + note: None, + }) + } + + pub fn new_batch(utxo_context: &UtxoContext, outgoing_tx: &OutgoingTransaction, accepted_daa_score: Option) -> Result { + let binding = Binding::from(utxo_context.binding()); + let block_daa_score = + utxo_context.processor().current_daa_score().ok_or(Error::MissingDaaScore("TransactionRecord::new_batch()"))?; + + let utxo_entries = outgoing_tx.utxo_entries().into_iter().map(UtxoRecord::from).collect::>(); + + let unixtime = unixtime_as_millis_u64(); + + let PendingTransactionInner { + signable_tx, + fees, + aggregate_input_value, + aggregate_output_value, + payment_value, + change_output_value, + .. + } = &*outgoing_tx.pending_transaction().inner; + + let transaction = signable_tx.lock().unwrap().tx.clone(); + let id = transaction.id(); + + let transaction_data = TransactionData::Batch { + fees: *fees, + aggregate_input_value: *aggregate_input_value, + aggregate_output_value: *aggregate_output_value, + transaction, + payment_value: *payment_value, + change_value: *change_output_value, + accepted_daa_score, + utxo_entries, + }; + + Ok(TransactionRecord { + id, + unixtime_msec: Some(unixtime), + value: payment_value.unwrap_or(*aggregate_input_value), + binding, + transaction_data, + block_daa_score, + network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), + metadata: None, + note: None, + }) + } + + pub fn new_transfer_incoming( + utxo_context: &UtxoContext, + outgoing_tx: &OutgoingTransaction, + accepted_daa_score: Option, + utxos: &[UtxoEntryReference], + ) -> Result { + let binding = Binding::from(utxo_context.binding()); + let block_daa_score = utxo_context + .processor() + .current_daa_score() + .ok_or(Error::MissingDaaScore("TransactionRecord::new_transfer_incoming()"))?; + let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); + + let unixtime = unixtime_as_millis_u64(); + + let PendingTransactionInner { + signable_tx, + fees, + aggregate_input_value, + aggregate_output_value, + payment_value, + change_output_value, + .. + } = &*outgoing_tx.pending_transaction().inner; + + let transaction = signable_tx.lock().unwrap().tx.clone(); + let id = transaction.id(); + + let transaction_data = TransactionData::TransferIncoming { + fees: *fees, + aggregate_input_value: *aggregate_input_value, + aggregate_output_value: *aggregate_output_value, + transaction, + payment_value: *payment_value, + change_value: *change_output_value, + accepted_daa_score, + utxo_entries, + }; + + Ok(TransactionRecord { + id, + unixtime_msec: Some(unixtime), + value: payment_value.unwrap_or(*aggregate_input_value), + binding, + transaction_data, + block_daa_score, + network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), + metadata: None, + note: None, + }) + } + + pub fn new_transfer_outgoing( + utxo_context: &UtxoContext, + outgoing_tx: &OutgoingTransaction, + accepted_daa_score: Option, + utxos: &[UtxoEntryReference], + ) -> Result { + let binding = Binding::from(utxo_context.binding()); + let block_daa_score = utxo_context + .processor() + .current_daa_score() + .ok_or(Error::MissingDaaScore("TransactionRecord::new_transfer_outgoing()"))?; + let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); + + let unixtime = unixtime_as_millis_u64(); + + let PendingTransactionInner { + signable_tx, + fees, + aggregate_input_value, + aggregate_output_value, + payment_value, + change_output_value, + .. + } = &*outgoing_tx.pending_transaction().inner; + + let transaction = signable_tx.lock().unwrap().tx.clone(); + let id = transaction.id(); + + let transaction_data = TransactionData::TransferOutgoing { + fees: *fees, + aggregate_input_value: *aggregate_input_value, + aggregate_output_value: *aggregate_output_value, + transaction, + payment_value: *payment_value, + change_value: *change_output_value, + accepted_daa_score, + utxo_entries, + }; + + Ok(TransactionRecord { + id, + unixtime_msec: Some(unixtime), + value: payment_value.unwrap_or(*aggregate_input_value), + binding, + transaction_data, + block_daa_score, + network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), + metadata: None, + note: None, + }) + } + + pub fn new_change( + utxo_context: &UtxoContext, + outgoing_tx: &OutgoingTransaction, + accepted_daa_score: Option, + utxos: &[UtxoEntryReference], + ) -> Result { + let binding = Binding::from(utxo_context.binding()); + let block_daa_score = + utxo_context.processor().current_daa_score().ok_or(Error::MissingDaaScore("TransactionRecord::new_change()"))?; + let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); + + let unixtime = unixtime_as_millis_u64(); + + let PendingTransactionInner { + signable_tx, + aggregate_input_value, + aggregate_output_value, + payment_value, + change_output_value, + .. + } = &*outgoing_tx.pending_transaction().inner; + + let transaction = signable_tx.lock().unwrap().tx.clone(); + let id = transaction.id(); + + let transaction_data = TransactionData::Change { + aggregate_input_value: *aggregate_input_value, + aggregate_output_value: *aggregate_output_value, + transaction, + payment_value: *payment_value, + change_value: *change_output_value, + accepted_daa_score, + utxo_entries, + }; + + Ok(TransactionRecord { + id, + unixtime_msec: Some(unixtime), + value: *change_output_value, + binding, + transaction_data, + block_daa_score, + network_id: utxo_context.processor().network_id().expect("network expected for transaction record generation"), + metadata: None, + note: None, + }) + } +} + +impl Zeroize for TransactionRecord { + fn zeroize(&mut self) { + // TODO - this trait is added due to the + // Encryptable requirement + // for T to be Zeroize. + } +} + +impl BorshSerialize for TransactionRecord { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + StorageHeader::new(Self::STORAGE_MAGIC, Self::STORAGE_VERSION).serialize(writer)?; + BorshSerialize::serialize(&self.id, writer)?; + BorshSerialize::serialize(&self.unixtime_msec, writer)?; + BorshSerialize::serialize(&self.value, writer)?; + BorshSerialize::serialize(&self.binding, writer)?; + BorshSerialize::serialize(&self.block_daa_score, writer)?; + BorshSerialize::serialize(&self.network_id, writer)?; + BorshSerialize::serialize(&self.transaction_data, writer)?; + BorshSerialize::serialize(&self.note, writer)?; + BorshSerialize::serialize(&self.metadata, writer)?; + + Ok(()) + } +} + +impl BorshDeserialize for TransactionRecord { + fn deserialize(buf: &mut &[u8]) -> IoResult { + let StorageHeader { version: _, .. } = + StorageHeader::deserialize(buf)?.try_magic(Self::STORAGE_MAGIC)?.try_version(Self::STORAGE_VERSION)?; + + let id = BorshDeserialize::deserialize(buf)?; + let unixtime = BorshDeserialize::deserialize(buf)?; + let value = BorshDeserialize::deserialize(buf)?; + let binding = BorshDeserialize::deserialize(buf)?; + let block_daa_score = BorshDeserialize::deserialize(buf)?; + let network_id = BorshDeserialize::deserialize(buf)?; + let transaction_data = BorshDeserialize::deserialize(buf)?; + let note = BorshDeserialize::deserialize(buf)?; + let metadata = BorshDeserialize::deserialize(buf)?; + + Ok(Self { id, unixtime_msec: unixtime, value, binding, block_daa_score, network_id, transaction_data, note, metadata }) + } +} diff --git a/wallet/core/src/storage/transaction/utxo.rs b/wallet/core/src/storage/transaction/utxo.rs new file mode 100644 index 000000000..c52a2d8ce --- /dev/null +++ b/wallet/core/src/storage/transaction/utxo.rs @@ -0,0 +1,35 @@ +//! +//! UTXO record representation used by wallet transactions. +//! + +use crate::imports::*; +use kash_addresses::Address; +use serde::{Deserialize, Serialize}; + +pub use kash_consensus_core::tx::TransactionId; + +/// [`UtxoRecord`] represents an incoming transaction UTXO entry +/// stored within [`TransactionRecord`]. +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct UtxoRecord { + pub address: Option
, + pub index: TransactionIndexType, + pub amount: u64, + #[serde(rename = "scriptPubKey")] + pub script_public_key: ScriptPublicKey, + #[serde(rename = "isCoinbase")] + pub is_coinbase: bool, +} + +impl From<&UtxoEntryReference> for UtxoRecord { + fn from(utxo: &UtxoEntryReference) -> Self { + let UtxoEntryReference { utxo } = utxo; + UtxoRecord { + index: utxo.outpoint.get_index(), + address: utxo.address.clone(), + amount: utxo.entry.amount, + script_public_key: utxo.entry.script_public_key.clone(), + is_coinbase: utxo.entry.is_coinbase, + } + } +} diff --git a/wallet/core/src/tests/keys.rs b/wallet/core/src/tests/keys.rs new file mode 100644 index 000000000..7f2451cc9 --- /dev/null +++ b/wallet/core/src/tests/keys.rs @@ -0,0 +1,8 @@ +use crate::imports::*; +pub fn make_xpub() -> ExtendedPublicKeySecp256k1 { + use kash_bip32::ExtendedKey; + let xpub_base58 = + "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"; + let xpub = xpub_base58.parse::().unwrap(); + xpub.try_into().unwrap() +} diff --git a/wallet/core/src/tests/mod.rs b/wallet/core/src/tests/mod.rs new file mode 100644 index 000000000..37e86f57c --- /dev/null +++ b/wallet/core/src/tests/mod.rs @@ -0,0 +1,13 @@ +//! +//! Utilities and helpers for unit and integration testing. +//! + +#[cfg(test)] +mod rpc_core_mock; +pub use rpc_core_mock::*; + +mod keys; +pub use keys::*; + +mod storage; +pub use storage::*; diff --git a/wallet/core/src/tests/rpc_core_mock.rs b/wallet/core/src/tests/rpc_core_mock.rs new file mode 100644 index 000000000..86cfef3cf --- /dev/null +++ b/wallet/core/src/tests/rpc_core_mock.rs @@ -0,0 +1,258 @@ +use crate::imports::*; + +use async_channel::{unbounded, Receiver}; +use async_trait::async_trait; +use kash_notify::events::EVENT_TYPE_ARRAY; +use kash_notify::listener::ListenerId; +use kash_notify::notifier::{Notifier, Notify}; +use kash_notify::scope::Scope; +use kash_rpc_core::api::ctl::RpcCtl; +use kash_rpc_core::{api::rpc::RpcApi, *}; +use kash_rpc_core::{notify::connection::ChannelConnection, RpcResult}; +use std::sync::Arc; + +pub type RpcCoreNotifier = Notifier; + +impl From> for Rpc { + fn from(rpc_mock: Arc) -> Self { + Self::new(rpc_mock.clone(), rpc_mock.ctl.clone()) + } +} + +pub struct RpcCoreMock { + ctl: RpcCtl, + core_notifier: Arc, + _sync_receiver: Receiver<()>, +} + +impl RpcCoreMock { + pub fn new() -> Self { + Self::default() + } + + pub fn core_notifier(&self) -> Arc { + self.core_notifier.clone() + } + + #[allow(dead_code)] + pub fn notify_new_block_template(&self) -> kash_notify::error::Result<()> { + let notification = Notification::NewBlockTemplate(NewBlockTemplateNotification {}); + self.core_notifier.notify(notification) + } + + #[allow(dead_code)] + pub async fn notify_complete(&self) { + assert!(self._sync_receiver.recv().await.is_ok(), "the notifier sync channel is unexpectedly empty and closed"); + } + + pub fn start(&self) { + self.core_notifier.clone().start(); + } + + pub async fn join(&self) { + self.core_notifier.join().await.expect("core notifier shutdown") + } + + // --- + + pub fn ctl(&self) -> RpcCtl { + self.ctl.clone() + } +} + +impl Default for RpcCoreMock { + fn default() -> Self { + let (sync_sender, sync_receiver) = unbounded(); + let core_notifier: Arc = + Arc::new(Notifier::with_sync("rpc-core", EVENT_TYPE_ARRAY[..].into(), vec![], vec![], 10, Some(sync_sender))); + Self { core_notifier, _sync_receiver: sync_receiver, ctl: RpcCtl::new() } + } +} + +#[async_trait] +impl RpcApi for RpcCoreMock { + // This fn needs to succeed while the client connects + async fn get_info_call(&self, _request: GetInfoRequest) -> RpcResult { + Ok(GetInfoResponse { + p2p_id: "wallet-mock".to_string(), + mempool_size: 1234, + server_version: "mock".to_string(), + is_utxo_indexed: false, + is_synced: false, + has_notify_command: false, + has_message_id: false, + }) + } + + async fn ping_call(&self, _request: PingRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_metrics_call(&self, _request: GetMetricsRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_server_info_call(&self, _request: GetServerInfoRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_sync_status_call(&self, _request: GetSyncStatusRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_current_network_call(&self, _request: GetCurrentNetworkRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn submit_block_call(&self, _request: SubmitBlockRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_block_template_call(&self, _request: GetBlockTemplateRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_peer_addresses_call(&self, _request: GetPeerAddressesRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_sink_call(&self, _request: GetSinkRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_mempool_entry_call(&self, _request: GetMempoolEntryRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_mempool_entries_call(&self, _request: GetMempoolEntriesRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_connected_peer_info_call(&self, _request: GetConnectedPeerInfoRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn add_peer_call(&self, _request: AddPeerRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn submit_transaction_call(&self, _request: SubmitTransactionRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_block_call(&self, _request: GetBlockRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_subnetwork_call(&self, _request: GetSubnetworkRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_virtual_chain_from_block_call( + &self, + _request: GetVirtualChainFromBlockRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_blocks_call(&self, _request: GetBlocksRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_block_count_call(&self, _request: GetBlockCountRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_block_dag_info_call(&self, _request: GetBlockDagInfoRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn resolve_finality_conflict_call( + &self, + _request: ResolveFinalityConflictRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn shutdown_call(&self, _request: ShutdownRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_headers_call(&self, _request: GetHeadersRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_balance_by_address_call(&self, _request: GetBalanceByAddressRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_balances_by_addresses_call( + &self, + _request: GetBalancesByAddressesRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_utxos_by_addresses_call(&self, _request: GetUtxosByAddressesRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_sink_blue_score_call(&self, _request: GetSinkBlueScoreRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn ban_call(&self, _request: BanRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn unban_call(&self, _request: UnbanRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn estimate_network_hashes_per_second_call( + &self, + _request: EstimateNetworkHashesPerSecondRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_mempool_entries_by_addresses_call( + &self, + _request: GetMempoolEntriesByAddressesRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_coin_supply_call(&self, _request: GetCoinSupplyRequest) -> RpcResult { + Err(RpcError::NotImplemented) + } + + async fn get_daa_score_timestamp_estimate_call( + &self, + _request: GetDaaScoreTimestampEstimateRequest, + ) -> RpcResult { + Err(RpcError::NotImplemented) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Notification API + + fn register_new_listener(&self, connection: ChannelConnection) -> ListenerId { + self.core_notifier.register_new_listener(connection) + } + + async fn unregister_listener(&self, id: ListenerId) -> RpcResult<()> { + self.core_notifier.unregister_listener(id)?; + Ok(()) + } + + async fn start_notify(&self, id: ListenerId, scope: Scope) -> RpcResult<()> { + self.core_notifier.try_start_notify(id, scope)?; + Ok(()) + } + + async fn stop_notify(&self, id: ListenerId, scope: Scope) -> RpcResult<()> { + self.core_notifier.try_stop_notify(id, scope)?; + Ok(()) + } +} diff --git a/wallet/core/src/tests/storage.rs b/wallet/core/src/tests/storage.rs new file mode 100644 index 000000000..725731160 --- /dev/null +++ b/wallet/core/src/tests/storage.rs @@ -0,0 +1,31 @@ +use crate::result::Result; +use borsh::{BorshDeserialize, BorshSerialize}; + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct StorageGuard +where + T: Clone + BorshSerialize + BorshDeserialize, +{ + pub before: u32, + pub storable: T, + pub after: u32, +} + +impl StorageGuard +where + T: Clone + BorshSerialize + BorshDeserialize, +{ + pub fn new(storable: &T) -> Self { + Self { before: 0xdeadbeef, storable: storable.clone(), after: 0xbaadf00d } + } + + pub fn validate(&self) -> Result { + let bytes = self.try_to_vec()?; + let transform = Self::try_from_slice(bytes.as_slice())?; + assert_eq!(transform.before, 0xdeadbeef); + assert_eq!(transform.after, 0xbaadf00d); + let transform_bytes = transform.try_to_vec()?; + assert_eq!(bytes, transform_bytes); + Ok(transform.storable) + } +} diff --git a/wallet/core/src/tx/consensus.rs b/wallet/core/src/tx/consensus.rs index 815300ea0..1fcaae019 100644 --- a/wallet/core/src/tx/consensus.rs +++ b/wallet/core/src/tx/consensus.rs @@ -1,3 +1,8 @@ +//! +//! Helpers for obtaining consensus parameters based +//! on the network type or address prefix. +//! + use kash_addresses::{Address, Prefix}; use kash_consensus_core::{ config::params::{Params, DEVNET_PARAMS, MAINNET_PARAMS, SIMNET_PARAMS, TESTNET_PARAMS}, diff --git a/wallet/core/src/tx/fees.rs b/wallet/core/src/tx/fees.rs index 7c25a1caa..96a33763e 100644 --- a/wallet/core/src/tx/fees.rs +++ b/wallet/core/src/tx/fees.rs @@ -1,4 +1,10 @@ +//! +//! Primitives for declaring transaction fees. +//! + use crate::result::Result; +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; use workflow_wasm::prelude::*; @@ -24,7 +30,7 @@ use workflow_wasm::prelude::*; /// 1. Use estimation to check that the funds are sufficient. /// 2. Check balance and ensure that there is a sufficient amount of funds. /// -#[derive(Debug, Clone)] +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub enum Fees { /// Fee management disabled (sweep transactions, pays all fees) None, diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 71d564b07..60ec59827 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -65,7 +65,7 @@ use kash_consensus_core::asset_type::AssetType::KSH; use kash_consensus_core::constants::UNACCEPTED_DAA_SCORE; use kash_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kash_consensus_core::tx as cctx; -use kash_consensus_core::tx::{Transaction, TransactionInput, TransactionAction, TransactionOutpoint, TransactionOutput}; +use kash_consensus_core::tx::{Transaction, TransactionAction, TransactionInput, TransactionOutpoint, TransactionOutput}; use kash_consensus_wasm::UtxoEntry; use kash_txscript::pay_to_address_script; use std::collections::VecDeque; @@ -209,8 +209,10 @@ struct Inner { mass_calculator: MassCalculator, network_type: NetworkType, - // Utxo Context - utxo_context: Option, + // Source Utxo Context (Used for source UtxoEntry aggregation) + source_utxo_context: Option, + // Destination Utxo Context (Used only during transfer transactions) + destination_utxo_context: Option, // Event multiplexer multiplexer: Option>>, // typically a number of keys required to sign the transaction @@ -257,7 +259,7 @@ impl Generator { network_type, multiplexer, utxo_iterator, - utxo_context, + source_utxo_context: utxo_context, sig_op_count, minimum_signatures, change_address, @@ -265,6 +267,7 @@ impl Generator { final_transaction_priority_fee, final_transaction_destination, final_transaction_payload, + destination_utxo_context, } = settings; let mass_calculator = MassCalculator::new(&network_type.into()); @@ -304,9 +307,6 @@ impl Generator { return Err(Error::GeneratorChangeAddressNetworkTypeMismatch); } - // if final_transaction_amount.is_none() && !matches!(final_transaction_priority_fee, Fees::None) { - // } - let context = Mutex::new(Context { utxo_source_iterator: utxo_iterator, number_of_transactions: 0, @@ -338,7 +338,7 @@ impl Generator { signer, abortable: abortable.cloned(), mass_calculator, - utxo_context, + source_utxo_context: utxo_context, sig_op_count, minimum_signatures, change_address, @@ -351,6 +351,7 @@ impl Generator { final_transaction_outputs_mass, final_transaction_payload, final_transaction_payload_mass, + destination_utxo_context, }; Ok(Self { inner: Arc::new(inner) }) } @@ -360,8 +361,13 @@ impl Generator { } /// The underlying [`UtxoContext`] (if available). - pub fn utxo_context(&self) -> &Option { - &self.inner.utxo_context + pub fn source_utxo_context(&self) -> &Option { + &self.inner.source_utxo_context + } + + /// Signifies that the transaction is a transfer between accounts + pub fn destination_utxo_context(&self) -> &Option { + &self.inner.destination_utxo_context } /// Core [`Multiplexer`] (if available) @@ -689,6 +695,9 @@ impl Generator { self.inner.final_transaction_payload.clone(), ); + // for internal testing, please keep commented out + // script_engine_validator(self, &tx, &utxo_entry_references, &addresses); + context.final_transaction_id = Some(tx.id()); context.number_of_transactions += 1; @@ -797,3 +806,50 @@ impl Generator { } } } + +/* +// +// this function is used for short-circuiting the transaction generation process +// against the script engine. Until additional unit tests are developed, please +// keep this here for reference. +// +fn script_engine_validator(generator : &Generator, tx: &Transaction, utxo_entry_references: &Vec, addresses : &HashSet
) -> Result<()> { + + use kash_consensus_core::tx::{PopulatedTransaction,VerifiableTransaction,MutableTransaction}; + use kash_consensus_core::hashing::sighash::SigHashReusedValues; + use kash_consensus_core::errors::tx::TxRuleError; + use kash_txscript::{SigCacheKey,TxScriptEngine,caches::Cache}; + + let sig_cache = Cache::::new(1000); + let entries = utxo_entry_references + .iter() + .map(|utxo_entry_reference|utxo_entry_reference.utxo.entry.clone()).collect::>(); + + let mtx = MutableTransaction::with_entries(tx.clone(),entries.clone()); + let tx_addresses = addresses.iter().cloned().collect::>(); + + let signer = generator.signer().as_ref().expect("no signer in tx generator"); + let signed_tx = signer.try_sign(mtx, &tx_addresses)?; + + let tx = PopulatedTransaction::new(signed_tx.as_ref(),entries); + let mut reused_values = SigHashReusedValues::new(); + for (i, (input, entry)) in tx.populated_inputs().enumerate() { + match TxScriptEngine::from_transaction_input(&tx, input, i, entry, &mut reused_values, &sig_cache) + .map_err(TxRuleError::SignatureInvalid).map_err(|e|e.to_string()) { + Ok(mut engine) => { + match engine.execute().map_err(TxRuleError::SignatureInvalid).map_err(|e|e.to_string()) { + Ok(_) => { }, + Err(err) => { + println!("TxScriptEngine::execute error: {:?}", err); + } + } + }, + Err(err) => { + println!("TxScriptEngine::from_transaction_input error: {:?}", err); + } + }; + } + + Ok(()) +} +*/ diff --git a/wallet/core/src/tx/generator/mod.rs b/wallet/core/src/tx/generator/mod.rs index d1db59e7d..eacdb1edc 100644 --- a/wallet/core/src/tx/generator/mod.rs +++ b/wallet/core/src/tx/generator/mod.rs @@ -1,3 +1,8 @@ +//! +//! Transaction generator implementation used to construct +//! Kash transactions. +//! + #[allow(clippy::module_inception)] pub mod generator; pub mod iterator; @@ -16,4 +21,4 @@ pub use stream::*; pub use summary::*; #[cfg(test)] -mod test; +pub mod test; diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index 024616b73..d17b846bf 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -1,30 +1,30 @@ +//! +//! Pending transaction encapsulating a +//! transaction generated by the [`Generator`]. +//! + +use crate::imports::*; use crate::result::Result; use crate::rpc::DynRpcApi; use crate::tx::{DataKind, Generator}; -use crate::utxo::UtxoEntryReference; -use kash_addresses::Address; -use kash_consensus_core::network::NetworkType; +use crate::utxo::{UtxoContext, UtxoEntryReference}; use kash_consensus_core::sign::sign_with_multiple_v2; use kash_consensus_core::tx::{SignableTransaction, Transaction, TransactionId}; use kash_rpc_core::{RpcTransaction, RpcTransactionId}; -use std::sync::Mutex; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; -use workflow_log::log_info; pub(crate) struct PendingTransactionInner { /// Generator that produced the transaction pub(crate) generator: Generator, /// UtxoEntryReferences of the pending transaction - pub(crate) utxo_entries: Vec, + pub(crate) utxo_entries: AHashSet, + /// Transaction Id (cached in pending to avoid mutex lock) + pub(crate) id: TransactionId, /// Signable transaction (actual transaction that will be signed and sent) pub(crate) signable_tx: Mutex, /// UTXO addresses used by this transaction pub(crate) addresses: Vec
, /// Whether the transaction has been committed to the mempool via RPC - pub(crate) is_committed: AtomicBool, + pub(crate) is_submitted: AtomicBool, /// Payment value of the transaction (transaction destination amount) pub(crate) payment_value: Option, /// Change value of the transaction (transaction change amount) @@ -81,15 +81,18 @@ impl PendingTransaction { fees: u64, kind: DataKind, ) -> Result { + let id = transaction.id(); let entries = utxo_entries.iter().map(|e| e.utxo.entry.clone()).collect::>(); let signable_tx = Mutex::new(SignableTransaction::with_entries(transaction, entries)); + let utxo_entries = utxo_entries.into_iter().collect::>(); Ok(Self { inner: Arc::new(PendingTransactionInner { generator: generator.clone(), + id, signable_tx, utxo_entries, addresses, - is_committed: AtomicBool::new(false), + is_submitted: AtomicBool::new(false), payment_value, change_output_value, aggregate_input_value, @@ -102,7 +105,19 @@ impl PendingTransaction { } pub fn id(&self) -> TransactionId { - self.inner.signable_tx.lock().unwrap().id() + self.inner.id + } + + pub fn generator(&self) -> &Generator { + &self.inner.generator + } + + pub fn source_utxo_context(&self) -> &Option { + self.inner.generator.source_utxo_context() + } + + pub fn destination_utxo_context(&self) -> &Option { + self.inner.generator.destination_utxo_context() } /// Addresses used by the pending transaction @@ -110,8 +125,8 @@ impl PendingTransaction { &self.inner.addresses } - /// Get UTXO entries [`Vec`] of the pending transaction - pub fn utxo_entries(&self) -> &Vec { + /// Get UTXO entries [`AHashSet`] of the pending transaction + pub fn utxo_entries(&self) -> &AHashSet { &self.inner.utxo_entries } @@ -119,11 +134,11 @@ impl PendingTransaction { self.inner.fees } - pub fn input_aggregate_value(&self) -> u64 { + pub fn aggregate_input_value(&self) -> u64 { self.inner.aggregate_input_value } - pub fn output_aggregate_value(&self) -> u64 { + pub fn aggregate_output_value(&self) -> u64 { self.inner.aggregate_output_value } @@ -143,17 +158,6 @@ impl PendingTransaction { !self.inner.kind.is_final() } - async fn commit(&self) -> Result<()> { - self.inner.is_committed.load(Ordering::SeqCst).then(|| { - panic!("PendingTransaction::commit() called multiple times"); - }); - self.inner.is_committed.store(true, Ordering::SeqCst); - if let Some(utxo_context) = self.inner.generator.utxo_context() { - utxo_context.handle_outgoing_transaction(self).await?; - } - Ok(()) - } - pub fn network_type(&self) -> NetworkType { self.inner.generator.network_type() } @@ -168,9 +172,39 @@ impl PendingTransaction { /// Submit the transaction on the supplied rpc pub async fn try_submit(&self, rpc: &Arc) -> Result { - self.commit().await?; // commit transactions only if we are submitting + // sanity check to prevent multiple invocations (for API use) + self.inner.is_submitted.load(Ordering::SeqCst).then(|| { + panic!("PendingTransaction::try_submit() called multiple times"); + }); + self.inner.is_submitted.store(true, Ordering::SeqCst); + let rpc_transaction: RpcTransaction = self.rpc_transaction(); - Ok(rpc.submit_transaction(rpc_transaction, false).await?) + + // if we are running under UtxoProcessor + if let Some(utxo_context) = self.inner.generator.source_utxo_context() { + // lock UtxoProcessor notification ingest + let _lock = utxo_context.processor().notification_lock().await; + + // register pending UTXOs with UtxoProcessor + utxo_context.register_outgoing_transaction(self).await?; + + // try to submit transaction + match rpc.submit_transaction(rpc_transaction, false).await { + Ok(id) => { + // on successful submit, create a notification + utxo_context.notify_outgoing_transaction(self).await?; + Ok(id) + } + Err(error) => { + // in case of failure, remove transaction UTXOs from the consumed list + utxo_context.cancel_outgoing_transaction(self).await?; + Err(error.into()) + } + } + } else { + // No UtxoProcessor present (API etc) + Ok(rpc.submit_transaction(rpc_transaction, false).await?) + } } pub async fn log(&self) -> Result<()> { @@ -187,7 +221,7 @@ impl PendingTransaction { pub fn try_sign_with_keys(&self, privkeys: Vec<[u8; 32]>) -> Result<()> { let mutable_tx = self.inner.signable_tx.lock()?.clone(); - let signed_tx = sign_with_multiple_v2(mutable_tx, privkeys); + let signed_tx = sign_with_multiple_v2(mutable_tx, privkeys).fully_signed()?; *self.inner.signable_tx.lock().unwrap() = signed_tx; Ok(()) } diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index 0dc473b7e..f5e03d76d 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -1,10 +1,14 @@ +//! +//! Transaction [`GeneratorSettings`] used when +//! constructing and instance of the [`Generator`](crate::tx::Generator). +//! + +use crate::events::Events; +use crate::imports::*; use crate::result::Result; -use crate::runtime::Account; use crate::tx::{Fees, PaymentDestination}; use crate::utxo::{UtxoContext, UtxoEntryReference, UtxoIterator}; -use crate::Events; use kash_addresses::Address; -use kash_consensus_core::network::NetworkType; use kash_consensus_core::tx::TransactionAction; use std::sync::Arc; use workflow_core::channel::Multiplexer; @@ -17,7 +21,7 @@ pub struct GeneratorSettings { // Utxo iterator pub utxo_iterator: Box + Send + Sync + 'static>, // Utxo Context - pub utxo_context: Option, + pub source_utxo_context: Option, // typically a number of keys required to sign the transaction pub sig_op_count: u8, // number of minimum signatures required to sign the transaction @@ -32,6 +36,8 @@ pub struct GeneratorSettings { pub final_transaction_destination: PaymentDestination, // payload pub final_transaction_payload: Option>, + // transaction is a transfer between accounts + pub destination_utxo_context: Option, } impl GeneratorSettings { @@ -57,11 +63,12 @@ impl GeneratorSettings { minimum_signatures, change_address, utxo_iterator: Box::new(utxo_iterator), - utxo_context: Some(account.utxo_context().clone()), + source_utxo_context: Some(account.utxo_context().clone()), final_transaction_action, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, + destination_utxo_context: None, }; Ok(settings) @@ -88,11 +95,12 @@ impl GeneratorSettings { minimum_signatures, change_address, utxo_iterator: Box::new(utxo_iterator), - utxo_context: Some(utxo_context), + source_utxo_context: Some(utxo_context), final_transaction_action, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, + destination_utxo_context: None, }; Ok(settings) @@ -118,13 +126,19 @@ impl GeneratorSettings { minimum_signatures, change_address, utxo_iterator: Box::new(utxo_iterator), - utxo_context: None, + source_utxo_context: None, final_transaction_action, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, + destination_utxo_context: None, }; Ok(settings) } + + pub fn utxo_context_transfer(mut self, destination_utxo_context: &UtxoContext) -> Self { + self.destination_utxo_context = Some(destination_utxo_context.clone()); + self + } } diff --git a/wallet/core/src/tx/generator/signer.rs b/wallet/core/src/tx/generator/signer.rs index e2679add4..eb3ebe0a2 100644 --- a/wallet/core/src/tx/generator/signer.rs +++ b/wallet/core/src/tx/generator/signer.rs @@ -1,15 +1,13 @@ -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; +//! +//! Transaction signing trait and generic signer implementations.. +//! -use kash_addresses::Address; +use crate::imports::*; +use crate::result::Result; +use crate::secret::Secret; use kash_bip32::PrivateKey; use kash_consensus_core::{sign::sign_with_multiple_v2, tx::SignableTransaction}; -use crate::result::Result; -use crate::{runtime::Account, secret::Secret, storage::PrvKeyData}; - pub trait SignerT: Send + Sync + 'static { fn try_sign(&self, transaction: SignableTransaction, addresses: &[Address]) -> Result; } @@ -18,8 +16,7 @@ struct Inner { keydata: PrvKeyData, account: Arc, payment_secret: Option, - // keys : Mutex>, - keys: Mutex>, + keys: Mutex>, } pub struct Signer { @@ -28,15 +25,15 @@ pub struct Signer { impl Signer { pub fn new(account: Arc, keydata: PrvKeyData, payment_secret: Option) -> Self { - Self { inner: Arc::new(Inner { keydata, account, payment_secret, keys: Mutex::new(HashMap::new()) }) } + Self { inner: Arc::new(Inner { keydata, account, payment_secret, keys: Mutex::new(AHashMap::new()) }) } } fn ingest(&self, addresses: &[Address]) -> Result<()> { let mut keys = self.inner.keys.lock().unwrap(); + // skip address that are already present in the key map let addresses = addresses.iter().filter(|a| !keys.contains_key(a)).collect::>(); if !addresses.is_empty() { - let account = self.inner.account.clone().as_derivation_capable().expect("expecting derivation capable"); - + let account = self.inner.account.clone().as_derivation_capable().expect("expecting derivation capable account"); let (receive, change) = account.derivation().addresses_indexes(&addresses)?; let private_keys = account.create_private_keys(&self.inner.keydata, &self.inner.payment_secret, &receive, &change)?; for (address, private_key) in private_keys { @@ -54,7 +51,8 @@ impl SignerT for Signer { let keys = self.inner.keys.lock().unwrap(); let keys_for_signing = addresses.iter().map(|address| *keys.get(address).unwrap()).collect::>(); - Ok(sign_with_multiple_v2(mutable_tx, keys_for_signing)) + // TODO - refactor for multisig + Ok(sign_with_multiple_v2(mutable_tx, keys_for_signing).fully_signed()?) } } @@ -78,6 +76,7 @@ impl KeydataSigner { impl SignerT for KeydataSigner { fn try_sign(&self, mutable_tx: SignableTransaction, addresses: &[Address]) -> Result { let keys_for_signing = addresses.iter().map(|address| *self.inner.keys.get(address).unwrap()).collect::>(); - Ok(sign_with_multiple_v2(mutable_tx, keys_for_signing)) + // TODO - refactor for multisig + Ok(sign_with_multiple_v2(mutable_tx, keys_for_signing).fully_signed()?) } } diff --git a/wallet/core/src/tx/generator/summary.rs b/wallet/core/src/tx/generator/summary.rs index 4c097c712..5b7394209 100644 --- a/wallet/core/src/tx/generator/summary.rs +++ b/wallet/core/src/tx/generator/summary.rs @@ -1,9 +1,18 @@ +//! +//! [`GeneratorSummary`] is a struct that holds the summary +//! of a [`Generator`](crate::tx::Generator) output after transaction generation. +//! The summary includes total amount, total fees consumed, +//! total UTXOs consumed etc. +//! + use crate::utils::*; +use borsh::{BorshDeserialize, BorshSerialize}; use kash_consensus_core::network::NetworkType; use kash_consensus_core::tx::TransactionId; +use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Debug, Clone)] +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct GeneratorSummary { pub network_type: NetworkType, pub aggregated_utxos: usize, diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index d418545d6..5d8faba3e 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -18,7 +18,7 @@ use super::*; const LOGS: bool = false; #[derive(Clone)] -struct Sompi(u64); +pub(crate) struct Sompi(u64); #[derive(Clone)] struct Kash(f64); @@ -127,7 +127,7 @@ struct Accumulator { list: Vec, } -struct Expected { +pub(crate) struct Expected { is_final: bool, input_count: usize, aggregate_input_value: Sompi, @@ -149,7 +149,6 @@ fn expect(pt: &PendingTransaction, expected: &Expected) { assert_eq!(transaction_mass, pt.inner.mass, "pending transaction mass does not match calculated mass"); - // let (total_output_value_with_fees, priority_fees) = match expected.priority_fees { FeesExpected::Sender(priority_fees) => { let total_fees_expected = priority_fees + relay_fees; @@ -198,7 +197,7 @@ fn expect(pt: &PendingTransaction, expected: &Expected) { assert_eq!(tx.outputs.len(), expected.output_count, "output count"); } -struct Harness { +pub(crate) struct Harness { generator: Generator, accumulator: RefCell, } @@ -252,7 +251,13 @@ impl Harness { } } -fn generator(network_type: NetworkType, head: &[f64], tail: &[f64], fees: Fees, outputs: &[(F, T)]) -> Result +pub(crate) fn generator( + network_type: NetworkType, + head: &[f64], + tail: &[f64], + fees: Fees, + outputs: &[(F, T)], +) -> Result where T: Into + Clone, F: FnOnce(NetworkType) -> Address + Clone, @@ -275,7 +280,7 @@ where ) } -fn make_generator( +pub(crate) fn make_generator( network_type: NetworkType, head: &[f64], tail: &[f64], @@ -290,12 +295,13 @@ where let mut values = head.to_vec(); values.extend(tail); - let utxo_entries: Vec = values.into_iter().map(kash_to_sompi).map(UtxoEntryReference::fake).collect(); + let utxo_entries: Vec = values.into_iter().map(kash_to_sompi).map(UtxoEntryReference::simulated).collect(); let multiplexer = None; let sig_op_count = 0; let minimum_signatures = 0; let utxo_iterator: Box + Send + Sync + 'static> = Box::new(utxo_entries.into_iter()); - let utxo_context = None; + let source_utxo_context = None; + let destination_utxo_context = None; let final_priority_fee = fees; let final_transaction_payload = None; let change_address = change_address(network_type); @@ -308,7 +314,8 @@ where change_address, final_transaction_action, utxo_iterator, - utxo_context, + source_utxo_context, + destination_utxo_context, final_transaction_priority_fee: final_priority_fee, final_transaction_destination, final_transaction_payload, @@ -317,7 +324,7 @@ where Generator::try_new(settings, None, None) } -fn change_address(network_type: NetworkType) -> Address { +pub(crate) fn change_address(network_type: NetworkType) -> Address { match network_type { NetworkType::Mainnet => Address::try_from("kash:qpauqsvk7yf9unexwmxsnmg547mhyga37csh0kj53q6xxgl24ydxjh3y20yhf").unwrap(), NetworkType::Testnet => Address::try_from("kashtest:qqz22l98sf8jun72rwh5rqe2tm8lhwtdxdmynrz4ypwak427qed5j0jung7ef").unwrap(), @@ -325,7 +332,7 @@ fn change_address(network_type: NetworkType) -> Address { } } -fn output_address(network_type: NetworkType) -> Address { +pub(crate) fn output_address(network_type: NetworkType) -> Address { match network_type { NetworkType::Mainnet => Address::try_from("kash:qrd9efkvg3pg34sgp6ztwyv3r569qlc43wa5w8nfs302532dzj47k59f59rgq").unwrap(), NetworkType::Testnet => Address::try_from("kashtest:qqrewmx4gpuekvk8grenkvj2hp7xt0c35rxgq383f6gy223c4ud5s8rk2c6jk").unwrap(), @@ -443,7 +450,6 @@ fn test_generator_inputs_100_outputs_1_fees_exclude() -> Result<()> { output_count: 2, priority_fees: FeesExpected::sender_pays(Kash(5.0)), }); - // .finalize(); Ok(()) } diff --git a/wallet/core/src/tx/mass.rs b/wallet/core/src/tx/mass.rs index 57b8849ce..9b3fe2c31 100644 --- a/wallet/core/src/tx/mass.rs +++ b/wallet/core/src/tx/mass.rs @@ -1,5 +1,8 @@ -use kash_consensus_core::tx::{Transaction, TransactionInput, TransactionOutput, SCRIPT_VECTOR_SIZE}; +//! +//! Transaction mass calculator. +//! +use kash_consensus_core::tx::{Transaction, TransactionInput, TransactionOutput, SCRIPT_VECTOR_SIZE}; use kash_consensus_core::{config::params::Params, constants::*, subnets::SUBNETWORK_ID_SIZE}; use kash_hashes::HASH_SIZE; @@ -43,7 +46,7 @@ pub fn calc_minimum_required_transaction_relay_fee(mass: u64) -> u64 { /// if the cost to the network to spend coins is more than 1/3 of the minimum /// transaction relay fee, it is considered dust. /// -/// It is exposed by [MiningManager] for use by transaction generators and wallets. +/// It is exposed by `MiningManager` for use by transaction generators and wallets. pub fn is_transaction_output_dust(transaction_output: &TransactionOutput) -> bool { // Unspendable outputs are considered dust. // diff --git a/wallet/core/src/tx/mod.rs b/wallet/core/src/tx/mod.rs index e306e61a9..baf3b29ac 100644 --- a/wallet/core/src/tx/mod.rs +++ b/wallet/core/src/tx/mod.rs @@ -1,3 +1,7 @@ +//! +//! Transaction generation and processing primitives. +//! + pub mod consensus; pub mod fees; pub mod generator; diff --git a/wallet/core/src/tx/payment.rs b/wallet/core/src/tx/payment.rs index 6517ea423..df50250cd 100644 --- a/wallet/core/src/tx/payment.rs +++ b/wallet/core/src/tx/payment.rs @@ -1,8 +1,13 @@ +//! +//! Primitives for declaring transaction payment destinations. +//! + use crate::imports::*; use kash_consensus_core::asset_type::AssetType; use kash_consensus_wasm::{TransactionOutput, TransactionOutputInner}; use kash_txscript::pay_to_address_script; +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub enum PaymentDestination { Change, PaymentOutputs(PaymentOutputs), @@ -17,7 +22,7 @@ impl PaymentDestination { } } -#[derive(Debug)] +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[wasm_bindgen(inspectable)] pub struct PaymentOutput { #[wasm_bindgen(getter_with_clone)] @@ -74,7 +79,7 @@ impl From for PaymentDestination { } } -#[derive(Debug)] +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[wasm_bindgen] pub struct PaymentOutputs { #[wasm_bindgen(skip)] @@ -106,7 +111,7 @@ impl From for Vec { #[wasm_bindgen] impl PaymentOutputs { #[wasm_bindgen(constructor)] - pub fn constructor(output_array: JsValue) -> crate::Result { + pub fn constructor(output_array: JsValue) -> crate::result::Result { let mut outputs = vec![]; let iterator = js_sys::try_iter(&output_array)?.ok_or("need to pass iterable JS values!")?; for x in iterator { diff --git a/wallet/core/src/types.rs b/wallet/core/src/types.rs new file mode 100644 index 000000000..488406eb6 --- /dev/null +++ b/wallet/core/src/types.rs @@ -0,0 +1,9 @@ +//! +//! Type aliases used by the wallet framework. +//! + +use std::sync::Arc; + +pub type ExtendedPublicKeySecp256k1 = kash_bip32::ExtendedPublicKey; + +pub type ExtendedPublicKeys = Arc>; diff --git a/wallet/core/src/utils.rs b/wallet/core/src/utils.rs index 45b0728ec..15dec5c54 100644 --- a/wallet/core/src/utils.rs +++ b/wallet/core/src/utils.rs @@ -1,8 +1,12 @@ +//! +//! Kash value formatting and parsing utilities. +//! + use crate::result::Result; use kash_addresses::Address; use kash_consensus_core::constants::*; use kash_consensus_core::network::NetworkType; -use separator::Separatable; +use separator::{separated_float, separated_int, separated_uint_with_output, Separatable}; use workflow_log::style; pub fn try_kash_str_to_sompi>(s: S) -> Result> { @@ -41,6 +45,11 @@ pub fn sompi_to_kash_string(sompi: u64) -> String { sompi_to_kash(sompi).separated_string() } +#[inline] +pub fn sompi_to_kash_string_with_trailing_zeroes(sompi: u64) -> String { + separated_float!(format!("{:.8}", sompi_to_kash(sompi))) +} + pub fn kash_suffix(network_type: &NetworkType) -> &'static str { match network_type { NetworkType::Mainnet => "KAS", @@ -52,7 +61,14 @@ pub fn kash_suffix(network_type: &NetworkType) -> &'static str { #[inline] pub fn sompi_to_kash_string_with_suffix(sompi: u64, network_type: &NetworkType) -> String { - let kas = sompi_to_kash(sompi).separated_string(); + let kas = sompi_to_kash_string(sompi); + let suffix = kash_suffix(network_type); + format!("{kas} {suffix}") +} + +#[inline] +pub fn sompi_to_kash_string_with_trailing_zeroes_and_suffix(sompi: u64, network_type: &NetworkType) -> String { + let kas = sompi_to_kash_string_with_trailing_zeroes(sompi); let suffix = kash_suffix(network_type); format!("{kas} {suffix}") } @@ -81,7 +97,14 @@ fn str_to_sompi(amount: &str) -> Result { let integer = amount[..dot_idx].parse::()? * SOMPI_PER_KASH; let decimal = &amount[dot_idx + 1..]; let decimal_len = decimal.len(); - let decimal = - if decimal_len <= 8 { decimal.parse::()? * 10u64.pow(8 - decimal_len as u32) } else { decimal[..8].parse::()? }; + let decimal = if decimal_len == 0 { + 0 + } else if decimal_len <= 8 { + decimal.parse::()? * 10u64.pow(8 - decimal_len as u32) + } else { + // TODO - discuss how to handle values longer than 8 decimal places + // (reject, truncate, ceil(), etc.) + decimal[..8].parse::()? + }; Ok(integer + decimal) } diff --git a/wallet/core/src/runtime/balance.rs b/wallet/core/src/utxo/balance.rs similarity index 73% rename from wallet/core/src/runtime/balance.rs rename to wallet/core/src/utxo/balance.rs index 1ad1ad156..00a519101 100644 --- a/wallet/core/src/runtime/balance.rs +++ b/wallet/core/src/utxo/balance.rs @@ -1,5 +1,8 @@ +//! +//! Account balances. +//! + use crate::imports::*; -use kash_consensus_core::network::NetworkType; pub enum DeltaStyle { Mature, @@ -41,16 +44,39 @@ impl From for Delta { } #[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Balance { pub mature: u64, pub pending: u64, + pub outgoing: u64, + pub mature_utxo_count: usize, + pub pending_utxo_count: usize, + pub stasis_utxo_count: usize, + #[serde(skip)] mature_delta: Delta, + #[serde(skip)] pending_delta: Delta, } impl Balance { - pub fn new(mature: u64, pending: u64) -> Self { - Self { mature, pending, mature_delta: Delta::default(), pending_delta: Delta::default() } + pub fn new( + mature: u64, + pending: u64, + outgoing: u64, + mature_utxo_count: usize, + pending_utxo_count: usize, + stasis_utxo_count: usize, + ) -> Self { + Self { + mature, + pending, + outgoing, + mature_delta: Delta::default(), + pending_delta: Delta::default(), + mature_utxo_count, + pending_utxo_count, + stasis_utxo_count, + } } pub fn is_empty(&self) -> bool { @@ -72,12 +98,18 @@ impl Balance { pub struct AtomicBalance { pub mature: AtomicU64, pub pending: AtomicU64, + pub mature_utxos: AtomicUsize, + pub pending_utxos: AtomicUsize, + pub stasis_utxos: AtomicUsize, } impl AtomicBalance { pub fn add(&self, balance: Balance) { self.mature.fetch_add(balance.mature, Ordering::SeqCst); self.pending.fetch_add(balance.pending, Ordering::SeqCst); + self.mature_utxos.fetch_add(balance.mature_utxo_count, Ordering::SeqCst); + self.pending_utxos.fetch_add(balance.pending_utxo_count, Ordering::SeqCst); + self.stasis_utxos.fetch_add(balance.stasis_utxo_count, Ordering::SeqCst); } } @@ -86,6 +118,10 @@ impl From for Balance { Self { mature: atomic_balance.mature.load(Ordering::SeqCst), pending: atomic_balance.pending.load(Ordering::SeqCst), + outgoing: 0, + mature_utxo_count: atomic_balance.mature_utxos.load(Ordering::SeqCst), + pending_utxo_count: atomic_balance.pending_utxos.load(Ordering::SeqCst), + stasis_utxo_count: atomic_balance.stasis_utxos.load(Ordering::SeqCst), mature_delta: Delta::default(), pending_delta: Delta::default(), } diff --git a/wallet/core/src/utxo/binding.rs b/wallet/core/src/utxo/binding.rs index 0a6e7d86a..42c3cb168 100644 --- a/wallet/core/src/utxo/binding.rs +++ b/wallet/core/src/utxo/binding.rs @@ -1,6 +1,10 @@ +//! +//! Implementation of [`UtxoContextBinding`] which allows binding of +//! [`UtxoContext`] to [`Account`] or custom developer-defined ids. +//! + use crate::imports::*; use crate::utxo::UtxoContextId; -use runtime::AccountId; #[derive(Clone)] pub enum UtxoContextBinding { diff --git a/wallet/core/src/utxo/context.rs b/wallet/core/src/utxo/context.rs index 771508cc5..782760ddd 100644 --- a/wallet/core/src/utxo/context.rs +++ b/wallet/core/src/utxo/context.rs @@ -1,23 +1,35 @@ +//! +//! Implementation of the [`UtxoContext`] which is a runtime +//! primitive responsible for monitoring multiple addresses, +//! generation of address-related events and balance tracking. +//! + use crate::encryption::sha256_hash; use crate::events::Events; use crate::imports::*; use crate::result::Result; -use crate::runtime::{AccountId, Balance}; -use crate::storage::{TransactionRecord, TransactionType}; +use crate::storage::TransactionRecord; use crate::tx::PendingTransaction; use crate::utxo::{ - PendingUtxoEntryReference, UtxoContextBinding, UtxoEntryId, UtxoEntryReference, UtxoEntryReferenceExtension, UtxoProcessor, + Maturity, OutgoingTransaction, PendingUtxoEntryReference, UtxoContextBinding, UtxoEntryId, UtxoEntryReference, + UtxoEntryReferenceExtension, UtxoProcessor, }; use kash_hashes::Hash; use sorted_insert::SortedInsertBinaryByKey; +// If enabled, upon submission of an outgoing transaction, +// change UTXOs are immediately promoted to the mature set. +// Otherwise they are treated as regular incoming transactions +// and require a maturity period. +// const SKIP_CHANGE_UTXO_PROMOTION: bool = true; + static PROCESSOR_ID_SEQUENCER: AtomicU64 = AtomicU64::new(0); fn next_processor_id() -> Hash { let id = PROCESSOR_ID_SEQUENCER.fetch_add(1, Ordering::SeqCst); Hash::from_slice(sha256_hash(id.to_le_bytes().as_slice()).as_ref()) } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct UtxoContextId(pub(crate) Hash); impl Default for UtxoContextId { @@ -38,6 +50,12 @@ impl From<&AccountId> for UtxoContextId { } } +impl From for AccountId { + fn from(id: UtxoContextId) -> Self { + AccountId(id.0) + } +} + impl UtxoContextId { pub fn new(id: Hash) -> Self { UtxoContextId(id) @@ -55,61 +73,47 @@ impl ToHex for UtxoContextId { } } -pub struct Consumed { - entry: UtxoEntryReference, - timeout: Instant, -} - -impl From<(UtxoEntryReference, &Instant)> for Consumed { - fn from((entry, timeout): (UtxoEntryReference, &Instant)) -> Self { - Self { entry, timeout: *timeout } - } -} - -impl From<(&UtxoEntryReference, &Instant)> for Consumed { - fn from((entry, timeout): (&UtxoEntryReference, &Instant)) -> Self { - Self { entry: entry.clone(), timeout: *timeout } +impl std::fmt::Display for UtxoContextId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } pub enum UtxoEntryVariant { Mature(UtxoEntryReference), Pending(UtxoEntryReference), - Consumed(UtxoEntryReference), + Stasis(UtxoEntryReference), } pub struct Context { /// Mature (Confirmed) UTXOs pub(crate) mature: Vec, /// UTXOs that are pending confirmation - pub(crate) pending: HashMap, - /// UTXOs consumed by recently created outgoing transactions - pub(crate) consumed: HashMap, - /// All UTXOs in posession of this context instance - pub(crate) map: HashMap, + pub(crate) pending: AHashMap, + /// UTXOs that are in stasis (freshly minted coinbase transactions only) + pub(crate) stasis: AHashMap, + /// All UTXOs in possession of this context instance + pub(crate) map: AHashMap, /// Outgoing transactions that have not yet been confirmed. /// Confirmation occurs when the transaction UTXOs are /// removed from the context by the UTXO change notification. - outgoing: HashMap, + pub(crate) outgoing: AHashMap, /// Total balance of all UTXOs in this context (mature, pending) balance: Option, /// Addresses monitored by this UTXO context addresses: Arc>>, - /// Timeout for UTXO recovery - recovery_period: Duration, } impl Default for Context { fn default() -> Self { Self { mature: vec![], - pending: HashMap::default(), - consumed: HashMap::default(), - map: HashMap::default(), - outgoing: HashMap::default(), + pending: AHashMap::default(), + stasis: AHashMap::default(), + map: AHashMap::default(), + outgoing: AHashMap::default(), balance: None, addresses: Arc::new(DashSet::new()), - recovery_period: Duration::from_secs(crate::utxo::UTXO_RECOVERY_PERIOD_SECONDS.load(Ordering::Relaxed)), } } } @@ -122,7 +126,7 @@ impl Context { pub fn clear(&mut self) { self.map.clear(); self.mature.clear(); - self.consumed.clear(); + self.stasis.clear(); self.pending.clear(); self.outgoing.clear(); self.addresses.clear(); @@ -148,7 +152,20 @@ impl Inner { } } -/// a collection of UTXO entries +/// +/// UtxoContext is a data structure responsible for monitoring multiple addresses +/// for transactions. It scans the address set for existing UtxoEntry records, then +/// monitors for transaction-related events in order to maintain a consistent view +/// on that UtxoEntry set throughout its connection lifetime. +/// +/// UtxoContext typically represents a single wallet account, but can monitor any set +/// of addresses. When receiving transaction events, UtxoContext detects types of these +/// events and emits corresponding notifications on the UtxoProcessor event multiplexer. +/// +/// In addition to standard monitoring, UtxoContext works in conjunction with the +/// TransactionGenerator to track outgoing transactions in an effort to segregate +/// different types of UtxoEntry updates (regular incoming vs. change). +/// #[derive(Clone)] pub struct UtxoContext { inner: Arc, @@ -217,61 +234,62 @@ impl UtxoContext { } pub async fn update_balance(&self) -> Result { - let (balance, mature_utxo_size, pending_utxo_size) = { + let balance = { let previous_balance = self.balance(); let mut balance = self.calculate_balance().await; balance.delta(&previous_balance); - let mut context = self.context(); context.balance.replace(balance.clone()); - let mature_utxo_size = context.mature.len(); - let pending_utxo_size = context.pending.len(); - - (balance, mature_utxo_size, pending_utxo_size) + balance }; - - self.processor() - .notify(Events::Balance { balance: Some(balance.clone()), id: self.id(), mature_utxo_size, pending_utxo_size }) - .await?; + self.processor().notify(Events::Balance { balance: Some(balance.clone()), id: self.id() }).await?; Ok(balance) } /// Process pending transaction. Remove mature UTXO entries and add them to the consumed set. /// Produces a notification on the even multiplexer. - pub(crate) async fn handle_outgoing_transaction(&self, pending_tx: &PendingTransaction) -> Result<()> { + pub(crate) async fn register_outgoing_transaction(&self, pending_tx: &PendingTransaction) -> Result<()> { { + let current_daa_score = + self.processor().current_daa_score().ok_or(Error::MissingDaaScore("register_outgoing_transaction()"))?; + let mut context = self.context(); let pending_utxo_entries = pending_tx.utxo_entries(); context.mature.retain(|entry| !pending_utxo_entries.contains(entry)); - let timeout = Instant::now().checked_add(context.recovery_period).unwrap(); - pending_utxo_entries.iter().for_each(|entry| { - context.consumed.insert(entry.id().clone(), (entry, &timeout).into()); - }); - - context.outgoing.insert(pending_tx.id(), pending_tx.clone()); + let outgoing_transaction = OutgoingTransaction::new(current_daa_score, self.clone(), pending_tx.clone()); + self.processor().register_outgoing_transaction(outgoing_transaction.clone()); + context.outgoing.insert(outgoing_transaction.id(), outgoing_transaction); } - let processor = self.processor(); - processor.register_recoverable_context(self); - let record = TransactionRecord::new_outgoing(self, pending_tx); - processor.notify(Events::Outgoing { record }).await?; + Ok(()) + } + pub(crate) async fn notify_outgoing_transaction(&self, pending_tx: &PendingTransaction) -> Result<()> { + let outgoing_tx = self.processor().outgoing().get(&pending_tx.id()).expect("outgoing transaction for notification"); + + if pending_tx.is_batch() { + let record = TransactionRecord::new_batch(self, &outgoing_tx, None)?; + self.processor().notify(Events::Pending { record }).await?; + } else { + let record = TransactionRecord::new_outgoing(self, &outgoing_tx, None)?; + self.processor().notify(Events::Pending { record }).await?; + } + self.update_balance().await?; Ok(()) } - /// Removes entries from mature utxo set and adds them to the consumed utxo set. - /// NOTE: This method does not issue a notification on the event multiplexer. - /// This has been replaced with `handle_outgoing_transaction`, pending decision - /// on removal. - #[allow(dead_code)] - pub(crate) async fn consume(&self, entries: &[UtxoEntryReference]) -> Result<()> { + /// Cancel outgoing transaction in case of a submission error. Removes [`OutgoingTransaction`] from the + /// [`UtxoProcessor`] and returns UtxoEntries from the outgoing transaction back to the mature pool. + pub(crate) async fn cancel_outgoing_transaction(&self, pending_tx: &PendingTransaction) -> Result<()> { + self.processor().cancel_outgoing_transaction(pending_tx.id()); + let mut context = self.context(); - context.mature.retain(|entry| !entries.contains(entry)); - let timeout = Instant::now().checked_add(context.recovery_period).unwrap(); - entries.iter().for_each(|entry| { - context.consumed.insert(entry.id().clone(), (entry, &timeout).into()); + + let outgoing_transaction = context.outgoing.remove(&pending_tx.id()).expect("outgoing transaction"); + outgoing_transaction.utxo_entries().iter().for_each(|entry| { + context.mature.push(entry.clone()); }); Ok(()) @@ -279,30 +297,45 @@ impl UtxoContext { /// Insert `utxo_entry` into the `UtxoSet`. /// NOTE: The insert will be ignored if already present in the inner map. - pub async fn insert(&self, utxo_entry: UtxoEntryReference, current_daa_score: u64) -> Result<()> { + pub async fn insert(&self, utxo_entry: UtxoEntryReference, current_daa_score: u64, force_maturity: bool) -> Result<()> { let mut context = self.context(); if let std::collections::hash_map::Entry::Vacant(e) = context.map.entry(utxo_entry.id().clone()) { e.insert(utxo_entry.clone()); - if utxo_entry.is_mature(current_daa_score) { - context.mature.sorted_insert_binary_asc_by_key(utxo_entry, |entry| entry.amount_as_ref()); - Ok(()) + if force_maturity { + context.mature.sorted_insert_binary_asc_by_key(utxo_entry.clone(), |entry| entry.amount_as_ref()); } else { - context.pending.insert(utxo_entry.id().clone(), utxo_entry.clone()); - self.processor().pending().insert(utxo_entry.id().clone(), PendingUtxoEntryReference::new(utxo_entry, self.clone())); - Ok(()) + match utxo_entry.maturity(current_daa_score) { + Maturity::Stasis => { + context.stasis.insert(utxo_entry.id().clone(), utxo_entry.clone()); + self.processor() + .stasis() + .insert(utxo_entry.id().clone(), PendingUtxoEntryReference::new(utxo_entry, self.clone())); + } + Maturity::Pending => { + context.pending.insert(utxo_entry.id().clone(), utxo_entry.clone()); + self.processor() + .pending() + .insert(utxo_entry.id().clone(), PendingUtxoEntryReference::new(utxo_entry, self.clone())); + } + Maturity::Confirmed => { + context.mature.sorted_insert_binary_asc_by_key(utxo_entry.clone(), |entry| entry.amount_as_ref()); + } + } } + Ok(()) } else { - log_error!("ignoring duplicate utxo entry"); + log_warning!("ignoring duplicate utxo entry"); Ok(()) } } - pub async fn remove(&self, ids: Vec) -> Result> { + pub async fn remove(&self, utxos: Vec) -> Result> { let mut context = self.context(); let mut removed = vec![]; let mut remove_mature_ids = vec![]; - for id in ids.into_iter() { + for utxo in utxos.into_iter() { + let id = utxo.id(); // remove from local map if context.map.remove(&id).is_some() { if let Some(pending) = context.pending.remove(&id) { @@ -310,26 +343,19 @@ impl UtxoContext { if self.processor().pending().remove(&id).is_none() { log_error!("Error: unable to remove utxo entry from global pending (with context)"); } + } else if let Some(stasis) = context.stasis.remove(&id) { + removed.push(UtxoEntryVariant::Stasis(stasis)); + if self.processor().stasis().remove(&id).is_none() { + log_error!("Error: unable to remove utxo entry from global pending (with context)"); + } } else { remove_mature_ids.push(id); } } else { - log_error!("Error: unable to remove utxo entry from local map (with context)"); + log_error!("Error: UTXO not found in UtxoContext map!"); } } - let remove_mature_ids = remove_mature_ids - .into_iter() - .filter(|id| { - if let Some(consumed) = context.consumed.remove(id) { - removed.push(UtxoEntryVariant::Consumed(consumed.entry)); - false - } else { - true - } - }) - .collect::>(); - context.mature.retain(|entry| { if remove_mature_ids.contains(&entry.id()) { removed.push(UtxoEntryVariant::Mature(entry.clone())); @@ -342,6 +368,7 @@ impl UtxoContext { Ok(removed) } + /// This function handles `Pending` to `Mature` transformation. pub async fn promote(&self, utxos: Vec) -> Result<()> { let transactions = HashMap::group_from(utxos.iter().map(|utxo| (utxo.transaction_id(), utxo.clone()))); @@ -352,139 +379,263 @@ impl UtxoContext { context.mature.sorted_insert_binary_asc_by_key(utxo_entry.clone(), |entry| entry.amount_as_ref()); } else { log_error!("Error: non-pending utxo promotion!"); + unreachable!("Error: non-pending utxo promotion!"); } } - let is_outgoing = self.consume_outgoing_transaction(&txid); + if self.context().outgoing.get(&txid).is_some() { + unreachable!("Error: promotion of the outgoing transaction!"); + } - let record = TransactionRecord::new_incoming(self, TransactionType::Incoming, txid, utxos); - self.processor().notify(Events::Maturity { record, is_outgoing }).await?; + let record = TransactionRecord::new_incoming(self, txid, &utxos); + self.processor().notify(Events::Maturity { record }).await?; } Ok(()) } - fn is_outgoing_transaction(&self, txid: &TransactionId) -> bool { - let context = self.context(); - context.outgoing.contains_key(txid) - } - - fn consume_outgoing_transaction(&self, txid: &TransactionId) -> bool { - let mut context = self.context(); - context.outgoing.remove(txid).is_some() - } + /// This function handles `Stasis` to `Pending` transformation. + pub async fn revive(&self, utxos: Vec) -> Result<()> { + let transactions = HashMap::group_from(utxos.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); - pub async fn extend(&self, utxo_entries: Vec, current_daa_score: u64) -> Result<()> { - let mut context = self.context(); - for utxo_entry in utxo_entries.into_iter() { - if let std::collections::hash_map::Entry::Vacant(e) = context.map.entry(utxo_entry.id()) { - e.insert(utxo_entry.clone()); - if utxo_entry.is_mature(current_daa_score) { - context.mature.push(utxo_entry); - } else { + for (txid, utxos) in transactions.into_iter() { + for utxo_entry in utxos.iter() { + let mut context = self.context(); + if context.stasis.remove(utxo_entry.id_as_ref()).is_some() { context.pending.insert(utxo_entry.id(), utxo_entry.clone()); - self.processor().pending().insert(utxo_entry.id(), PendingUtxoEntryReference::new(utxo_entry, self.clone())); + } else { + log_error!("Error: non-stasis utxo revival!"); + panic!("Error: non-stasis utxo revival!"); } - } else { - log_warning!("ignoring duplicate utxo entry"); } - } - context.mature.sort(); + let record = TransactionRecord::new_incoming(self, txid, &utxos); + self.processor().notify(Events::Pending { record }).await?; + } Ok(()) } - /// recover UTXOs that went into `consumed` state but were never removed - /// from the set by the UtxoChanged notification. - pub fn recover(&self, _current_daa_score: u64) -> bool { + pub fn remove_outgoing_transaction(&self, txid: &TransactionId) -> Option { let mut context = self.context(); - if context.consumed.is_empty() { - return false; - } + context.outgoing.remove(txid) + } - let checkpoint = Instant::now(); - let mut removed = vec![]; - context.consumed.retain(|_, consumed| { - if consumed.timeout > checkpoint { - removed.push(consumed.entry.clone()); - false - } else { - true + pub async fn extend_from_scan(&self, utxo_entries: Vec, current_daa_score: u64) -> Result<()> { + let (pending, mature) = { + let mut context = self.context(); + + let mut pending = vec![]; + let mut mature = vec![]; + + for utxo_entry in utxo_entries.into_iter() { + if let std::collections::hash_map::Entry::Vacant(e) = context.map.entry(utxo_entry.id()) { + e.insert(utxo_entry.clone()); + match utxo_entry.maturity(current_daa_score) { + Maturity::Stasis => { + context.stasis.insert(utxo_entry.id().clone(), utxo_entry.clone()); + self.processor() + .stasis() + .insert(utxo_entry.id().clone(), PendingUtxoEntryReference::new(utxo_entry, self.clone())); + } + Maturity::Pending => { + pending.push(utxo_entry.clone()); + context.pending.insert(utxo_entry.id().clone(), utxo_entry.clone()); + self.processor() + .pending() + .insert(utxo_entry.id().clone(), PendingUtxoEntryReference::new(utxo_entry, self.clone())); + } + Maturity::Confirmed => { + mature.push(utxo_entry.clone()); + context.mature.sorted_insert_binary_asc_by_key(utxo_entry.clone(), |entry| entry.amount_as_ref()); + } + } + } else { + log_warning!("ignoring duplicate utxo entry"); + } } - }); - removed.into_iter().for_each(|entry| { - context.mature.sorted_insert_binary_asc_by_key(entry, |entry| entry.amount_as_ref()); - }); + (pending, mature) + }; + + // cascade discovery to the processor + // for unixtime resolution - context.consumed.is_not_empty() + let pending = HashMap::group_from(pending.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); + for (id, utxos) in pending.into_iter() { + let record = TransactionRecord::new_external(self, id, &utxos); + self.processor().handle_discovery(record).await?; + } + + let mature = HashMap::group_from(mature.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); + for (id, utxos) in mature.into_iter() { + let record = TransactionRecord::new_external(self, id, &utxos); + self.processor().handle_discovery(record).await?; + } + + Ok(()) } pub async fn calculate_balance(&self) -> Balance { let context = self.context(); - let mature = context.mature.iter().map(|e| e.as_ref().entry.amount).sum(); - let pending = context.pending.values().map(|e| e.as_ref().entry.amount).sum(); - Balance::new(mature, pending) + let mature: u64 = context.mature.iter().map(|e| e.as_ref().entry.amount).sum(); + let pending: u64 = context.pending.values().map(|e| e.as_ref().entry.amount).sum(); + + // this will aggregate only transactions containing + // the final payments (not compound transactions) + // and outgoing transactions that have not yet + // been accepted + let mut outgoing: u64 = 0; + let mut consumed: u64 = 0; + for tx in context.outgoing.values() { + if !tx.is_accepted() { + if let Some(payment_value) = tx.payment_value() { + // final tx + outgoing += tx.fees() + payment_value; + consumed += tx.aggregate_input_value(); + } else { + // compound tx has no payment value + // we skip them, accumulating only fees + // as fees are the only component that will + // reduce the final balance after the + // compound process + outgoing += tx.fees(); + } + } + } + + Balance::new( + (mature + consumed) - outgoing, + pending, + outgoing, + context.mature.len(), + context.pending.len(), + context.stasis.len(), + ) } - pub(crate) async fn handle_utxo_added(&self, utxos: Vec) -> Result<()> { + pub(crate) async fn handle_utxo_added(&self, utxos: Vec, current_daa_score: u64) -> Result<()> { // add UTXOs to account set - let current_daa_score = self.processor().current_daa_score().expect("daa score expected when invoking handle_utxo_added()"); - for utxo in utxos.iter() { - if let Err(err) = self.insert(utxo.clone(), current_daa_score).await { - log_error!("{}", err); + let mut accepted_outgoing_transactions = AHashSet::new(); + + let added = HashMap::group_from(utxos.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); + for (txid, utxos) in added.into_iter() { + // get outgoing transaction from the processor in case the transaction + // originates from a different [`Account`] represented by a different [`UtxoContext`]. + let outgoing_transaction = self.processor().outgoing().get(&txid); + + let force_maturity_if_outgoing = outgoing_transaction.is_some(); + let is_coinbase_stasis = + utxos.first().map(|utxo| matches!(utxo.maturity(current_daa_score), Maturity::Stasis)).unwrap_or_default(); + + for utxo in utxos.iter() { + if let Err(err) = self.insert(utxo.clone(), current_daa_score, force_maturity_if_outgoing).await { + // TODO - remove `Result<>` from insert at a later date once + // we are confident that the insert will never result in an error. + log_error!("{}", err); + } + } + + if let Some(outgoing_transaction) = outgoing_transaction { + accepted_outgoing_transactions.insert((*outgoing_transaction).clone()); + + if outgoing_transaction.is_batch() { + let record = TransactionRecord::new_batch(self, &outgoing_transaction, Some(current_daa_score))?; + self.processor().notify(Events::Maturity { record }).await?; + } else if outgoing_transaction.originating_context() == self { + let record = TransactionRecord::new_change(self, &outgoing_transaction, Some(current_daa_score), &utxos)?; + self.processor().notify(Events::Maturity { record }).await?; + } else { + let record = + TransactionRecord::new_transfer_incoming(self, &outgoing_transaction, Some(current_daa_score), &utxos)?; + self.processor().notify(Events::Maturity { record }).await?; + } + } else if !is_coinbase_stasis { + // do not notify if coinbase transaction is in stasis + let record = TransactionRecord::new_incoming(self, txid, &utxos); + self.processor().notify(Events::Pending { record }).await?; } } - let pending = HashMap::group_from(utxos.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); - for (txid, utxos) in pending.into_iter() { - let is_outgoing = self.is_outgoing_transaction(&txid); - let record = TransactionRecord::new_incoming(self, TransactionType::Incoming, txid, utxos); - self.processor().notify(Events::Pending { record, is_outgoing }).await?; + for outgoing_transaction in accepted_outgoing_transactions.into_iter() { + outgoing_transaction.tag_as_accepted_at_daa_score(current_daa_score); } - self.update_balance().await?; Ok(()) } - pub(crate) async fn handle_utxo_removed(&self, utxos: Vec) -> Result<()> { + pub(crate) async fn handle_utxo_removed(&self, mut utxos: Vec, current_daa_score: u64) -> Result<()> { // remove UTXOs from account set - let utxo_ids: Vec = utxos.iter().map(|utxo| utxo.id()).collect(); - let removed = self.remove(utxo_ids).await?; + + let outgoing_transactions = self.processor().outgoing(); + let mut accepted_outgoing_transactions = HashSet::::new(); + + utxos.retain(|id| { + for outgoing_transaction in outgoing_transactions.iter() { + if outgoing_transaction.utxo_entries().contains(id) { + accepted_outgoing_transactions.insert((*outgoing_transaction).clone()); + return false; + } + } + true + }); + + for accepted_outgoing_transaction in accepted_outgoing_transactions.into_iter() { + if accepted_outgoing_transaction.is_batch() { + let record = TransactionRecord::new_batch(self, &accepted_outgoing_transaction, Some(current_daa_score))?; + self.processor().notify(Events::Maturity { record }).await?; + } else if accepted_outgoing_transaction.destination_context().is_some() { + let record = + TransactionRecord::new_transfer_outgoing(self, &accepted_outgoing_transaction, Some(current_daa_score), &utxos)?; + self.processor().notify(Events::Maturity { record }).await?; + } else { + let record = TransactionRecord::new_outgoing(self, &accepted_outgoing_transaction, Some(current_daa_score))?; + self.processor().notify(Events::Maturity { record }).await?; + } + } + + if utxos.is_empty() { + return Ok(()); + } + + let removed = self.remove(utxos).await?; let mut mature = vec![]; - let mut consumed = vec![]; let mut pending = vec![]; + let mut stasis = vec![]; removed.into_iter().for_each(|entry| match entry { UtxoEntryVariant::Mature(utxo) => { mature.push(utxo); } - UtxoEntryVariant::Consumed(utxo) => { - consumed.push(utxo); - } UtxoEntryVariant::Pending(utxo) => { pending.push(utxo); } + UtxoEntryVariant::Stasis(utxo) => { + stasis.push(utxo); + } }); let mature = HashMap::group_from(mature.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); let pending = HashMap::group_from(pending.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); + let stasis = HashMap::group_from(stasis.into_iter().map(|utxo| (utxo.transaction_id(), utxo))); for (txid, utxos) in mature.into_iter() { - let record = TransactionRecord::new_external(self, txid, utxos); - self.processor().notify(Events::External { record }).await?; + let record = TransactionRecord::new_external(self, txid, &utxos); + self.processor().notify(Events::Maturity { record }).await?; } for (txid, utxos) in pending.into_iter() { - let record = TransactionRecord::new_incoming(self, TransactionType::Reorg, txid, utxos); + let record = TransactionRecord::new_reorg(self, txid, &utxos); self.processor().notify(Events::Reorg { record }).await?; } - // post balance update - self.update_balance().await?; + for (txid, utxos) in stasis.into_iter() { + let record = TransactionRecord::new_stasis(self, txid, &utxos); + self.processor().notify(Events::Stasis { record }).await?; + } + Ok(()) } @@ -517,7 +668,7 @@ impl UtxoContext { local.remove(address); }); } else { - log_warning!("utxo processor: unregistering empty address set") + log_warning!("utxo processor: unregister for an empty address set") } Ok(()) @@ -532,7 +683,7 @@ impl UtxoContext { .current_daa_score() .expect("daa score or initialized UtxoProcessor are when invoking scan_and_register_addresses()") }); - self.extend(refs, current_daa_score).await?; + self.extend_from_scan(refs, current_daa_score).await?; self.update_balance().await?; Ok(()) } diff --git a/wallet/core/src/utxo/iterator.rs b/wallet/core/src/utxo/iterator.rs index 9e261ccd0..09c40d7fc 100644 --- a/wallet/core/src/utxo/iterator.rs +++ b/wallet/core/src/utxo/iterator.rs @@ -1,3 +1,7 @@ +//! +//! Associative iterator over the UTXO set. +//! + use crate::utxo::{UtxoContext, UtxoEntryReference}; pub struct UtxoIterator { diff --git a/wallet/core/src/utxo/mod.rs b/wallet/core/src/utxo/mod.rs index f3afeed6a..e4a61752f 100644 --- a/wallet/core/src/utxo/mod.rs +++ b/wallet/core/src/utxo/mod.rs @@ -1,21 +1,33 @@ +//! +//! UTXO handling primitives. +//! + +pub mod balance; pub mod binding; pub mod context; pub mod iterator; +pub mod outgoing; pub mod pending; pub mod processor; pub mod reference; pub mod scan; pub mod settings; pub mod stream; +pub mod sync; +pub use balance::Balance; pub use binding::UtxoContextBinding; pub use context::{UtxoContext, UtxoContextId}; pub use iterator::UtxoIterator; +pub use kash_consensus_wasm::UtxoEntryId; +pub use outgoing::OutgoingTransaction; pub use pending::PendingUtxoEntryReference; pub use processor::UtxoProcessor; -pub use reference::{TryIntoUtxoEntryReferences, UtxoEntryReference, UtxoEntryReferenceExtension}; +pub use reference::{Maturity, TryIntoUtxoEntryReferences, UtxoEntryReference, UtxoEntryReferenceExtension}; pub use scan::{Scan, ScanExtent}; pub use settings::*; pub use stream::UtxoStream; +pub use sync::SyncMonitor; -pub use kash_consensus_wasm::UtxoEntryId; +#[cfg(test)] +pub mod test; diff --git a/wallet/core/src/utxo/outgoing.rs b/wallet/core/src/utxo/outgoing.rs new file mode 100644 index 000000000..40eb89bdc --- /dev/null +++ b/wallet/core/src/utxo/outgoing.rs @@ -0,0 +1,105 @@ +//! +//! Implements the [`OutgoingTransaction`] type, +//! which is a wrapper around [`PendingTransaction`] +//! that adds additional transaction context information. +//! + +use crate::imports::*; +use crate::tx::PendingTransaction; +use crate::utxo::{UtxoContext, UtxoEntryReference}; + +struct Inner { + pub id: TransactionId, + pub pending_transaction: PendingTransaction, + pub originating_context: UtxoContext, + pub destination_context: Option, + #[allow(dead_code)] + pub creation_daa_score: u64, + pub acceptance_daa_score: AtomicU64, +} + +/// A wrapper around [`PendingTransaction`] that adds additional context and +/// convenience methods for handling within [`UtxoContext`]. +#[derive(Clone)] +pub struct OutgoingTransaction { + inner: Arc, +} + +impl OutgoingTransaction { + pub fn new(current_daa_score: u64, originating_context: UtxoContext, pending_transaction: PendingTransaction) -> Self { + let destination_context = pending_transaction.generator().destination_utxo_context().clone(); + + let inner = Inner { + id: pending_transaction.id(), + pending_transaction, + originating_context, + destination_context, + creation_daa_score: current_daa_score, + acceptance_daa_score: AtomicU64::new(0), + }; + + Self { inner: Arc::new(inner) } + } + + pub fn id(&self) -> TransactionId { + self.inner.id + } + + pub fn payment_value(&self) -> Option { + self.inner.pending_transaction.payment_value() + } + + pub fn fees(&self) -> u64 { + self.inner.pending_transaction.fees() + } + + pub fn aggregate_input_value(&self) -> u64 { + self.inner.pending_transaction.aggregate_input_value() + } + + pub fn pending_transaction(&self) -> &PendingTransaction { + &self.inner.pending_transaction + } + + pub fn tag_as_accepted_at_daa_score(&self, accepted_daa_score: u64) { + self.inner.acceptance_daa_score.store(accepted_daa_score, Ordering::Relaxed); + } + + pub fn acceptance_daa_score(&self) -> u64 { + self.inner.acceptance_daa_score.load(Ordering::Relaxed) + } + + pub fn is_accepted(&self) -> bool { + self.inner.acceptance_daa_score.load(Ordering::Relaxed) != 0 + } + + pub fn is_batch(&self) -> bool { + self.inner.pending_transaction.is_batch() + } + + pub fn utxo_entries(&self) -> &AHashSet { + self.inner.pending_transaction.utxo_entries() + } + + pub fn originating_context(&self) -> &UtxoContext { + &self.inner.originating_context + } + + pub fn destination_context(&self) -> &Option { + &self.inner.destination_context + } +} + +impl Eq for OutgoingTransaction {} + +impl PartialEq for OutgoingTransaction { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl std::hash::Hash for OutgoingTransaction { + fn hash(&self, state: &mut H) { + self.id().hash(state); + } +} diff --git a/wallet/core/src/utxo/pending.rs b/wallet/core/src/utxo/pending.rs index 260ed4bd1..84749c22c 100644 --- a/wallet/core/src/utxo/pending.rs +++ b/wallet/core/src/utxo/pending.rs @@ -1,6 +1,10 @@ +//! +//! Implements the [`PendingUtxoEntryReference`] type used +//! by the [`UtxoProcessor`] to monitor UTXO maturity progress. +//! + use crate::imports::*; -use crate::runtime::Account; -use crate::utxo::{UtxoContext, UtxoEntryId, UtxoEntryReference, UtxoEntryReferenceExtension}; +use crate::utxo::{Maturity, UtxoContext, UtxoEntryId, UtxoEntryReference, UtxoEntryReferenceExtension}; pub struct PendingUtxoEntryReferenceInner { pub entry: UtxoEntryReference, @@ -43,8 +47,8 @@ impl PendingUtxoEntryReference { } #[inline(always)] - pub fn is_mature(&self, current_daa_score: u64) -> bool { - self.inner().entry.is_mature(current_daa_score) + pub fn maturity(&self, current_daa_score: u64) -> Maturity { + self.inner().entry.maturity(current_daa_score) } } diff --git a/wallet/core/src/utxo/processor.rs b/wallet/core/src/utxo/processor.rs index ba8b858e9..f9e9c798d 100644 --- a/wallet/core/src/utxo/processor.rs +++ b/wallet/core/src/utxo/processor.rs @@ -1,4 +1,11 @@ -use futures::{select_biased, FutureExt}; +//! +//! Implements [`UtxoProcessor`], which is the main component +//! of the UTXO subsystem. It is responsible for managing and +//! coordinating multiple [`UtxoContext`] instances acting as +//! a hub for UTXO event dispersal and related processing. +//! + +use crate::imports::*; use kash_notify::{ listener::ListenerId, scope::{Scope, UtxosChangedScope, VirtualDaaScoreChangedScope}, @@ -11,25 +18,31 @@ use kash_wrpc_client::KashRpcClient; use workflow_core::channel::{Channel, DuplexChannel}; use workflow_core::task::spawn; -use crate::imports::*; +use crate::events::Events; use crate::result::Result; -use crate::utxo::{PendingUtxoEntryReference, UtxoContext, UtxoEntryId, UtxoEntryReference}; -use crate::{events::Events, runtime::SyncMonitor}; +use crate::utxo::{ + Maturity, OutgoingTransaction, PendingUtxoEntryReference, SyncMonitor, UtxoContext, UtxoEntryId, UtxoEntryReference, +}; +use crate::wallet::WalletBusMessage; +use async_std::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use kash_rpc_core::{ notify::connection::{ChannelConnection, ChannelType}, Notification, }; -use std::collections::HashMap; + +use super::UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA; pub struct Inner { - /// UTXOs pending maturity (confirmation) + /// Coinbase UTXOs in stasis + stasis: DashMap, + /// UTXOs pending maturity pending: DashMap, + /// Outgoing Transactions + outgoing: DashMap, + outgoing_longevity_period: u64, /// Address to UtxoContext map (maps all addresses used by /// all UtxoContexts to their respective UtxoContexts) address_to_utxo_context_map: DashMap, UtxoContext>, - /// UtxoContexts that have recoverable UTXOs (UTXOs used in - /// outgoing transactions, that have not yet been confirmed) - recoverable_contexts: DashSet, // --- current_daa_score: Arc, network_id: Arc>>, @@ -41,14 +54,23 @@ pub struct Inner { notification_channel: Channel, sync_proc: SyncMonitor, multiplexer: Multiplexer>, + wallet_bus: Option>, + notification_lock: AsyncMutex<()>, } impl Inner { - pub fn new(rpc: Option, network_id: Option, multiplexer: Multiplexer>) -> Self { + pub fn new( + rpc: Option, + network_id: Option, + multiplexer: Multiplexer>, + wallet_bus: Option>, + ) -> Self { Self { + stasis: DashMap::new(), pending: DashMap::new(), + outgoing: DashMap::new(), + outgoing_longevity_period: UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA.load(Ordering::Relaxed), address_to_utxo_context_map: DashMap::new(), - recoverable_contexts: DashSet::new(), current_daa_score: Arc::new(AtomicU64::new(0)), network_id: Arc::new(Mutex::new(network_id)), rpc: Mutex::new(rpc.clone()), @@ -59,6 +81,8 @@ impl Inner { notification_channel: Channel::::unbounded(), sync_proc: SyncMonitor::new(rpc.clone(), &multiplexer), multiplexer, + wallet_bus, + notification_lock: AsyncMutex::new(()), } } } @@ -69,9 +93,14 @@ pub struct UtxoProcessor { } impl UtxoProcessor { - pub fn new(rpc: Option, network_id: Option, multiplexer: Option>>) -> Self { + pub fn new( + rpc: Option, + network_id: Option, + multiplexer: Option>>, + wallet_bus: Option>, + ) -> Self { let multiplexer = multiplexer.unwrap_or_default(); - UtxoProcessor { inner: Arc::new(Inner::new(rpc, network_id, multiplexer)) } + UtxoProcessor { inner: Arc::new(Inner::new(rpc, network_id, multiplexer, wallet_bus)) } } pub fn rpc_api(&self) -> Arc { @@ -96,6 +125,10 @@ impl UtxoProcessor { Ok(()) } + pub fn wallet_bus(&self) -> &Option> { + &self.inner.wallet_bus + } + pub fn has_rpc(&self) -> bool { self.inner.rpc.lock().unwrap().is_some() } @@ -104,12 +137,16 @@ impl UtxoProcessor { &self.inner.multiplexer } + pub async fn notification_lock(&self) -> AsyncMutexGuard<()> { + self.inner.notification_lock.lock().await + } + pub fn sync_proc(&self) -> &SyncMonitor { &self.inner.sync_proc } - pub fn listener_id(&self) -> ListenerId { - self.inner.listener_id.lock().unwrap().expect("missing listener_id in UtxoProcessor::listener_id()") + pub fn listener_id(&self) -> Result { + self.inner.listener_id.lock().unwrap().ok_or(Error::ListenerId) } pub fn set_network_id(&self, network_id: NetworkId) { @@ -124,6 +161,14 @@ impl UtxoProcessor { &self.inner.pending } + pub fn outgoing(&self) -> &DashMap { + &self.inner.outgoing + } + + pub fn stasis(&self) -> &DashMap { + &self.inner.stasis + } + pub fn current_daa_score(&self) -> Option { self.is_connected().then_some(self.inner.current_daa_score.load(Ordering::SeqCst)) } @@ -150,7 +195,7 @@ impl UtxoProcessor { if !addresses.is_empty() { let addresses = addresses.into_iter().map(|address| (*address).clone()).collect::>(); let utxos_changed_scope = UtxosChangedScope { addresses }; - self.rpc_api().start_notify(self.listener_id(), Scope::UtxosChanged(utxos_changed_scope)).await?; + self.rpc_api().start_notify(self.listener_id()?, Scope::UtxosChanged(utxos_changed_scope)).await?; } else { log_error!("registering empty address list!"); } @@ -167,7 +212,7 @@ impl UtxoProcessor { if !addresses.is_empty() { let addresses = addresses.into_iter().map(|address| (*address).clone()).collect::>(); let utxos_changed_scope = UtxosChangedScope { addresses }; - self.rpc_api().stop_notify(self.listener_id(), Scope::UtxosChanged(utxos_changed_scope)).await?; + self.rpc_api().stop_notify(self.listener_id()?, Scope::UtxosChanged(utxos_changed_scope)).await?; } else { log_error!("unregistering empty address list!"); } @@ -193,59 +238,141 @@ impl UtxoProcessor { self.inner.current_daa_score.store(current_daa_score, Ordering::SeqCst); self.notify(Events::DAAScoreChange { current_daa_score }).await?; self.handle_pending(current_daa_score).await?; - self.handle_recoverable(current_daa_score).await?; + self.handle_outgoing(current_daa_score).await?; Ok(()) } pub async fn handle_pending(&self, current_daa_score: u64) -> Result<()> { - let mature_entries = { + let (mature_entries, revived_entries) = { + // scan and remove any pending entries that gained maturity let mut mature_entries = vec![]; let pending_entries = &self.inner.pending; - pending_entries.retain(|_, pending| { - if pending.is_mature(current_daa_score) { - mature_entries.push(pending.clone()); + pending_entries.retain(|_, pending_entry| match pending_entry.maturity(current_daa_score) { + Maturity::Confirmed => { + mature_entries.push(pending_entry.clone()); false - } else { - true } + _ => true, }); - mature_entries + + // scan and remove any stasis entries that can now become pending + // or gained maturity + let mut revived_entries = vec![]; + let stasis_entries = &self.inner.stasis; + stasis_entries.retain(|_, stasis_entry| { + match stasis_entry.maturity(current_daa_score) { + Maturity::Confirmed => { + mature_entries.push(stasis_entry.clone()); + false + } + Maturity::Pending => { + revived_entries.push(stasis_entry.clone()); + // relocate from stasis to pending ... + pending_entries.insert(stasis_entry.id(), stasis_entry.clone()); + false + } + Maturity::Stasis => true, + } + }); + (mature_entries, revived_entries) }; // ------ let promotions = HashMap::group_from(mature_entries.into_iter().map(|utxo| (utxo.inner.utxo_context.clone(), utxo.inner.entry.clone()))); - let contexts = promotions.keys().cloned().collect::>(); + let mut updated_contexts: HashSet = HashSet::from_iter(promotions.keys().cloned()); for (context, utxos) in promotions.into_iter() { context.promote(utxos).await?; } - for context in contexts.into_iter() { + // ------ + + let revivals = + HashMap::group_from(revived_entries.into_iter().map(|utxo| (utxo.inner.utxo_context.clone(), utxo.inner.entry.clone()))); + updated_contexts.extend(revivals.keys().cloned()); + + for (context, utxos) in revivals.into_iter() { + context.revive(utxos).await?; + } + + for context in updated_contexts.into_iter() { context.update_balance().await?; } Ok(()) } - async fn handle_recoverable(&self, current_daa_score: u64) -> Result<()> { - self.inner.recoverable_contexts.retain(|context| context.recover(current_daa_score)); + async fn handle_outgoing(&self, current_daa_score: u64) -> Result<()> { + let longevity = self.inner.outgoing_longevity_period; + + self.inner.outgoing.retain(|_, outgoing| { + if outgoing.acceptance_daa_score() != 0 && (outgoing.acceptance_daa_score() + longevity) < current_daa_score { + outgoing.originating_context().remove_outgoing_transaction(&outgoing.id()); + false + } else { + true + } + }); Ok(()) } - pub fn register_recoverable_context(&self, context: &UtxoContext) { - self.inner.recoverable_contexts.insert(context.clone()); + pub fn register_outgoing_transaction(&self, outgoing_transaction: OutgoingTransaction) { + self.inner.outgoing.insert(outgoing_transaction.id(), outgoing_transaction); + } + + pub fn cancel_outgoing_transaction(&self, transaction_id: TransactionId) { + self.inner.outgoing.remove(&transaction_id); + } + + pub async fn handle_discovery(&self, record: TransactionRecord) -> Result<()> { + if let Some(wallet_bus) = self.wallet_bus() { + // if UtxoProcessor has an associated wallet_bus installed + // by the wallet, cascade the discovery to the wallet so that + // it can check if the record exists in its storage and handle + // it in accordance to its policies. + wallet_bus.sender.send(WalletBusMessage::Discovery { record }).await?; + } else { + // otherwise we fetch the unixtime and broadcast the discovery event + let transaction_daa_score = record.block_daa_score(); + match self.rpc_api().get_daa_score_timestamp_estimate(vec![transaction_daa_score]).await { + Ok(timestamps) => { + if let Some(timestamp) = timestamps.first() { + let mut record = record.clone(); + record.set_unixtime(*timestamp); + self.notify(Events::Discovery { record }).await?; + } else { + self.notify(Events::Error { + message: format!( + "Unable to obtain DAA to unixtime for DAA {transaction_daa_score}, timestamp data is empty" + ), + }) + .await?; + } + } + Err(err) => { + self.notify(Events::Error { message: format!("Unable to resolve DAA to unixtime: {err}") }).await?; + } + } + } + + Ok(()) } pub async fn handle_utxo_changed(&self, utxos: UtxosChangedNotification) -> Result<()> { + let current_daa_score = self.current_daa_score().expect("DAA score expected when handling UTXO Changed notifications"); + + let mut updated_contexts: HashSet = HashSet::default(); + let removed = (*utxos.removed).clone().into_iter().filter_map(|entry| entry.address.clone().map(|address| (address, entry))); let removed = HashMap::group_from(removed); for (address, entries) in removed.into_iter() { if let Some(utxo_context) = self.address_to_utxo_context(&address) { + updated_contexts.insert(utxo_context.clone()); let entries = entries.into_iter().map(|entry| entry.into()).collect::>(); - utxo_context.handle_utxo_removed(entries).await?; + utxo_context.handle_utxo_removed(entries, current_daa_score).await?; } else { log_error!("receiving UTXO Changed 'removed' notification for an unknown address: {}", address); } @@ -255,13 +382,20 @@ impl UtxoProcessor { let added = HashMap::group_from(added); for (address, entries) in added.into_iter() { if let Some(utxo_context) = self.address_to_utxo_context(&address) { + updated_contexts.insert(utxo_context.clone()); let entries = entries.into_iter().map(|entry| entry.into()).collect::>(); - utxo_context.handle_utxo_added(entries).await?; + utxo_context.handle_utxo_added(entries, current_daa_score).await?; } else { log_error!("receiving UTXO Changed 'added' notification for an unknown address: {}", address); } } + // iterate over all affected utxo contexts and + // update as well as notify their balances. + for context in updated_contexts.iter() { + context.update_balance().await?; + } + Ok(()) } @@ -380,7 +514,7 @@ impl UtxoProcessor { } async fn handle_notification(&self, notification: Notification) -> Result<()> { - // log_info!("handling notification: {:?}", notification); + let _lock = self.notification_lock().await; match notification { Notification::VirtualDaaScoreChanged(virtual_daa_score_changed_notification) => { @@ -461,9 +595,10 @@ impl UtxoProcessor { notification = notification_receiver.recv().fuse() => { match notification { Ok(notification) => { - this.handle_notification(notification).await.unwrap_or_else(|err| { + if let Err(err) = this.handle_notification(notification).await { + this.notify(Events::UtxoProcError { message: err.to_string() }).await.ok(); log_error!("error while handling notification: {err}"); - }); + } } Err(err) => { log_error!("RPC notification channel error: {err}"); @@ -502,3 +637,18 @@ impl UtxoProcessor { Ok(()) } } + +#[cfg(test)] +pub(crate) mod mock { + use super::*; + + impl UtxoProcessor { + pub fn mock_set_connected(&self, connected: bool) { + self.inner.is_connected.store(connected, Ordering::SeqCst); + } + + // pub fn mock_set_daa_score(&self, connected : bool) { + // self.inner.is_connected.store(connected, Ordering::SeqCst); + // } + } +} diff --git a/wallet/core/src/utxo/reference.rs b/wallet/core/src/utxo/reference.rs index c5fdca6f9..1391a4308 100644 --- a/wallet/core/src/utxo/reference.rs +++ b/wallet/core/src/utxo/reference.rs @@ -1,27 +1,64 @@ +//! +//! Extensions for [`UtxoEntryReference`] for handling UTXO maturity. +//! + use crate::imports::*; -use crate::runtime::Balance; -use crate::utxo::{UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA, UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA}; +use crate::utxo::{ + UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA, UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA, + UTXO_STASIS_PERIOD_COINBASE_TRANSACTION_DAA, +}; pub use kash_consensus_wasm::{TryIntoUtxoEntryReferences, UtxoEntryReference}; +pub enum Maturity { + /// Coinbase UTXO that has not reached [`UTXO_STASIS_PERIOD_COINBASE_TRANSACTION_DAA`] + Stasis, + /// Coinbase UTXO that has reached [`UTXO_STASIS_PERIOD_COINBASE_TRANSACTION_DAA`] + /// but has not reached [`UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA`] or + /// user UTXO that has not reached [`UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA`] + Pending, + /// UTXO that has reached [`UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA`] or + /// [`UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA`] respectively. + Confirmed, +} + +impl std::fmt::Display for Maturity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Maturity::Stasis => write!(f, "stasis"), + Maturity::Pending => write!(f, "pending"), + Maturity::Confirmed => write!(f, "confirmed"), + } + } +} + pub trait UtxoEntryReferenceExtension { - fn is_mature(&self, current_daa_score: u64) -> bool; + fn maturity(&self, current_daa_score: u64) -> Maturity; fn balance(&self, current_daa_score: u64) -> Balance; } impl UtxoEntryReferenceExtension for UtxoEntryReference { - fn is_mature(&self, current_daa_score: u64) -> bool { + fn maturity(&self, current_daa_score: u64) -> Maturity { if self.is_coinbase() { - self.block_daa_score() + UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA.load(Ordering::SeqCst) < current_daa_score + if self.block_daa_score() + UTXO_STASIS_PERIOD_COINBASE_TRANSACTION_DAA.load(Ordering::SeqCst) > current_daa_score { + Maturity::Stasis + } else if self.block_daa_score() + UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA.load(Ordering::SeqCst) > current_daa_score + { + Maturity::Pending + } else { + Maturity::Confirmed + } + } else if self.block_daa_score() + UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA.load(Ordering::SeqCst) > current_daa_score { + Maturity::Pending } else { - self.block_daa_score() + UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA.load(Ordering::SeqCst) < current_daa_score + Maturity::Confirmed } } fn balance(&self, current_daa_score: u64) -> Balance { - if self.is_mature(current_daa_score) { - Balance::new(self.amount(), 0) - } else { - Balance::new(0, self.amount()) + match self.maturity(current_daa_score) { + Maturity::Pending => Balance::new(0, self.amount(), self.amount(), 0, 1, 0), + Maturity::Stasis => Balance::new(0, 0, 0, 0, 0, 1), + Maturity::Confirmed => Balance::new(self.amount(), 0, 0, 1, 0, 0), } } } diff --git a/wallet/core/src/utxo/scan.rs b/wallet/core/src/utxo/scan.rs index d816f2abb..5b07612fd 100644 --- a/wallet/core/src/utxo/scan.rs +++ b/wallet/core/src/utxo/scan.rs @@ -1,7 +1,12 @@ +//! +//! Address scanner implementation, responsible for +//! aggregating UTXOs from multiple addresses and +//! building corresponding balances. +//! + use crate::derivation::AddressManager; use crate::imports::*; -use crate::result::Result; -use crate::runtime::{AtomicBalance, Balance}; +use crate::utxo::balance::AtomicBalance; use crate::utxo::{UtxoContext, UtxoEntryReference, UtxoEntryReferenceExtension}; use kash_consensus_core::asset_type::AssetType; use std::cmp::max; @@ -84,11 +89,6 @@ impl Scan { let mut last_address_index = address_manager.index(); 'scan: loop { - // Initialize separate balances for each currency - let mut ksh_balance = Balance::default(); - let mut kusd_balance = Balance::default(); - let mut krv_balance = Balance::default(); - let first = cursor; let last = if cursor == 0 { max(last_address_index + 1, window_size) } else { cursor + window_size }; cursor = last; @@ -104,44 +104,45 @@ impl Scan { } yield_executor().await; - let refs: Vec = resp.into_iter().map(UtxoEntryReference::from).collect(); - for utxo_ref in refs.iter() { - if let Some(address) = utxo_ref.utxo.address.as_ref() { - if let Some(utxo_address_index) = address_manager.inner().address_to_index_map.get(address) { - if last_address_index < *utxo_address_index { - last_address_index = *utxo_address_index; + if !resp.is_empty() { + let refs: Vec = resp.into_iter().map(UtxoEntryReference::from).collect(); + for utxo_ref in refs.iter() { + if let Some(address) = utxo_ref.utxo.address.as_ref() { + if let Some(utxo_address_index) = address_manager.inner().address_to_index_map.get(address) { + if last_address_index < *utxo_address_index { + last_address_index = *utxo_address_index; + } + } else { + panic!("Account::scan_address_manager() has received an unknown address: `{address}`"); } - } else { - panic!("Account::scan_address_manager() has received an unknown address: `{address}`"); } } - - // Update balance based on asset type - let entry_balance = utxo_ref.balance(self.current_daa_score); - match utxo_ref.utxo.entry.asset_type { - AssetType::KSH => { - ksh_balance.mature += entry_balance.mature; - ksh_balance.pending += entry_balance.pending; - } - AssetType::KUSD => { - kusd_balance.mature += entry_balance.mature; - kusd_balance.pending += entry_balance.pending; - } - AssetType::KRV => { - krv_balance.mature += entry_balance.mature; - krv_balance.pending += entry_balance.pending; + // Initialize separate balances for each currency + let mut ksh_balance = Balance::default(); + let mut kusd_balance = Balance::default(); + let mut krv_balance = Balance::default(); + + // Process each UTXO reference and accumulate balances for each asset type + refs.iter().for_each(|utxo_ref| { + let entry_balance = utxo_ref.balance(self.current_daa_score); + match utxo_ref.utxo.entry.asset_type { + AssetType::KSH => update_balance(&mut ksh_balance, entry_balance), + AssetType::KUSD => update_balance(&mut kusd_balance, entry_balance), + AssetType::KRV => update_balance(&mut krv_balance, entry_balance), } - } - } - yield_executor().await; + }); - utxo_context.extend(refs, self.current_daa_score).await?; + utxo_context.extend_from_scan(refs, self.current_daa_score).await?; - // Check if any balance is not empty and update accordingly - if !ksh_balance.is_empty() || !kusd_balance.is_empty() || !krv_balance.is_empty() { - self.ksh_balance.add(ksh_balance); - self.kusd_balance.add(kusd_balance); - self.krv_balance.add(krv_balance); + if !ksh_balance.is_empty() { + self.ksh_balance.add(ksh_balance); + } + if !kusd_balance.is_empty() { + self.kusd_balance.add(kusd_balance); + } + if !krv_balance.is_empty() { + self.krv_balance.add(krv_balance); + } } else { match &extent { ScanExtent::EmptyWindow => { @@ -171,41 +172,44 @@ impl Scan { let resp = utxo_context.processor().rpc_api().get_utxos_by_addresses(address_vec).await?; let refs: Vec = resp.into_iter().map(UtxoEntryReference::from).collect(); - // Initialize separate balances for each currency - let mut ksh_balance = Balance::default(); - let mut kusd_balance = Balance::default(); - let mut krv_balance = Balance::default(); + // Structs to hold the balances for each asset type + let mut ksh_total_balance = Balance::default(); + let mut kusd_total_balance = Balance::default(); + let mut krv_total_balance = Balance::default(); + // Process each UTXO reference and accumulate balances for each asset type for r in refs.iter() { - // Update balance based on asset type let entry_balance = r.balance(self.current_daa_score); match r.utxo.entry.asset_type { - AssetType::KSH => { - ksh_balance.mature += entry_balance.mature; - ksh_balance.pending += entry_balance.pending; - } - AssetType::KUSD => { - kusd_balance.mature += entry_balance.mature; - kusd_balance.pending += entry_balance.pending; - } - AssetType::KRV => { - krv_balance.mature += entry_balance.mature; - krv_balance.pending += entry_balance.pending; - } + AssetType::KSH => update_balance(&mut ksh_total_balance, entry_balance), + AssetType::KUSD => update_balance(&mut kusd_total_balance, entry_balance), + AssetType::KRV => update_balance(&mut krv_total_balance, entry_balance), } } yield_executor().await; - utxo_context.extend(refs, self.current_daa_score).await?; + utxo_context.extend_from_scan(refs, self.current_daa_score).await?; - // Check if any balance is not empty and update accordingly - if !ksh_balance.is_empty() || !kusd_balance.is_empty() || !krv_balance.is_empty() { - // Here you need to update the respective balances for KSH, KUSD, KRV - self.ksh_balance.add(ksh_balance); - self.kusd_balance.add(kusd_balance); - self.krv_balance.add(krv_balance); + // Update the Scan struct balances + if !ksh_total_balance.is_empty() { + self.ksh_balance.add(ksh_total_balance); + } + if !kusd_total_balance.is_empty() { + self.kusd_balance.add(kusd_total_balance); + } + if !krv_total_balance.is_empty() { + self.krv_balance.add(krv_total_balance); } Ok(()) } } + +// Function to update balance +fn update_balance(total_balance: &mut Balance, entry_balance: Balance) { + total_balance.mature += entry_balance.mature; + total_balance.pending += entry_balance.pending; + total_balance.mature_utxo_count += entry_balance.mature_utxo_count; + total_balance.pending_utxo_count += entry_balance.pending_utxo_count; + total_balance.stasis_utxo_count += entry_balance.stasis_utxo_count; +} diff --git a/wallet/core/src/utxo/settings.rs b/wallet/core/src/utxo/settings.rs index a6b438947..d40fb8be9 100644 --- a/wallet/core/src/utxo/settings.rs +++ b/wallet/core/src/utxo/settings.rs @@ -1,38 +1,48 @@ +//! +//! Wallet framework settings that control maturity +//! durations. +//! + use crate::imports::*; use crate::result::Result; /// Maturity period for coinbase transactions. -pub static UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA: AtomicU64 = AtomicU64::new(128); +pub static UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA: AtomicU64 = AtomicU64::new(100); +/// Stasis period for coinbase transactions (no standard notifications occur until the +/// coinbase tx is out of stasis). +pub static UTXO_STASIS_PERIOD_COINBASE_TRANSACTION_DAA: AtomicU64 = AtomicU64::new(50); /// Maturity period for user transactions. -pub static UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA: AtomicU64 = AtomicU64::new(16); -/// Recovery period for UTXOs used in transactions. -pub static UTXO_RECOVERY_PERIOD_SECONDS: AtomicU64 = AtomicU64::new(180); +pub static UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA: AtomicU64 = AtomicU64::new(10); +/// Enables wallet events containing context UTXO updates. +/// Useful if the client wants to keep track of UTXO sets or +/// supply them during creation of transactions. +pub static ENABLE_UTXO_SELECTION_EVENTS: AtomicBool = AtomicBool::new(false); #[derive(Default)] pub struct UtxoProcessingSettings { pub coinbase_transaction_maturity_daa: Option, pub user_transaction_maturity_daa: Option, - pub utxo_recovery_period_seconds: Option, + pub enable_utxo_selection_events: Option, } impl UtxoProcessingSettings { pub fn new( coinbase_transaction_maturity_daa: Option, user_transaction_maturity_daa: Option, - utxo_recovery_period_seconds: Option, + enable_utxo_selection_events: Option, ) -> Self { - Self { coinbase_transaction_maturity_daa, user_transaction_maturity_daa, utxo_recovery_period_seconds } + Self { coinbase_transaction_maturity_daa, user_transaction_maturity_daa, enable_utxo_selection_events } } - pub fn init(thresholds: UtxoProcessingSettings) { - if let Some(v) = thresholds.coinbase_transaction_maturity_daa { + pub fn init(settings: UtxoProcessingSettings) { + if let Some(v) = settings.coinbase_transaction_maturity_daa { UTXO_MATURITY_PERIOD_COINBASE_TRANSACTION_DAA.store(v, Ordering::Relaxed) } - if let Some(v) = thresholds.user_transaction_maturity_daa { + if let Some(v) = settings.user_transaction_maturity_daa { UTXO_MATURITY_PERIOD_USER_TRANSACTION_DAA.store(v, Ordering::Relaxed) } - if let Some(v) = thresholds.utxo_recovery_period_seconds { - UTXO_RECOVERY_PERIOD_SECONDS.store(v, Ordering::Relaxed) + if let Some(v) = settings.enable_utxo_selection_events { + ENABLE_UTXO_SELECTION_EVENTS.store(v, Ordering::Relaxed) } } } @@ -42,10 +52,10 @@ pub fn configure_utxo_processing(thresholds: &JsValue) -> Result<()> { let object = Object::try_from(thresholds).ok_or(Error::custom("Supplied value must be an object"))?; let coinbase_transaction_maturity_daa = object.get_u64("coinbaseTransactionMaturityInDAA").ok(); let user_transaction_maturity_daa = object.get_u64("userTransactionMaturityInDAA").ok(); - let utxo_recovery_period_seconds = object.get_u64("utxoRecoveryPeriodInSeconds").ok(); + let enable_utxo_selection_events = object.get_bool("enableUtxoSelectionEvents").ok(); let thresholds = - UtxoProcessingSettings { coinbase_transaction_maturity_daa, user_transaction_maturity_daa, utxo_recovery_period_seconds }; + UtxoProcessingSettings { coinbase_transaction_maturity_daa, user_transaction_maturity_daa, enable_utxo_selection_events }; UtxoProcessingSettings::init(thresholds); diff --git a/wallet/core/src/utxo/stream.rs b/wallet/core/src/utxo/stream.rs index 37ec9c6da..cd7e6c54e 100644 --- a/wallet/core/src/utxo/stream.rs +++ b/wallet/core/src/utxo/stream.rs @@ -1,3 +1,7 @@ +//! +//! Implements an async stream of UTXOs. +//! + use super::{UtxoContext, UtxoEntryReference}; use crate::imports::*; diff --git a/wallet/core/src/runtime/sync.rs b/wallet/core/src/utxo/sync.rs similarity index 96% rename from wallet/core/src/runtime/sync.rs rename to wallet/core/src/utxo/sync.rs index 8acd198e0..d12a89c5f 100644 --- a/wallet/core/src/runtime/sync.rs +++ b/wallet/core/src/utxo/sync.rs @@ -1,3 +1,8 @@ +//! +//! Sync monitor implementation. Sync monitor tracks +//! the node's sync state and notifies the wallet. +//! + use crate::imports::*; use crate::result::Result; use futures::stream::StreamExt; @@ -139,7 +144,6 @@ impl SyncMonitor { if is_synced != this.is_synced() { this.inner.is_synced.store(true, Ordering::SeqCst); this.notify(Events::SyncState { sync_state : SyncState::Synced }).await.unwrap_or_else(|err|log_error!("SyncProc error dispatching notification event: {err}")); - // this.notify(Events::NodeSync { is_synced }).await.unwrap_or_else(|err|log_error!("SyncProc error dispatching notification event: {err}")); } break; @@ -163,7 +167,7 @@ impl SyncMonitor { } } - log_info!("sync monitor task is shutting down..."); + log_trace!("sync monitor task is shutting down..."); this.inner.running.store(false, Ordering::SeqCst); task_ctl_sender.send(()).await.unwrap(); }); @@ -256,8 +260,6 @@ impl StateObserver { } } else if self.utxo_resync.is_match(line) { state = Some(SyncState::UtxoResync); - // } else if self.accepted_block.is_match(line) { - // state = Some(SyncState::UtxoResync); } state diff --git a/wallet/core/src/utxo/test.rs b/wallet/core/src/utxo/test.rs new file mode 100644 index 000000000..47e733f77 --- /dev/null +++ b/wallet/core/src/utxo/test.rs @@ -0,0 +1,45 @@ +use crate::imports::*; +use crate::result::Result; +use crate::tests::RpcCoreMock; +use crate::tx::generator::test::*; +use crate::tx::*; +use crate::utils::*; +use crate::utxo::*; +use kash_consensus_core::asset_type::AssetType; +use kash_consensus_core::tx::TransactionAction; + +#[tokio::test] +async fn test_utxo_subsystem_bootstrap() -> Result<()> { + let network_id = NetworkId::with_suffix(NetworkType::Testnet, 0); + let rpc_api_mock = Arc::new(RpcCoreMock::new()); + let processor = UtxoProcessor::new(Some(rpc_api_mock.clone().into()), Some(network_id), None, None); + let _context = UtxoContext::new(&processor, UtxoContextBinding::default()); + + processor.mock_set_connected(true); + processor.handle_daa_score_change(1).await?; + // println!("daa score: {:?}", processor.current_daa_score()); + // context.register_addresses(&[output_address(network_id.into())]).await?; + Ok(()) +} + +#[test] +fn test_utxo_generator_empty_utxo_noop() -> Result<()> { + let network_type = NetworkType::Testnet; + let output_address = output_address(network_type); + + let payment_output = PaymentOutput::new(output_address, kash_to_sompi(2.0), AssetType::KSH); + let generator = make_generator( + network_type, + &[10.0], + &[], + Fees::SenderPaysAll(0), + change_address, + TransactionAction::TransferKSH, + payment_output.into(), + ) + .unwrap(); + let _tx = generator.generate_transaction().unwrap(); + // println!("tx: {:?}", tx); + // assert!(tx.is_none()); + Ok(()) +} diff --git a/wallet/core/src/wallet/api.rs b/wallet/core/src/wallet/api.rs new file mode 100644 index 000000000..2f9d79808 --- /dev/null +++ b/wallet/core/src/wallet/api.rs @@ -0,0 +1,361 @@ +//! +//! [`WalletApi`] trait implementation for [`Wallet`]. +//! + +use crate::api::{message::*, traits::WalletApi}; +use crate::imports::*; +use crate::result::Result; +use crate::storage::interface::TransactionRangeResult; +use crate::storage::Binding; +use crate::tx::Fees; +use workflow_core::channel::Receiver; + +#[async_trait] +impl WalletApi for super::Wallet { + async fn register_notifications(self: Arc, _channel: Receiver) -> Result { + todo!() + } + async fn unregister_notifications(self: Arc, _channel_id: u64) -> Result<()> { + todo!() + } + + async fn get_status_call(self: Arc, _request: GetStatusRequest) -> Result { + let is_connected = self.is_connected(); + let is_synced = self.is_synced(); + let is_open = self.is_open(); + let network_id = self.network_id().ok(); + let (url, is_wrpc_client) = + if let Some(wrpc_client) = self.wrpc_client() { (Some(wrpc_client.url()), true) } else { (None, false) }; + + Ok(GetStatusResponse { is_connected, is_synced, is_open, network_id, url, is_wrpc_client }) + } + + // ------------------------------------------------------------------------------------- + + async fn connect_call(self: Arc, request: ConnectRequest) -> Result { + use workflow_rpc::client::{ConnectOptions, ConnectStrategy}; + + let ConnectRequest { url, network_id } = request; + + if let Some(wrpc_client) = self.wrpc_client().as_ref() { + // let network_type = NetworkType::from(network_id); + let url = wrpc_client.parse_url_with_network_type(url, network_id.into()).map_err(|e| e.to_string())?; + let options = ConnectOptions { + block_async_connect: true, + strategy: ConnectStrategy::Fallback, + url: Some(url), + ..Default::default() + }; + wrpc_client.connect(options).await.map_err(|e| e.to_string())?; + Ok(ConnectResponse {}) + } else { + Err(Error::NotWrpcClient) + } + } + + async fn disconnect_call(self: Arc, _request: DisconnectRequest) -> Result { + if let Some(wrpc_client) = self.wrpc_client().as_ref() { + wrpc_client.shutdown().await?; + Ok(DisconnectResponse {}) + } else { + Err(Error::NotWrpcClient) + } + } + + // ------------------------------------------------------------------------------------- + + async fn ping_call(self: Arc, request: PingRequest) -> Result { + log_info!("Wallet received ping request '{:?}' ...", request.payload); + Ok(PingResponse { payload: request.payload }) + } + + async fn batch_call(self: Arc, _request: BatchRequest) -> Result { + self.store().batch().await?; + Ok(BatchResponse {}) + } + + async fn flush_call(self: Arc, request: FlushRequest) -> Result { + let FlushRequest { wallet_secret } = request; + self.store().flush(&wallet_secret).await?; + Ok(FlushResponse {}) + } + + async fn wallet_enumerate_call(self: Arc, _request: WalletEnumerateRequest) -> Result { + let wallet_list = self.store().wallet_list().await?; + Ok(WalletEnumerateResponse { wallet_list }) + } + + async fn wallet_create_call(self: Arc, request: WalletCreateRequest) -> Result { + let WalletCreateRequest { wallet_secret, wallet_args } = request; + + let (wallet_descriptor, storage_descriptor) = self.create_wallet(&wallet_secret, wallet_args).await?; + + Ok(WalletCreateResponse { wallet_descriptor, storage_descriptor }) + } + + async fn wallet_open_call(self: Arc, request: WalletOpenRequest) -> Result { + let WalletOpenRequest { wallet_secret, wallet_filename, account_descriptors, legacy_accounts } = request; + let args = WalletOpenArgs { account_descriptors, legacy_accounts: legacy_accounts.unwrap_or_default() }; + let account_descriptors = self.open(&wallet_secret, wallet_filename, args).await?; + Ok(WalletOpenResponse { account_descriptors }) + } + + async fn wallet_close_call(self: Arc, _request: WalletCloseRequest) -> Result { + self.close().await?; + Ok(WalletCloseResponse {}) + } + + async fn wallet_rename_call(self: Arc, request: WalletRenameRequest) -> Result { + let WalletRenameRequest { wallet_secret, title, filename } = request; + self.rename(title, filename, &wallet_secret).await?; + Ok(WalletRenameResponse {}) + } + + async fn wallet_change_secret_call(self: Arc, request: WalletChangeSecretRequest) -> Result { + let WalletChangeSecretRequest { old_wallet_secret, new_wallet_secret } = request; + self.store().change_secret(&old_wallet_secret, &new_wallet_secret).await?; + Ok(WalletChangeSecretResponse {}) + } + + async fn wallet_export_call(self: Arc, request: WalletExportRequest) -> Result { + let WalletExportRequest { wallet_secret, include_transactions } = request; + + let options = storage::WalletExportOptions { include_transactions }; + let wallet_data = self.store().wallet_export(&wallet_secret, options).await?; + + Ok(WalletExportResponse { wallet_data }) + } + + async fn wallet_import_call(self: Arc, request: WalletImportRequest) -> Result { + let WalletImportRequest { wallet_secret, wallet_data } = request; + + let wallet_descriptor = self.store().wallet_import(&wallet_secret, &wallet_data).await?; + + Ok(WalletImportResponse { wallet_descriptor }) + } + + async fn prv_key_data_enumerate_call( + self: Arc, + _request: PrvKeyDataEnumerateRequest, + ) -> Result { + let prv_key_data_list = self.store().as_prv_key_data_store()?.iter().await?.try_collect::>().await?; + Ok(PrvKeyDataEnumerateResponse { prv_key_data_list }) + } + + async fn prv_key_data_create_call(self: Arc, request: PrvKeyDataCreateRequest) -> Result { + let PrvKeyDataCreateRequest { wallet_secret, prv_key_data_args } = request; + let prv_key_data_id = self.create_prv_key_data(&wallet_secret, prv_key_data_args).await?; + Ok(PrvKeyDataCreateResponse { prv_key_data_id }) + } + + async fn prv_key_data_remove_call(self: Arc, _request: PrvKeyDataRemoveRequest) -> Result { + // TODO handle key removal + return Err(Error::NotImplemented); + } + + async fn prv_key_data_get_call(self: Arc, request: PrvKeyDataGetRequest) -> Result { + let PrvKeyDataGetRequest { prv_key_data_id, wallet_secret } = request; + + let prv_key_data = self.store().as_prv_key_data_store()?.load_key_data(&wallet_secret, &prv_key_data_id).await?; + + Ok(PrvKeyDataGetResponse { prv_key_data }) + } + + async fn accounts_rename_call(self: Arc, request: AccountsRenameRequest) -> Result { + let AccountsRenameRequest { account_id, name, wallet_secret } = request; + + let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + account.rename(&wallet_secret, name.as_deref()).await?; + + Ok(AccountsRenameResponse {}) + } + + async fn accounts_enumerate_call(self: Arc, _request: AccountsEnumerateRequest) -> Result { + let account_list = self.accounts(None).await?.try_collect::>().await?; + let descriptor_list = account_list.iter().map(|account| account.descriptor().unwrap()).collect::>(); + + Ok(AccountsEnumerateResponse { descriptor_list }) + } + + async fn accounts_activate_call(self: Arc, request: AccountsActivateRequest) -> Result { + let AccountsActivateRequest { account_ids } = request; + + self.activate_accounts(account_ids.as_deref()).await?; + + Ok(AccountsActivateResponse {}) + } + + async fn accounts_deactivate_call(self: Arc, request: AccountsDeactivateRequest) -> Result { + let AccountsDeactivateRequest { account_ids } = request; + + self.deactivate_accounts(account_ids.as_deref()).await?; + + Ok(AccountsDeactivateResponse {}) + } + + async fn accounts_discovery_call(self: Arc, request: AccountsDiscoveryRequest) -> Result { + let AccountsDiscoveryRequest { discovery_kind: _, address_scan_extent, account_scan_extent, bip39_passphrase, bip39_mnemonic } = + request; + + let last_account_index_found = + self.scan_bip44_accounts(bip39_mnemonic, bip39_passphrase, address_scan_extent, account_scan_extent).await?; + + Ok(AccountsDiscoveryResponse { last_account_index_found }) + } + + async fn accounts_create_call(self: Arc, request: AccountsCreateRequest) -> Result { + let AccountsCreateRequest { wallet_secret, account_create_args } = request; + + let account = self.create_account(&wallet_secret, account_create_args, true).await?; + let account_descriptor = account.descriptor()?; + + Ok(AccountsCreateResponse { account_descriptor }) + } + + async fn accounts_import_call(self: Arc, _request: AccountsImportRequest) -> Result { + // TODO handle account imports + return Err(Error::NotImplemented); + } + + async fn accounts_get_call(self: Arc, request: AccountsGetRequest) -> Result { + let AccountsGetRequest { account_id } = request; + let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + let descriptor = account.descriptor().unwrap(); + Ok(AccountsGetResponse { descriptor }) + } + + async fn accounts_create_new_address_call( + self: Arc, + request: AccountsCreateNewAddressRequest, + ) -> Result { + let AccountsCreateNewAddressRequest { account_id, kind } = request; + + let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + + let address = match kind { + NewAddressKind::Receive => account.as_derivation_capable()?.new_receive_address().await?, + NewAddressKind::Change => account.as_derivation_capable()?.new_change_address().await?, + }; + + Ok(AccountsCreateNewAddressResponse { address }) + } + + async fn accounts_send_call(self: Arc, request: AccountsSendRequest) -> Result { + let AccountsSendRequest { account_id, asset_type, wallet_secret, payment_secret, destination, priority_fee_sompi, payload } = + request; + + let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + + let abortable = Abortable::new(); + + let (generator_summary, transaction_ids) = account + .send(asset_type, destination, priority_fee_sompi, payload, wallet_secret, payment_secret, &abortable, None) + .await?; + + Ok(AccountsSendResponse { generator_summary, transaction_ids }) + } + + async fn accounts_transfer_call(self: Arc, request: AccountsTransferRequest) -> Result { + let AccountsTransferRequest { + asset_type, + source_account_id, + destination_account_id, + wallet_secret, + payment_secret, + priority_fee_sompi, + transfer_amount_sompi, + } = request; + + let source_account = self.get_account_by_id(&source_account_id).await?.ok_or(Error::AccountNotFound(source_account_id))?; + + let abortable = Abortable::new(); + let (generator_summary, transaction_ids) = source_account + .transfer( + asset_type, + destination_account_id, + transfer_amount_sompi, + priority_fee_sompi.unwrap_or(Fees::SenderPaysAll(0)), + wallet_secret, + payment_secret, + &abortable, + None, + ) + .await?; + + Ok(AccountsTransferResponse { generator_summary, transaction_ids }) + } + + async fn accounts_estimate_call(self: Arc, request: AccountsEstimateRequest) -> Result { + let AccountsEstimateRequest { account_id, tx_action: tx_kind, destination, priority_fee_sompi, payload } = request; + + let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + + // Abort currently running async estimate for the same account if present. The estimate + // call can be invoked continuously by the client/UI. If the estimate call is + // invoked more than once for the same account, the previous estimate call should + // be aborted. The [`Abortable`] is an [`AtomicBool`] that is periodically checked by the + // [`Generator`], resulting in the [`Generator`] halting the estimation process if it + // detects that the [`Abortable`] is set to `true`. This effectively halts the previously + // spawned async task that will return [`Error::Aborted`]. + if let Some(abortable) = self.inner.estimation_abortables.lock().unwrap().get(&account_id) { + abortable.abort(); + } + + let abortable = Abortable::new(); + self.inner.estimation_abortables.lock().unwrap().insert(account_id, abortable.clone()); + let result = account.estimate(tx_kind, destination, priority_fee_sompi, payload, &abortable).await; + self.inner.estimation_abortables.lock().unwrap().remove(&account_id); + + Ok(AccountsEstimateResponse { generator_summary: result? }) + } + + async fn transactions_data_get_call(self: Arc, request: TransactionsDataGetRequest) -> Result { + let TransactionsDataGetRequest { account_id, network_id, filter, start, end } = request; + + if start > end { + return Err(Error::InvalidRange(start, end)); + } + + let binding = Binding::Account(account_id); + let store = self.store().as_transaction_record_store()?; + let TransactionRangeResult { transactions, total } = + store.load_range(&binding, &network_id, filter, start as usize..end as usize).await?; + + Ok(TransactionsDataGetResponse { transactions, total, account_id, start }) + } + + async fn transactions_replace_note_call( + self: Arc, + request: TransactionsReplaceNoteRequest, + ) -> Result { + let TransactionsReplaceNoteRequest { account_id, network_id, transaction_id, note } = request; + + self.store() + .as_transaction_record_store()? + .store_transaction_note(&Binding::Account(account_id), &network_id, transaction_id, note) + .await?; + + Ok(TransactionsReplaceNoteResponse {}) + } + + async fn transactions_replace_metadata_call( + self: Arc, + request: TransactionsReplaceMetadataRequest, + ) -> Result { + let TransactionsReplaceMetadataRequest { account_id, network_id, transaction_id, metadata } = request; + + self.store() + .as_transaction_record_store()? + .store_transaction_metadata(&Binding::Account(account_id), &network_id, transaction_id, metadata) + .await?; + + Ok(TransactionsReplaceMetadataResponse {}) + } + + async fn address_book_enumerate_call( + self: Arc, + _request: AddressBookEnumerateRequest, + ) -> Result { + return Err(Error::NotImplemented); + } +} diff --git a/wallet/core/src/wallet/args.rs b/wallet/core/src/wallet/args.rs new file mode 100644 index 000000000..77cc6521c --- /dev/null +++ b/wallet/core/src/wallet/args.rs @@ -0,0 +1,147 @@ +//! +//! Structs used as various arguments for internal wallet operations. +//! + +use crate::imports::*; +use crate::secret::Secret; +use crate::storage::interface::CreateArgs; +use crate::storage::{Hint, PrvKeyDataId}; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use zeroize::Zeroize; + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct WalletCreateArgs { + pub title: Option, + pub filename: Option, + pub encryption_kind: EncryptionKind, + pub user_hint: Option, + pub overwrite_wallet_storage: bool, +} + +impl WalletCreateArgs { + pub fn new( + title: Option, + filename: Option, + encryption_kind: EncryptionKind, + user_hint: Option, + overwrite_wallet_storage: bool, + ) -> Self { + Self { title, filename, encryption_kind, user_hint, overwrite_wallet_storage } + } +} + +impl From for CreateArgs { + fn from(args: WalletCreateArgs) -> Self { + CreateArgs::new(args.title, args.filename, args.encryption_kind, args.user_hint, args.overwrite_wallet_storage) + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] +pub struct WalletOpenArgs { + /// Return account descriptors + pub account_descriptors: bool, + /// Enable support for legacy accounts + pub legacy_accounts: bool, +} + +impl WalletOpenArgs { + pub fn default_with_legacy_accounts() -> Self { + Self { legacy_accounts: true, ..Default::default() } + } + + pub fn load_account_descriptors(&self) -> bool { + self.account_descriptors || self.legacy_accounts + } + + pub fn is_legacy_only(&self) -> bool { + self.legacy_accounts && !self.account_descriptors + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, BorshSchema)] +pub struct PrvKeyDataCreateArgs { + pub name: Option, + pub payment_secret: Option, + pub mnemonic: String, +} + +impl PrvKeyDataCreateArgs { + pub fn new(name: Option, payment_secret: Option, mnemonic: String) -> Self { + Self { name, payment_secret, mnemonic } + } +} + +impl Zeroize for PrvKeyDataCreateArgs { + fn zeroize(&mut self) { + self.mnemonic.zeroize(); + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct AccountCreateArgsBip32 { + pub account_name: Option, + pub account_index: Option, +} + +impl AccountCreateArgsBip32 { + pub fn new(account_name: Option, account_index: Option) -> Self { + Self { account_name, account_index } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct PrvKeyDataArgs { + pub prv_key_data_id: PrvKeyDataId, + pub payment_secret: Option, +} + +impl PrvKeyDataArgs { + pub fn new(prv_key_data_id: PrvKeyDataId, payment_secret: Option) -> Self { + Self { prv_key_data_id, payment_secret } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum AccountCreateArgs { + Bip32 { + prv_key_data_args: PrvKeyDataArgs, + account_args: AccountCreateArgsBip32, + }, + Legacy { + prv_key_data_id: PrvKeyDataId, + account_name: Option, + }, + Multisig { + prv_key_data_args: Vec, + additional_xpub_keys: Vec, + name: Option, + minimum_signatures: u16, + }, +} + +impl AccountCreateArgs { + pub fn new_bip32( + prv_key_data_id: PrvKeyDataId, + payment_secret: Option, + account_name: Option, + account_index: Option, + ) -> Self { + let prv_key_data_args = PrvKeyDataArgs { prv_key_data_id, payment_secret }; + let account_args = AccountCreateArgsBip32 { account_name, account_index }; + AccountCreateArgs::Bip32 { prv_key_data_args, account_args } + } + + pub fn new_legacy(prv_key_data_id: PrvKeyDataId, account_name: Option) -> Self { + AccountCreateArgs::Legacy { prv_key_data_id, account_name } + } + + pub fn new_multisig( + prv_key_data_args: Vec, + additional_xpub_keys: Vec, + name: Option, + minimum_signatures: u16, + ) -> Self { + AccountCreateArgs::Multisig { prv_key_data_args, additional_xpub_keys, name, minimum_signatures } + } +} diff --git a/wallet/core/src/runtime/maps.rs b/wallet/core/src/wallet/maps.rs similarity index 92% rename from wallet/core/src/runtime/maps.rs rename to wallet/core/src/wallet/maps.rs index bdeca491d..430f54e56 100644 --- a/wallet/core/src/runtime/maps.rs +++ b/wallet/core/src/wallet/maps.rs @@ -1,5 +1,9 @@ +//! +//! Implementation of an [`ActiveAccountMap`] which is a +//! thread-safe map of [`AccountId`] to [`Account`]. +//! + use crate::imports::*; -use crate::runtime::{Account, AccountId}; #[derive(Default, Clone)] pub struct ActiveAccountMap(Arc>>>); diff --git a/wallet/core/src/wallet/mod.rs b/wallet/core/src/wallet/mod.rs new file mode 100644 index 000000000..1b17afed1 --- /dev/null +++ b/wallet/core/src/wallet/mod.rs @@ -0,0 +1,1412 @@ +//! +//! Kash wallet runtime implementation. +//! + +use crate::account::ScanNotifier; +use crate::factory::try_load_account; +use crate::imports::*; +use crate::settings::{SettingsStore, WalletSettings}; +use crate::storage::interface::{OpenArgs, StorageDescriptor}; +use crate::storage::local::interface::LocalStore; +use crate::storage::local::Storage; +use crate::wallet::maps::ActiveAccountMap; +use kash_bip32::Prefix as KeyPrefix; +use kash_bip32::{Language, Mnemonic, WordCount}; +use kash_notify::{ + listener::ListenerId, + scope::{Scope, VirtualDaaScoreChangedScope}, +}; +use kash_rpc_core::notify::mode::NotificationMode; +use kash_wrpc_client::{KashRpcClient, WrpcEncoding}; +use workflow_core::task::spawn; + +pub mod api; +pub mod args; +pub mod maps; +pub use api::*; +pub use args::*; + +#[derive(Clone)] +pub enum WalletBusMessage { + Discovery { record: TransactionRecord }, +} + +pub struct Inner { + active_accounts: ActiveAccountMap, + legacy_accounts: ActiveAccountMap, + listener_id: Mutex>, + task_ctl: DuplexChannel, + selected_account: Mutex>>, + store: Arc, + settings: SettingsStore, + utxo_processor: Arc, + multiplexer: Multiplexer>, + wallet_bus: Channel, + estimation_abortables: Mutex>, +} + +/// `Wallet` data structure +#[derive(Clone)] +pub struct Wallet { + inner: Arc, +} + +impl Wallet { + pub fn local_store() -> Result> { + Ok(Arc::new(LocalStore::try_new(false)?)) + } + + pub fn resident_store() -> Result> { + Ok(Arc::new(LocalStore::try_new(true)?)) + } + + pub fn try_new(storage: Arc, network_id: Option) -> Result { + Wallet::try_with_wrpc(storage, network_id) + } + + pub fn try_with_wrpc(store: Arc, network_id: Option) -> Result { + let rpc_client = + Arc::new(KashRpcClient::new_with_args(WrpcEncoding::Borsh, NotificationMode::MultiListeners, "wrpc://127.0.0.1:17110")?); + let rpc_ctl = rpc_client.ctl().clone(); + let rpc_api: Arc = rpc_client; + let rpc = Rpc::new(rpc_api, rpc_ctl); + Self::try_with_rpc(Some(rpc), store, network_id) + } + + pub fn try_with_rpc(rpc: Option, store: Arc, network_id: Option) -> Result { + let multiplexer = Multiplexer::>::new(); + let wallet_bus = Channel::unbounded(); + let utxo_processor = + Arc::new(UtxoProcessor::new(rpc.clone(), network_id, Some(multiplexer.clone()), Some(wallet_bus.clone()))); + + let wallet = Wallet { + inner: Arc::new(Inner { + multiplexer, + store, + active_accounts: ActiveAccountMap::default(), + legacy_accounts: ActiveAccountMap::default(), + listener_id: Mutex::new(None), + task_ctl: DuplexChannel::oneshot(), + selected_account: Mutex::new(None), + settings: SettingsStore::new_with_storage(Storage::default_settings_store()), + utxo_processor: utxo_processor.clone(), + wallet_bus, + estimation_abortables: Mutex::new(HashMap::new()), + }), + }; + + Ok(wallet) + } + + pub fn inner(&self) -> &Arc { + &self.inner + } + + pub fn is_resident(&self) -> Result { + Ok(self.store().location()? == StorageDescriptor::Resident) + } + + pub fn utxo_processor(&self) -> &Arc { + &self.inner.utxo_processor + } + + pub fn descriptor(&self) -> Option { + self.store().descriptor() + } + + pub fn store(&self) -> &Arc { + &self.inner.store + } + + pub fn active_accounts(&self) -> &ActiveAccountMap { + &self.inner.active_accounts + } + pub fn legacy_accounts(&self) -> &ActiveAccountMap { + &self.inner.legacy_accounts + } + + pub async fn reset(self: &Arc, clear_legacy_cache: bool) -> Result<()> { + self.utxo_processor().clear().await?; + + self.select(None).await?; + + let accounts = self.active_accounts().collect(); + let futures = accounts.into_iter().map(|account| account.stop()); + join_all(futures).await.into_iter().collect::>>()?; + + if clear_legacy_cache { + self.legacy_accounts().clear(); + } + + Ok(()) + } + + pub async fn reload(self: &Arc) -> Result<()> { + if self.is_open() { + // similar to reset(), but effectively reboots the wallet + + let accounts = self.active_accounts().collect(); + let account_descriptors = Some(accounts.iter().map(|account| account.descriptor()).collect::>>()?); + let wallet_descriptor = self.store().descriptor(); + + let futures = accounts.iter().map(|account| account.clone().stop()); + join_all(futures).await.into_iter().collect::>>()?; + + self.utxo_processor().clear().await?; + + let futures = accounts.into_iter().map(|account| account.start()); + join_all(futures).await.into_iter().collect::>>()?; + + self.notify(Events::WalletReload { wallet_descriptor, account_descriptors }).await?; + } + + Ok(()) + } + + pub async fn close(self: &Arc) -> Result<()> { + if self.is_open() { + self.reset(true).await?; + self.store().close().await?; + self.notify(Events::WalletClose).await?; + } + + Ok(()) + } + + cfg_if! { + if #[cfg(not(feature = "multi-user"))] { + + fn default_active_account(&self) -> Option> { + self.active_accounts().first() + } + + /// For end-user wallets only - selects an account only if there + /// is only a single account currently active in the wallet. + /// Can be used to automatically select the default account. + pub async fn autoselect_default_account_if_single(self: &Arc) -> Result<()> { + if self.active_accounts().len() == 1 { + self.select(self.default_active_account().as_ref()).await?; + } + Ok(()) + } + + /// Select an account as 'active'. Supply `None` to remove active selection. + pub async fn select(self: &Arc, account: Option<&Arc>) -> Result<()> { + *self.inner.selected_account.lock().unwrap() = account.cloned(); + if let Some(account) = account { + // log_info!("selecting account: {}", account.name_or_id()); + account.clone().start().await?; + self.notify(Events::AccountSelection{ id : Some(*account.id()) }).await?; + } else { + self.notify(Events::AccountSelection{ id : None }).await?; + } + Ok(()) + } + + /// Get currently selected account + pub fn account(&self) -> Result> { + self.inner.selected_account.lock().unwrap().clone().ok_or_else(|| Error::AccountSelection) + } + + + + } + } + + /// Loads a wallet from storage. Accounts are not activated by this call. + async fn open_impl( + self: &Arc, + wallet_secret: &Secret, + filename: Option, + args: WalletOpenArgs, + ) -> Result>> { + let filename = filename.or_else(|| self.settings().get(WalletSettings::Wallet)); + // let name = Some(make_filename(&name, &None)); + + let was_open = self.is_open(); + + self.store().open(wallet_secret, OpenArgs::new(filename)).await?; + let wallet_name = self.store().descriptor(); + + if was_open { + self.notify(Events::WalletClose).await?; + } + + // reset current state only after we have successfully opened another wallet + self.reset(true).await?; + + let accounts: Option>> = if args.load_account_descriptors() { + let stored_accounts = self.inner.store.as_account_store().unwrap().iter(None).await?.try_collect::>().await?; + let stored_accounts = if !args.is_legacy_only() { + stored_accounts + } else { + stored_accounts + .into_iter() + .filter(|(account_storage, _)| account_storage.kind.as_ref() == LEGACY_ACCOUNT_KIND) + .collect::>() + }; + Some( + futures::stream::iter(stored_accounts.into_iter()) + .then(|(account, meta)| try_load_account(self, account, meta)) + .try_collect::>() + // .try_collect::>>() + .await?, + ) + } else { + None + }; + + let account_descriptors = accounts + .as_ref() + .map(|accounts| accounts.iter().map(|account| account.descriptor()).collect::>>()) + .transpose()?; + + if let Some(accounts) = accounts { + for account in accounts.into_iter() { + if let Ok(legacy_account) = account.clone().as_legacy_account() { + self.legacy_accounts().insert(account); + legacy_account.create_private_context(wallet_secret, None, None).await?; + } + } + } + + self.notify(Events::WalletOpen { wallet_descriptor: wallet_name, account_descriptors: account_descriptors.clone() }).await?; + + let hint = self.store().get_user_hint().await?; + self.notify(Events::WalletHint { hint }).await?; + + Ok(account_descriptors) + } + + /// Loads a wallet from storage. Accounts are not activated by this call. + pub async fn open( + self: &Arc, + wallet_secret: &Secret, + filename: Option, + args: WalletOpenArgs, + ) -> Result>> { + // This is a wrapper of open_impl() that catches errors and notifies the UI + match self.open_impl(wallet_secret, filename, args).await { + Ok(account_descriptors) => Ok(account_descriptors), + Err(err) => { + self.notify(Events::WalletError { message: err.to_string() }).await?; + Err(err) + } + } + } + + async fn activate_accounts_impl(self: &Arc, account_ids: Option<&[AccountId]>) -> Result<()> { + let stored_accounts = if let Some(ids) = account_ids { + self.inner.store.as_account_store().unwrap().load_multiple(ids).await? + } else { + self.inner.store.as_account_store().unwrap().iter(None).await?.try_collect::>().await? + }; + + let ids = stored_accounts.iter().map(|(account, _)| *account.id()).collect::>(); + + for (account_storage, meta) in stored_accounts.into_iter() { + if account_storage.kind.as_ref() == LEGACY_ACCOUNT_KIND { + let legacy_account = self + .legacy_accounts() + .get(account_storage.id()) + .ok_or_else(|| Error::LegacyAccountNotInitialized)? + .clone() + .as_legacy_account()?; + legacy_account.clone().start().await?; + legacy_account.clear_private_context().await?; + } else { + let account = try_load_account(self, account_storage, meta).await?; + account.clone().start().await?; + } + } + + self.notify(Events::AccountActivation { ids }).await?; + + Ok(()) + } + + /// Activates accounts (performs account address space counts, initializes balance tracking, etc.) + pub async fn activate_accounts(self: &Arc, account_ids: Option<&[AccountId]>) -> Result<()> { + // This is a wrapper of activate_accounts_impl() that catches errors and notifies the UI + if let Err(err) = self.activate_accounts_impl(account_ids).await { + self.notify(Events::WalletError { message: err.to_string() }).await?; + Err(err) + } else { + Ok(()) + } + } + + pub async fn deactivate_accounts(self: &Arc, ids: Option<&[AccountId]>) -> Result<()> { + let (ids, futures) = if let Some(ids) = ids { + let accounts = + ids.iter().map(|id| self.active_accounts().get(id).ok_or(Error::AccountNotFound(*id))).collect::>>()?; + (ids.to_vec(), accounts.into_iter().map(|account| account.stop()).collect::>()) + } else { + self.active_accounts().collect().iter().map(|account| (account.id(), account.clone().stop())).unzip() + }; + + join_all(futures).await.into_iter().collect::>>()?; + self.notify(Events::AccountDeactivation { ids }).await?; + + Ok(()) + } + + pub async fn get_prv_key_data(&self, wallet_secret: &Secret, id: &PrvKeyDataId) -> Result> { + self.inner.store.as_prv_key_data_store()?.load_key_data(wallet_secret, id).await + } + + pub async fn get_prv_key_info(&self, account: &Arc) -> Result>> { + self.inner.store.as_prv_key_data_store()?.load_key_info(account.prv_key_data_id()?).await + } + + pub async fn is_account_key_encrypted(&self, account: &Arc) -> Result> { + Ok(self.get_prv_key_info(account).await?.map(|info| info.is_encrypted())) + } + + pub fn wrpc_client(&self) -> Option> { + self.rpc_api().clone().downcast_arc::().ok() + } + + pub fn rpc_api(&self) -> Arc { + self.utxo_processor().rpc_api() + } + + pub fn rpc_ctl(&self) -> RpcCtl { + self.utxo_processor().rpc_ctl() + } + + pub fn has_rpc(&self) -> bool { + self.utxo_processor().has_rpc() + } + + pub async fn bind_rpc(self: &Arc, rpc: Option) -> Result<()> { + self.utxo_processor().bind_rpc(rpc).await?; + Ok(()) + } + + pub fn multiplexer(&self) -> &Multiplexer> { + &self.inner.multiplexer + } + + pub(crate) fn wallet_bus(&self) -> &Channel { + &self.inner.wallet_bus + } + + pub fn settings(&self) -> &SettingsStore { + &self.inner.settings + } + + pub fn current_daa_score(&self) -> Option { + self.utxo_processor().current_daa_score() + } + + pub async fn load_settings(&self) -> Result<()> { + self.settings().try_load().await?; + + let settings = self.settings(); + + if let Some(network_type) = settings.get(WalletSettings::Network) { + self.set_network_id(network_type).unwrap_or_else(|_| log_error!("Unable to select network type: `{}`", network_type)); + } + + if let Some(url) = settings.get::(WalletSettings::Server) { + if let Some(wrpc_client) = self.wrpc_client() { + wrpc_client.set_url(url.as_str()).unwrap_or_else(|_| log_error!("Unable to set rpc url: `{}`", url)); + } + } + + Ok(()) + } + + // intended for starting async management tasks + pub async fn start(self: &Arc) -> Result<()> { + // self.load_settings().await.unwrap_or_else(|_| log_error!("Unable to load settings, discarding...")); + + // internal event loop + self.start_task().await?; + self.utxo_processor().start().await?; + // rpc services (notifier) + if let Some(rpc_client) = self.wrpc_client() { + rpc_client.start().await?; + } + + Ok(()) + } + + // intended for stopping async management task + pub async fn stop(&self) -> Result<()> { + self.utxo_processor().stop().await?; + self.stop_task().await?; + Ok(()) + } + + pub fn listener_id(&self) -> Result { + self.inner.listener_id.lock().unwrap().ok_or(Error::ListenerId) + } + + pub async fn get_info(&self) -> Result { + let v = self.rpc_api().get_info().await?; + Ok(format!("{v:#?}").replace('\n', "\r\n")) + } + + pub async fn subscribe_daa_score(&self) -> Result<()> { + self.rpc_api().start_notify(self.listener_id()?, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; + Ok(()) + } + + pub async fn unsubscribe_daa_score(&self) -> Result<()> { + self.rpc_api().stop_notify(self.listener_id()?, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; + Ok(()) + } + + pub async fn broadcast(&self) -> Result<()> { + Ok(()) + } + + pub fn set_network_id(&self, network_id: NetworkId) -> Result<()> { + if self.is_connected() { + return Err(Error::NetworkTypeConnected); + } + self.utxo_processor().set_network_id(network_id); + Ok(()) + } + + pub fn network_id(&self) -> Result { + self.utxo_processor().network_id() + } + + pub fn address_prefix(&self) -> Result { + Ok(self.network_id()?.into()) + } + + pub fn default_port(&self) -> Result> { + let network_type = self.network_id()?; + if let Some(wrpc_client) = self.wrpc_client() { + let port = match wrpc_client.encoding() { + WrpcEncoding::Borsh => network_type.default_borsh_rpc_port(), + WrpcEncoding::SerdeJson => network_type.default_json_rpc_port(), + }; + Ok(Some(port)) + } else { + Ok(None) + } + } + + pub async fn create_account( + self: &Arc, + wallet_secret: &Secret, + account_create_args: AccountCreateArgs, + notify: bool, + ) -> Result> { + let account = match account_create_args { + AccountCreateArgs::Bip32 { prv_key_data_args, account_args } => { + let PrvKeyDataArgs { prv_key_data_id, payment_secret } = prv_key_data_args; + self.create_account_bip32(wallet_secret, prv_key_data_id, payment_secret.as_ref(), account_args).await? + } + AccountCreateArgs::Legacy { prv_key_data_id, account_name } => { + self.create_account_legacy(wallet_secret, prv_key_data_id, account_name).await? + } + AccountCreateArgs::Multisig { prv_key_data_args, additional_xpub_keys, name, minimum_signatures } => { + self.create_account_multisig(wallet_secret, prv_key_data_args, additional_xpub_keys, name, minimum_signatures).await? + } + }; + + if notify { + let account_descriptor = account.descriptor()?; + self.notify(Events::AccountCreate { account_descriptor }).await?; + } + + Ok(account) + } + + pub async fn create_account_multisig( + self: &Arc, + wallet_secret: &Secret, + prv_key_data_args: Vec, + mut xpub_keys: Vec, + account_name: Option, + minimum_signatures: u16, + ) -> Result> { + let account_store = self.inner.store.clone().as_account_store()?; + + let account: Arc = if prv_key_data_args.is_not_empty() { + let mut generated_xpubs = Vec::with_capacity(prv_key_data_args.len()); + let mut prv_key_data_ids = Vec::with_capacity(prv_key_data_args.len()); + for prv_key_data_arg in prv_key_data_args.into_iter() { + let PrvKeyDataArgs { prv_key_data_id, payment_secret } = prv_key_data_arg; + let prv_key_data = self + .inner + .store + .as_prv_key_data_store()? + .load_key_data(wallet_secret, &prv_key_data_id) + .await? + .ok_or_else(|| Error::PrivateKeyNotFound(prv_key_data_id))?; + let xpub_key = prv_key_data.create_xpub(payment_secret.as_ref(), MULTISIG_ACCOUNT_KIND.into(), 0).await?; // todo it can be done concurrently + generated_xpubs.push(xpub_key.to_string(Some(KeyPrefix::XPUB))); + prv_key_data_ids.push(prv_key_data_id); + } + + generated_xpubs.sort_unstable(); + xpub_keys.extend_from_slice(generated_xpubs.as_slice()); + xpub_keys.sort_unstable(); + + let min_cosigner_index = + generated_xpubs.first().and_then(|first_generated| xpub_keys.binary_search(first_generated).ok()).map(|v| v as u8); + + let xpub_keys = xpub_keys + .into_iter() + .map(|xpub_key| { + ExtendedPublicKeySecp256k1::from_str(&xpub_key).map_err(|err| Error::InvalidExtendedPublicKey(xpub_key, err)) + }) + .collect::>>()?; + + Arc::new( + multisig::MultiSig::try_new( + self, + account_name, + Arc::new(xpub_keys), + Some(Arc::new(prv_key_data_ids)), + min_cosigner_index, + minimum_signatures, + false, + ) + .await?, + ) + } else { + let xpub_keys = xpub_keys + .into_iter() + .map(|xpub_key| { + ExtendedPublicKeySecp256k1::from_str(&xpub_key).map_err(|err| Error::InvalidExtendedPublicKey(xpub_key, err)) + }) + .collect::>>()?; + + Arc::new( + multisig::MultiSig::try_new(self, account_name, Arc::new(xpub_keys), None, None, minimum_signatures, false).await?, + ) + }; + + if account_store.load_single(account.id()).await?.is_some() { + return Err(Error::AccountAlreadyExists(*account.id())); + } + + self.inner.store.clone().as_account_store()?.store_single(&account.to_storage()?, None).await?; + self.inner.store.commit(wallet_secret).await?; + + Ok(account) + } + + pub async fn create_account_bip32( + self: &Arc, + wallet_secret: &Secret, + prv_key_data_id: PrvKeyDataId, + payment_secret: Option<&Secret>, + account_args: AccountCreateArgsBip32, + ) -> Result> { + let account_store = self.inner.store.clone().as_account_store()?; + + let prv_key_data = self + .inner + .store + .as_prv_key_data_store()? + .load_key_data(wallet_secret, &prv_key_data_id) + .await? + .ok_or_else(|| Error::PrivateKeyNotFound(prv_key_data_id))?; + + let AccountCreateArgsBip32 { account_name, account_index } = account_args; + + let account_index = if let Some(account_index) = account_index { + account_index + } else { + account_store.clone().len(Some(prv_key_data_id)).await? as u64 + }; + + let xpub_key = prv_key_data.create_xpub(payment_secret, BIP32_ACCOUNT_KIND.into(), account_index).await?; + let xpub_keys = Arc::new(vec![xpub_key]); + + let account: Arc = + Arc::new(bip32::Bip32::try_new(self, account_name, prv_key_data.id, account_index, xpub_keys, false).await?); + + if account_store.load_single(account.id()).await?.is_some() { + return Err(Error::AccountAlreadyExists(*account.id())); + } + + self.inner.store.clone().as_account_store()?.store_single(&account.to_storage()?, None).await?; + self.inner.store.commit(wallet_secret).await?; + + Ok(account) + } + + async fn create_account_legacy( + self: &Arc, + wallet_secret: &Secret, + prv_key_data_id: PrvKeyDataId, + account_name: Option, + ) -> Result> { + let account_store = self.inner.store.clone().as_account_store()?; + + let prv_key_data = self + .inner + .store + .as_prv_key_data_store()? + .load_key_data(wallet_secret, &prv_key_data_id) + .await? + .ok_or_else(|| Error::PrivateKeyNotFound(prv_key_data_id))?; + + let account: Arc = Arc::new(legacy::Legacy::try_new(self, account_name, prv_key_data.id).await?); + + if account_store.load_single(account.id()).await?.is_some() { + return Err(Error::AccountAlreadyExists(*account.id())); + } + + self.inner.store.clone().as_account_store()?.store_single(&account.to_storage()?, None).await?; + self.inner.store.commit(wallet_secret).await?; + + Ok(account) + } + + pub async fn create_wallet( + self: &Arc, + wallet_secret: &Secret, + args: WalletCreateArgs, + ) -> Result<(WalletDescriptor, StorageDescriptor)> { + self.close().await?; + + let wallet_descriptor = self.inner.store.create(wallet_secret, args.into()).await?; + let storage_descriptor = self.inner.store.location()?; + self.inner.store.commit(wallet_secret).await?; + + self.notify(Events::WalletCreate { + wallet_descriptor: wallet_descriptor.clone(), + storage_descriptor: storage_descriptor.clone(), + }) + .await?; + + Ok((wallet_descriptor, storage_descriptor)) + } + + pub async fn create_prv_key_data( + self: &Arc, + wallet_secret: &Secret, + prv_key_data_create_args: PrvKeyDataCreateArgs, + ) -> Result { + let mnemonic = Mnemonic::new(prv_key_data_create_args.mnemonic, Language::default())?; + let prv_key_data = PrvKeyData::try_from_mnemonic( + mnemonic.clone(), + prv_key_data_create_args.payment_secret.as_ref(), + self.store().encryption_kind()?, + )?; + let prv_key_data_info = PrvKeyDataInfo::from(prv_key_data.as_ref()); + let prv_key_data_id = prv_key_data.id; + let prv_key_data_store = self.inner.store.as_prv_key_data_store()?; + prv_key_data_store.store(wallet_secret, prv_key_data).await?; + self.inner.store.commit(wallet_secret).await?; + + self.notify(Events::PrvKeyDataCreate { prv_key_data_info }).await?; + + Ok(prv_key_data_id) + } + + pub async fn create_wallet_with_accounts( + self: &Arc, + wallet_secret: &Secret, + wallet_args: WalletCreateArgs, + account_name: Option, + account_kind: Option, + mnemonic_phrase_word_count: WordCount, + payment_secret: Option, + ) -> Result<(WalletDescriptor, StorageDescriptor, Mnemonic, Arc)> { + self.close().await?; + + let encryption_kind = wallet_args.encryption_kind; + let wallet_descriptor = self.inner.store.create(wallet_secret, wallet_args.into()).await?; + let storage_descriptor = self.inner.store.location()?; + let mnemonic = Mnemonic::random(mnemonic_phrase_word_count, Default::default())?; + let account_index = 0; + let prv_key_data = PrvKeyData::try_from_mnemonic(mnemonic.clone(), payment_secret.as_ref(), encryption_kind)?; + let xpub_key = prv_key_data + .create_xpub(payment_secret.as_ref(), account_kind.unwrap_or(BIP32_ACCOUNT_KIND.into()), account_index) + .await?; + let xpub_keys = Arc::new(vec![xpub_key]); + + let account: Arc = + Arc::new(bip32::Bip32::try_new(self, account_name, prv_key_data.id, account_index, xpub_keys, false).await?); + + let prv_key_data_store = self.inner.store.as_prv_key_data_store()?; + prv_key_data_store.store(wallet_secret, prv_key_data).await?; + self.inner.store.clone().as_account_store()?.store_single(&account.to_storage()?, None).await?; + self.inner.store.commit(wallet_secret).await?; + + self.select(Some(&account)).await?; + Ok((wallet_descriptor, storage_descriptor, mnemonic, account)) + } + + pub async fn get_account_by_id(self: &Arc, account_id: &AccountId) -> Result>> { + if let Some(account) = self.active_accounts().get(account_id) { + Ok(Some(account.clone())) + } else { + let account_storage = self.inner.store.as_account_store()?; + let stored = account_storage.load_single(account_id).await?; + if let Some((stored_account, stored_metadata)) = stored { + let account = try_load_account(self, stored_account, stored_metadata).await?; + Ok(Some(account)) + } else { + Ok(None) + } + } + } + + pub async fn notify(&self, event: Events) -> Result<()> { + self.multiplexer() + .try_broadcast(Box::new(event)) + .map_err(|_| Error::Custom("multiplexer channel error during update_balance".to_string()))?; + Ok(()) + } + + pub fn is_synced(&self) -> bool { + self.utxo_processor().is_synced() + } + + pub fn is_connected(&self) -> bool { + self.utxo_processor().is_connected() + } + + pub(crate) async fn handle_discovery(&self, record: TransactionRecord) -> Result<()> { + let transaction_store = self.store().as_transaction_record_store()?; + + if let Err(_err) = transaction_store.load_single(record.binding(), &self.network_id()?, record.id()).await { + let transaction_daa_score = record.block_daa_score(); + match self.rpc_api().get_daa_score_timestamp_estimate(vec![transaction_daa_score]).await { + Ok(timestamps) => { + if let Some(timestamp) = timestamps.first() { + let mut record = record.clone(); + record.set_unixtime(*timestamp); + + transaction_store.store(&[&record]).await?; + + self.notify(Events::Discovery { record }).await?; + } else { + self.notify(Events::Error { + message: format!( + "Unable to obtain DAA to unixtime for DAA {transaction_daa_score}, timestamp data is empty" + ), + }) + .await?; + } + } + Err(err) => { + self.notify(Events::Error { message: format!("Unable to resolve DAA to unixtime: {err}") }).await?; + } + } + } + + Ok(()) + } + + async fn handle_wallet_bus(self: &Arc, message: WalletBusMessage) -> Result<()> { + match message { + WalletBusMessage::Discovery { record } => { + self.handle_discovery(record).await?; + } + } + Ok(()) + } + + async fn handle_event(self: &Arc, event: Box) -> Result<()> { + match &*event { + Events::Pending { record } | Events::Maturity { record } | Events::Reorg { record } => { + if !record.is_change() { + self.store().as_transaction_record_store()?.store(&[record]).await?; + } + } + + Events::SyncState { sync_state: _ } => { + // if sync_state.is_synced() && self.is_open() { + // self.reload().await?; + // } + } + _ => {} + } + + Ok(()) + } + + async fn start_task(self: &Arc) -> Result<()> { + let this = self.clone(); + let task_ctl_receiver = self.inner.task_ctl.request.receiver.clone(); + let task_ctl_sender = self.inner.task_ctl.response.sender.clone(); + let events = self.multiplexer().channel(); + let wallet_bus_receiver = self.wallet_bus().receiver.clone(); + + spawn(async move { + loop { + select! { + _ = task_ctl_receiver.recv().fuse() => { + break; + }, + + msg = events.receiver.recv().fuse() => { + match msg { + Ok(event) => { + this.handle_event(event).await.unwrap_or_else(|e| log_error!("Wallet::handle_event() error: {}", e)); + }, + Err(err) => { + log_error!("Wallet: error while receiving multiplexer message: {err}"); + log_error!("Suspending Wallet processing..."); + + break; + } + } + }, + + msg = wallet_bus_receiver.recv().fuse() => { + match msg { + Ok(message) => { + this.handle_wallet_bus(message).await.unwrap_or_else(|e| log_error!("Wallet::handle_wallet_bus() error: {}", e)); + }, + Err(err) => { + log_error!("Wallet: error while receiving wallet bus message: {err}"); + log_error!("Suspending Wallet processing..."); + + break; + } + } + } + } + } + + task_ctl_sender.send(()).await.unwrap(); + }); + Ok(()) + } + + async fn stop_task(&self) -> Result<()> { + self.inner.task_ctl.signal(()).await.expect("Wallet::stop_task() `signal` error"); + Ok(()) + } + + pub fn is_open(&self) -> bool { + self.inner.store.is_open() + } + + pub fn location(&self) -> Result { + self.inner.store.location() + } + + pub async fn exists(&self, name: Option<&str>) -> Result { + self.inner.store.exists(name).await + } + + pub async fn keys(&self) -> Result>>> { + self.inner.store.as_prv_key_data_store()?.iter().await + } + + pub async fn find_accounts_by_name_or_id(&self, pat: &str) -> Result>> { + let active_accounts = self.active_accounts().inner().values().cloned().collect::>(); + let matches = active_accounts + .into_iter() + .filter(|account| { + account.name().map(|name| name.starts_with(pat)).unwrap_or(false) || account.id().to_hex().starts_with(pat) + }) + .collect::>(); + Ok(matches) + } + + pub async fn accounts(self: &Arc, filter: Option) -> Result>>> { + let iter = self.inner.store.as_account_store().unwrap().iter(filter).await.unwrap(); + let wallet = self.clone(); + + let stream = iter.then(move |stored| { + let wallet = wallet.clone(); + + async move { + let (stored_account, stored_metadata) = stored.unwrap(); + if let Some(account) = wallet.legacy_accounts().get(&stored_account.id) { + if !wallet.active_accounts().contains(account.id()) { + account.clone().start().await?; + } + Ok(account) + } else if let Some(account) = wallet.active_accounts().get(&stored_account.id) { + Ok(account) + } else { + let account = try_load_account(&wallet, stored_account, stored_metadata).await?; + account.clone().start().await?; + Ok(account) + } + } + }); + + Ok(Box::pin(stream)) + } + + // TODO - remove these comments (these functions are a part of + // a major refactoring and are temporarily kept here for reference) + + // pub async fn initialize_legacy_accounts( + // self: &Arc, + // filter: Option, + // secret: Secret, + // ) -> Result<()> { + // let mut iter = self.inner.store.as_account_store().unwrap().iter(filter).await.unwrap(); + // let wallet = self.clone(); + + // while let Some((stored_account, stored_metadata)) = iter.try_next().await? { + // if matches!(stored_account.data, AccountData::Legacy { .. }) { + + // let account = try_from_storage(&wallet, stored_account, stored_metadata).await?; + + // account.clone().initialize_private_data(secret.clone(), None, None).await?; + // wallet.legacy_accounts().insert(account.clone()); + // // account.clone().start().await?; + + // // if is_legacy { + // // let derivation = account.clone().as_derivation_capable()?.derivation(); + // // let m = derivation.receive_address_manager(); + // // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // // let m = derivation.change_address_manager(); + // // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + + // // - TODO - consider two-phase approach + // // account.clone().clear_private_data().await?; + // // } + // } + // } + + // Ok(()) + + // // let stream = iter.then(move |stored| { + // let wallet = wallet.clone(); + // let secret = secret.clone(); + + // // async move { + // let (stored_account, stored_metadata) = stored.unwrap(); + // // if let Some(account) = wallet.active_accounts().get(&stored_account.id) { + // // Ok(account) + // // } else { + // if matches!(stored_account.data, AccountData::Legacy { .. }) { + + // let account = try_from_storage(&wallet, stored_account, stored_metadata).await?; + + // // if is_legacy { + // account.clone().initialize_private_data(secret, None, None).await?; + // wallet.legacy_accounts().insert(account.clone()); + // // } + + // // account.clone().start().await?; + + // // if is_legacy { + // let derivation = account.clone().as_derivation_capable()?.derivation(); + // let m = derivation.receive_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // let m = derivation.change_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // account.clone().clear_private_data().await?; + // // } + // } + + // Ok(account) + // } + // } + // }); + // Ok(Box::pin(stream)) + // } + + // pub async fn initialize_accounts( + // self: &Arc, + // filter: Option, + // secret: Secret, + // ) -> Result>>> { + // let iter = self.inner.store.as_account_store().unwrap().iter(filter).await.unwrap(); + // let wallet = self.clone(); + + // let stream = iter.then(move |stored| { + // let wallet = wallet.clone(); + // let secret = secret.clone(); + + // async move { + // let (stored_account, stored_metadata) = stored.unwrap(); + // if let Some(account) = wallet.active_accounts().get(&stored_account.id) { + // Ok(account) + // } else { + // let is_legacy = matches!(stored_account.data, AccountData::Legacy { .. }); + // let account = try_from_storage(&wallet, stored_account, stored_metadata).await?; + + // if is_legacy { + // account.clone().initialize_private_data(secret, None, None).await?; + // wallet.legacy_accounts().insert(account.clone()); + // } + + // // account.clone().start().await?; + + // if is_legacy { + // let derivation = account.clone().as_derivation_capable()?.derivation(); + // let m = derivation.receive_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // let m = derivation.change_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // account.clone().clear_private_data().await?; + // } + + // Ok(account) + // } + // } + // }); + + // Ok(Box::pin(stream)) + // } + + pub async fn import_legacy_keydata( + self: &Arc, + import_secret: &Secret, + wallet_secret: &Secret, + payment_secret: Option<&Secret>, + notifier: Option, + ) -> Result> { + use crate::derivation::gen0::import::load_v0_keydata; + + let notifier = notifier.as_ref(); + let keydata = load_v0_keydata(import_secret).await?; + + let mnemonic = Mnemonic::new(keydata.mnemonic.trim(), Language::English)?; + let prv_key_data = PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret, self.store().encryption_kind()?)?; + let prv_key_data_store = self.inner.store.as_prv_key_data_store()?; + if prv_key_data_store.load_key_data(wallet_secret, &prv_key_data.id).await?.is_some() { + return Err(Error::PrivateKeyAlreadyExists(prv_key_data.id)); + } + + let account: Arc = Arc::new(legacy::Legacy::try_new(self, None, prv_key_data.id).await?); + + // activate account (add it to wallet active account list) + self.active_accounts().insert(account.clone().as_dyn_arc()); + self.legacy_accounts().insert(account.clone().as_dyn_arc()); + + // store private key and account + self.inner.store.batch().await?; + prv_key_data_store.store(wallet_secret, prv_key_data).await?; + self.inner.store.clone().as_account_store()?.store_single(&account.to_storage()?, None).await?; + self.inner.store.flush(wallet_secret).await?; + + let legacy_account = account.clone().as_legacy_account()?; + legacy_account.create_private_context(wallet_secret, payment_secret, None).await?; + // account.clone().initialize_private_data(wallet_secret, payment_secret, None).await?; + + if self.is_connected() { + if let Some(notifier) = notifier { + notifier(0, 0, 0, None); + } + account.clone().scan(Some(100), Some(5000)).await?; + } + + // let derivation = account.clone().as_derivation_capable()?.derivation(); + // let m = derivation.receive_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // let m = derivation.change_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // account.clone().clear_private_data().await?; + + legacy_account.clear_private_context().await?; + + Ok(account) + } + + pub async fn import_gen1_keydata(self: &Arc, secret: Secret) -> Result<()> { + use crate::derivation::gen1::import::load_v1_keydata; + + let _keydata = load_v1_keydata(&secret).await?; + + Ok(()) + } + + pub async fn import_with_mnemonic( + self: &Arc, + wallet_secret: &Secret, + payment_secret: Option<&Secret>, + mnemonic: Mnemonic, + account_kind: AccountKind, + ) -> Result> { + let prv_key_data = storage::PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret, self.store().encryption_kind()?)?; + let prv_key_data_store = self.store().as_prv_key_data_store()?; + if prv_key_data_store.load_key_data(wallet_secret, &prv_key_data.id).await?.is_some() { + return Err(Error::PrivateKeyAlreadyExists(prv_key_data.id)); + } + // let mut is_legacy = false; + let account: Arc = match account_kind.as_ref() { + BIP32_ACCOUNT_KIND => { + let account_index = 0; + let xpub_key = prv_key_data.create_xpub(payment_secret, account_kind, account_index).await?; + let xpub_keys = Arc::new(vec![xpub_key]); + let ecdsa = false; + // --- + Arc::new(bip32::Bip32::try_new(self, None, prv_key_data.id, account_index, xpub_keys, ecdsa).await?) + } + LEGACY_ACCOUNT_KIND => Arc::new(legacy::Legacy::try_new(self, None, prv_key_data.id).await?), + _ => { + return Err(Error::AccountKindFeature); + } + }; + + let account_store = self.inner.store.as_account_store()?; + self.inner.store.batch().await?; + account_store.store_single(&account.to_storage()?, None).await?; + self.inner.store.flush(wallet_secret).await?; + + if let Ok(legacy_account) = account.clone().as_legacy_account() { + self.legacy_accounts().insert(account.clone()); + legacy_account.create_private_context(wallet_secret, None, None).await?; + legacy_account.clone().start().await?; + legacy_account.clear_private_context().await?; + } else { + account.clone().start().await?; + } + + // if is_legacy { + // account.clone().initialize_private_data(wallet_secret, None, None).await?; + // self.legacy_accounts().insert(account.clone()); + // } + // account.clone().start().await?; + // if is_legacy { + // let derivation = account.clone().as_derivation_capable()?.derivation(); + // let m = derivation.receive_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // let m = derivation.change_address_manager(); + // m.get_range(0..(m.index() + CACHE_ADDRESS_OFFSET))?; + // account.clone().clear_private_data().await?; + // } + + Ok(account) + } + + /// Perform a "2d" scan of account derivations while scanning addresses + /// in each account (UTXOs up to `address_scan_extent` address derivation). + /// Report back the last account index that has UTXOs. The scan is performed + /// until we have encountered at least `account_scan_extent` of empty + /// accounts. + pub async fn scan_bip44_accounts( + self: &Arc, + bip39_mnemonic: String, + bip39_passphrase: Option, + address_scan_extent: u32, + account_scan_extent: u32, + ) -> Result { + let mnemonic = Mnemonic::new(bip39_mnemonic.as_str(), Language::English)?; + // TODO @aspect - this is not efficient, we need to scan without encrypting prv_key_data + let prv_key_data = + storage::PrvKeyData::try_new_from_mnemonic(mnemonic, bip39_passphrase.as_ref(), EncryptionKind::XChaCha20Poly1305)?; + + let mut last_account_index = 0; + let mut account_index = 0; + + while account_index < last_account_index + account_scan_extent { + let xpub_key = + prv_key_data.create_xpub(bip39_passphrase.as_ref(), BIP32_ACCOUNT_KIND.into(), account_index as u64).await?; + let xpub_keys = Arc::new(vec![xpub_key]); + let ecdsa = false; + // --- + + let addresses = bip32::Bip32::try_new(self, None, prv_key_data.id, account_index as u64, xpub_keys, ecdsa) + .await? + .get_address_range_for_scan(0..address_scan_extent)?; + if self.rpc_api().get_utxos_by_addresses(addresses).await?.is_not_empty() { + last_account_index = account_index; + } + account_index += 1; + } + + Ok(last_account_index) + } + + pub async fn import_multisig_with_mnemonic( + self: &Arc, + wallet_secret: &Secret, + mnemonics_secrets: Vec<(Mnemonic, Option)>, + minimum_signatures: u16, + mut additional_xpub_keys: Vec, + ) -> Result> { + let mut generated_xpubs = Vec::with_capacity(mnemonics_secrets.len()); + let mut prv_key_data_ids = Vec::with_capacity(mnemonics_secrets.len()); + let prv_key_data_store = self.store().as_prv_key_data_store()?; + + for (mnemonic, payment_secret) in mnemonics_secrets { + let prv_key_data = + storage::PrvKeyData::try_new_from_mnemonic(mnemonic, payment_secret.as_ref(), self.store().encryption_kind()?)?; + if prv_key_data_store.load_key_data(wallet_secret, &prv_key_data.id).await?.is_some() { + return Err(Error::PrivateKeyAlreadyExists(prv_key_data.id)); + } + let xpub_key = prv_key_data.create_xpub(payment_secret.as_ref(), MULTISIG_ACCOUNT_KIND.into(), 0).await?; // todo it can be done concurrently + generated_xpubs.push(xpub_key.to_string(Some(KeyPrefix::XPUB))); + prv_key_data_ids.push(prv_key_data.id); + prv_key_data_store.store(wallet_secret, prv_key_data).await?; + } + + generated_xpubs.sort_unstable(); + additional_xpub_keys.extend_from_slice(generated_xpubs.as_slice()); + let mut xpub_keys = additional_xpub_keys; + xpub_keys.sort_unstable(); + + let min_cosigner_index = + generated_xpubs.first().and_then(|first_generated| xpub_keys.binary_search(first_generated).ok()).map(|v| v as u8); + + let xpub_keys = xpub_keys + .into_iter() + .map(|xpub_key| { + ExtendedPublicKeySecp256k1::from_str(&xpub_key).map_err(|err| Error::InvalidExtendedPublicKey(xpub_key, err)) + }) + .collect::>>()?; + + let account: Arc = Arc::new( + multisig::MultiSig::try_new( + self, + None, + Arc::new(xpub_keys), + Some(Arc::new(prv_key_data_ids)), + min_cosigner_index, + minimum_signatures, + false, + ) + .await?, + ); + + self.inner.store.clone().as_account_store()?.store_single(&account.to_storage()?, None).await?; + account.clone().start().await?; + + Ok(account) + } + + async fn rename(&self, title: Option, filename: Option, wallet_secret: &Secret) -> Result<()> { + let store = self.store(); + store.rename(wallet_secret, title.as_deref(), filename.as_deref()).await?; + Ok(()) + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod test { + use std::{str::FromStr, thread::sleep, time}; + + use super::*; + use crate::derivation::gen1; + use crate::utxo::{UtxoContext, UtxoContextBinding, UtxoIterator}; + use kash_addresses::{Address, Prefix, Version}; + use kash_bip32::{ChildNumber, ExtendedPrivateKey, SecretKey}; + use kash_consensus_core::asset_type::AssetType; + use kash_consensus_core::subnets::SUBNETWORK_ID_NATIVE; + use kash_consensus_core::tx::TransactionAction; + use kash_consensus_wasm::{sign_transaction, SignableTransaction, Transaction, TransactionInput, TransactionOutput}; + use kash_txscript::pay_to_address_script; + use workflow_rpc::client::ConnectOptions; + + async fn create_utxos_context_with_addresses( + rpc: Arc, + addresses: Vec
, + current_daa_score: u64, + core: &UtxoProcessor, + ) -> Result { + let utxos = rpc.get_utxos_by_addresses(addresses).await?; + let utxo_context = UtxoContext::new(core, UtxoContextBinding::default()); + let entries = utxos.into_iter().map(|entry| entry.into()).collect::>(); + for entry in entries.into_iter() { + utxo_context.insert(entry, current_daa_score, false).await?; + } + Ok(utxo_context) + } + + #[allow(dead_code)] + // #[tokio::test] + async fn wallet_test() -> Result<()> { + println!("Creating wallet..."); + let resident_store = Wallet::resident_store()?; + let wallet = Arc::new(Wallet::try_new(resident_store, None)?); + + let rpc_api = wallet.rpc_api(); + let utxo_processor = wallet.utxo_processor(); + + let wrpc_client = wallet.wrpc_client().expect("Unable to obtain wRPC client"); + + let info = rpc_api.get_block_dag_info().await?; + let current_daa_score = info.virtual_daa_score; + + let _connect_result = wrpc_client.connect(ConnectOptions::fallback()).await; + //println!("connect_result: {_connect_result:?}"); + + let _result = wallet.start().await; + //println!("wallet.task(): {_result:?}"); + let result = wallet.get_info().await; + println!("wallet.get_info(): {result:#?}"); + + let address = Address::try_from("kashtest:qz7ulu4c25dh7fzec9zjyrmlhnkzrg4wmf89q7gzr3gfrsj3uz6xjceef60sd")?; + + let utxo_context = + self::create_utxos_context_with_addresses(rpc_api.clone(), vec![address.clone()], current_daa_score, utxo_processor) + .await?; + + let utxo_set_balance = utxo_context.calculate_balance().await; + println!("get_utxos_by_addresses: {utxo_set_balance:?}"); + + let to_address = Address::try_from("kashtest:qpakxqlesqywgkq7rg4wyhjd93kmw7trkl3gpa3vd5flyt59a43yyn8vu0w8c")?; + let mut iter = UtxoIterator::new(&utxo_context); + let utxo = iter.next().unwrap(); + let utxo = (*utxo.utxo).clone(); + let selected_entries = vec![utxo]; + + let entries = &selected_entries; + + let inputs = selected_entries + .iter() + .enumerate() + .map(|(sequence, utxo)| TransactionInput::new(utxo.outpoint.clone(), vec![], sequence as u64, 0)) + .collect::>(); + + let tx = Transaction::new( + 0, + inputs, + vec![TransactionOutput::new(1000, &pay_to_address_script(&to_address), AssetType::KSH)], + TransactionAction::TransferKSH, + 0, + SUBNETWORK_ID_NATIVE, + 0, + vec![], + )?; + + let mtx = SignableTransaction::new(tx, (*entries).clone().into()); + + let derivation_path = + gen1::WalletDerivationManager::build_derivate_path(false, 0, None, Some(kash_bip32::AddressType::Receive))?; + + let xprv = "kprv5y2qurMHCsXYrNfU3GCihuwG3vMqFji7PZXajMEqyBkNh9UZUJgoHYBLTKu1eM4MvUtomcXPQ3Sw9HZ5ebbM4byoUciHo1zrPJBQfqpLorQ"; + + let xkey = ExtendedPrivateKey::::from_str(xprv)?.derive_path(derivation_path)?; + + let xkey = xkey.derive_child(ChildNumber::new(0, false)?)?; + + // address test + let address_test = Address::new(Prefix::Testnet, Version::PubKey, &xkey.public_key().to_bytes()[1..]); + let address_str: String = address_test.clone().into(); + assert_eq!(address, address_test, "Addresses don't match"); + println!("address: {address_str}"); + + let private_keys = vec![xkey.to_bytes()]; + + println!("mtx: {mtx:?}"); + + let mtx = sign_transaction(mtx, private_keys, true)?; + + let utxo_context = + self::create_utxos_context_with_addresses(rpc_api.clone(), vec![to_address.clone()], current_daa_score, utxo_processor) + .await?; + let to_balance = utxo_context.calculate_balance().await; + println!("to address balance before tx submit: {to_balance:?}"); + + let result = rpc_api.submit_transaction(mtx.into(), false).await?; + + println!("tx submit result, {:?}", result); + println!("sleep for 5s..."); + sleep(time::Duration::from_millis(5000)); + let utxo_context = + self::create_utxos_context_with_addresses(rpc_api.clone(), vec![to_address.clone()], current_daa_score, utxo_processor) + .await?; + let to_balance = utxo_context.calculate_balance().await; + println!("to address balance after tx submit: {to_balance:?}"); + + Ok(()) + } +} diff --git a/wallet/core/src/wasm/balance.rs b/wallet/core/src/wasm/balance.rs index f74d1b29b..4b9763ae7 100644 --- a/wallet/core/src/wasm/balance.rs +++ b/wallet/core/src/wasm/balance.rs @@ -1,6 +1,6 @@ use crate::imports::*; use crate::result::Result; -use crate::runtime::balance as native; +use crate::utxo::balance as native; #[wasm_bindgen] pub struct Balance { diff --git a/wallet/core/src/wasm/mod.rs b/wallet/core/src/wasm/mod.rs index bae0e3bc0..cdd359824 100644 --- a/wallet/core/src/wasm/mod.rs +++ b/wallet/core/src/wasm/mod.rs @@ -1,3 +1,7 @@ +//! +//! WASM32 bindings for the wallet framework components. +//! + pub mod balance; pub mod message; pub mod tx; diff --git a/wallet/core/src/wasm/tx/generator/generator.rs b/wallet/core/src/wasm/tx/generator/generator.rs index 9b1e727c6..851515d93 100644 --- a/wallet/core/src/wasm/tx/generator/generator.rs +++ b/wallet/core/src/wasm/tx/generator/generator.rs @@ -1,6 +1,5 @@ use crate::imports::*; use crate::result::Result; -use crate::runtime; use crate::tx::{generator as native, Fees, PaymentDestination, PaymentOutputs}; use crate::utxo::{TryIntoUtxoEntryReferences, UtxoEntryReference}; use crate::wasm::tx::generator::*; @@ -8,17 +7,43 @@ use crate::wasm::wallet::Account; use crate::wasm::UtxoContext; use kash_consensus_core::tx::TransactionAction; +#[wasm_bindgen(typescript_custom_section)] +const IGeneratorSettingsObject: &'static str = r#" +interface IGeneratorSettingsObject { + outputs: PaymentOutputs | Array>; + changeAddress: Address | string; + priorityFee: bigint; + utxoEntries: Array; + sigOpCount: Uint8Array; + minimumSignatures: Uint16Array; + payload: Uint8Array | string; +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const IGeneratorSettingsObject: &'static str = r#" +interface IGeneratorSettingsObject { + outputs: PaymentOutputs | Array>; + changeAddress: Address | string; + priorityFee: bigint; + utxoEntries: Array; + sigOpCount: Uint8Array; + minimumSignatures: Uint16Array; + payload: Uint8Array | string; +} +"#; + #[wasm_bindgen] extern "C" { /// Supports the following properties (all values must be supplied in SOMPI): - /// - `outputs`: instance of [`PaymentOutputs`] or `[ [amount, address], [amount, address], ... ]` + /// - `outputs`: instance of [`PaymentOutputs`] or `[ [address, amount], [address, amount], ... ]` /// - `changeAddress`: [`Address`] or String representation of an address - /// - `priorityFee`: BigInt or [`Fees`] + /// - `priorityFee`: BigInt /// - `utxoEntries`: Array of [`UtxoEntryReference`] - /// - `sigOpCount`: [`u8`] - /// - `minimumSignatures`: [`u16`] + /// - `sigOpCount`: `u8` + /// - `minimumSignatures`: `u16` /// - `payload`: [`Uint8Array`] or hex String representation of a payload - #[wasm_bindgen(extends = Object, is_type_of = Array::is_array, typescript_type = "PrivateKey[]")] + #[wasm_bindgen(extends = Object, typescript_type = "IGeneratorSettingsObject")] #[derive(Clone, Debug, PartialEq, Eq)] pub type GeneratorSettingsObject; } @@ -30,7 +55,7 @@ extern "C" { /// transaction mass, at which point it will produce a compound transaction by forwarding /// all selected UTXO entries to the supplied change address and prepare to start generating /// a new transaction. Such sequence of daisy-chained transactions is known as a "batch". -/// Each compount transaction results in a new UTXO, which is immediately reused in the +/// Each compound transaction results in a new UTXO, which is immediately reused in the /// subsequent transaction. /// /// ```javascript @@ -109,7 +134,7 @@ impl Generator { )? } GeneratorSource::Account(account) => { - let account: Arc = account.into(); + let account: Arc = account.into(); native::GeneratorSettings::try_new_with_account( account, final_transaction_action, diff --git a/wallet/core/src/wasm/tx/generator/pending.rs b/wallet/core/src/wasm/tx/generator/pending.rs index 8a05f394a..fe7e93f6b 100644 --- a/wallet/core/src/wasm/tx/generator/pending.rs +++ b/wallet/core/src/wasm/tx/generator/pending.rs @@ -37,12 +37,12 @@ impl PendingTransaction { #[wasm_bindgen(getter, js_name = aggregateInputAmount)] pub fn aggregate_input_value(&self) -> BigInt { - BigInt::from(self.inner.input_aggregate_value()) + BigInt::from(self.inner.aggregate_input_value()) } #[wasm_bindgen(getter, js_name = aggregateOutputAmount)] pub fn aggregate_output_value(&self) -> BigInt { - BigInt::from(self.inner.output_aggregate_value()) + BigInt::from(self.inner.aggregate_output_value()) } #[wasm_bindgen(getter, js_name = "type")] @@ -65,7 +65,7 @@ impl PendingTransaction { } /// Sign transaction with supplied [`Array`] or [`PrivateKey`] or an array of - /// raw private key bytes (encoded as [`Uint8Array`] or as hex strings) + /// raw private key bytes (encoded as `Uint8Array` or as hex strings) pub fn sign(&self, js_value: JsValue) -> Result<()> { if let Ok(keys) = js_value.dyn_into::() { let keys = diff --git a/wallet/core/src/wasm/tx/mass.rs b/wallet/core/src/wasm/tx/mass.rs index 208c48a4f..ec89ea725 100644 --- a/wallet/core/src/wasm/tx/mass.rs +++ b/wallet/core/src/wasm/tx/mass.rs @@ -33,7 +33,7 @@ impl MassCalculator { /// if the cost to the network to spend coins is more than 1/3 of the minimum /// transaction relay fee, it is considered dust. /// - /// It is exposed by [MiningManager] for use by transaction generators and wallets. + /// It is exposed by `MiningManager` for use by transaction generators and wallets. #[wasm_bindgen(js_name=isTransactionOutputDust)] pub fn is_transaction_output_dust(transaction_output: JsValue) -> Result { let transaction_output = TransactionOutput::try_from(transaction_output)?; diff --git a/wallet/core/src/wasm/tx/utils.rs b/wallet/core/src/wasm/tx/utils.rs index 871cd3974..b881061d9 100644 --- a/wallet/core/src/wasm/tx/utils.rs +++ b/wallet/core/src/wasm/tx/utils.rs @@ -21,7 +21,7 @@ pub fn create_transaction_js( payload: JsValue, sig_op_count: JsValue, minimum_signatures: JsValue, -) -> crate::Result { +) -> crate::result::Result { let change_address = Address::try_from(change_address)?; let params = get_consensus_params_by_address(&change_address); let mc = MassCalculator::new(params); @@ -75,7 +75,7 @@ pub fn create_transaction_js( /// Creates a set of transactions using transaction [`Generator`]. #[wasm_bindgen(js_name=createTransactions)] -pub async fn create_transactions_js(settings: GeneratorSettingsObject) -> crate::Result { +pub async fn create_transactions_js(settings: GeneratorSettingsObject) -> Result { let generator = Generator::ctor(settings)?; if is_web() { // yield after each generated transaction if operating in the browser @@ -105,7 +105,7 @@ pub async fn create_transactions_js(settings: GeneratorSettingsObject) -> crate: /// Creates a set of transactions using transaction [`Generator`]. #[wasm_bindgen(js_name=estimateTransactions)] -pub async fn estimate_js(settings: GeneratorSettingsObject) -> crate::Result { +pub async fn estimate_js(settings: GeneratorSettingsObject) -> Result { let generator = Generator::ctor(settings)?; if is_web() { // yield after each generated transaction if operating in the browser diff --git a/wallet/core/src/wasm/utxo/processor.rs b/wallet/core/src/wasm/utxo/processor.rs index d2efb899f..f7e9e7d42 100644 --- a/wallet/core/src/wasm/utxo/processor.rs +++ b/wallet/core/src/wasm/utxo/processor.rs @@ -29,7 +29,7 @@ impl UtxoProcessor { let rpc_api: Arc = rpc.client().clone(); let rpc_ctl = rpc.client().rpc_ctl().clone(); let rpc_binding = Rpc::new(rpc_api, rpc_ctl); - let inner = native::UtxoProcessor::new(Some(rpc_binding), Some(network_id), None); + let inner = native::UtxoProcessor::new(Some(rpc_binding), Some(network_id), None, None); let events = EventDispatcher::new(); inner.start().await?; diff --git a/wallet/core/src/wasm/wallet/account.rs b/wallet/core/src/wasm/wallet/account.rs index 3d3629dde..004e48907 100644 --- a/wallet/core/src/wasm/wallet/account.rs +++ b/wallet/core/src/wasm/wallet/account.rs @@ -1,6 +1,6 @@ +use crate::account as native; use crate::imports::*; use crate::result::Result; -use crate::runtime; use crate::secret::Secret; use crate::tx::PaymentOutputs; use crate::wasm::utxo::UtxoContext; @@ -11,13 +11,13 @@ use workflow_wasm::abi::ref_from_abi; #[wasm_bindgen(inspectable)] #[derive(Clone)] pub struct Account { - inner: Arc, + inner: Arc, #[wasm_bindgen(getter_with_clone)] pub context: UtxoContext, } impl Account { - pub async fn try_new(inner: Arc) -> Result { + pub async fn try_new(inner: Arc) -> Result { let context = inner.utxo_context().clone(); Ok(Self { inner, context: context.into() }) } @@ -85,35 +85,11 @@ impl Account { pub async fn send(&self, js_value: JsValue) -> Result { let _args = AccountSendArgs::try_from(js_value)?; - // self.inner.clone().send( - - // self: Arc, - // destination: PaymentDestination, - // priority_fee_sompi: Fees, - // payload: Option>, - // wallet_secret: Secret, - // payment_secret: Option, - // abortable: &Abortable, - // notifier: Option, - - // ).await; - - // self.inner - // .send_v1( - // &args.outputs, - // args.priority_fee_sompi, - // args.include_fees_in_amount, - // args.wallet_secret, - // args.payment_secret, - // &args.abortable, - // ) - // .await?; - todo!() } } -impl From for Arc { +impl From for Arc { fn from(account: Account) -> Self { account.inner } @@ -126,42 +102,11 @@ impl TryFrom for Account { } } -// pub enum IterResult { -// Ok(T), -// Err(E), -// } - -// impl From> for IterResult { -// fn from(result: Result) -> IterResult { -// match result { -// Ok(t) => IterResult::Ok(t), -// Err(e) => IterResult::Err(e), -// } -// } -// } - -// impl From>> for JsValue { -// fn from(account: Result>) -> Self { -// account.map(|account| account.into()) -// } -// } - -// self: Arc, -// destination: PaymentDestination, -// priority_fee_sompi: Fees, -// payload: Option>, -// wallet_secret: Secret, -// payment_secret: Option, -// abortable: &Abortable, -// notifier: Option, - pub struct AccountSendArgs { - // pub destination : PaymentDestination, pub outputs: PaymentOutputs, pub priority_fee_sompi: Option, pub include_fees_in_amount: bool, - // pub utxos: Option>>, pub wallet_secret: Secret, pub payment_secret: Option, pub abortable: Abortable, @@ -190,10 +135,7 @@ impl TryFrom for AccountSendArgs { } } -pub struct AccountCreateArgs { - // rpc: RpcClient, - // network_id: NetworkId, -} +pub struct AccountCreateArgs {} impl TryFrom for AccountCreateArgs { type Error = Error; diff --git a/wallet/core/src/wasm/wallet/keydata.rs b/wallet/core/src/wasm/wallet/keydata.rs index 6e253fc0f..c689cfbf6 100644 --- a/wallet/core/src/wasm/wallet/keydata.rs +++ b/wallet/core/src/wasm/wallet/keydata.rs @@ -1,17 +1,16 @@ use crate::imports::*; use crate::result::Result; -use crate::runtime; use crate::storage::keydata; #[wasm_bindgen] pub struct PrvKeyDataInfo { inner: Arc, #[allow(dead_code)] - wallet: Arc, + wallet: Arc, } impl PrvKeyDataInfo { - pub fn new(wallet: Arc, inner: Arc) -> PrvKeyDataInfo { + pub fn new(wallet: Arc, inner: Arc) -> PrvKeyDataInfo { PrvKeyDataInfo { wallet, inner } } } diff --git a/wallet/core/src/wasm/wallet/storage.rs b/wallet/core/src/wasm/wallet/storage.rs index 564a7da29..c0a068a13 100644 --- a/wallet/core/src/wasm/wallet/storage.rs +++ b/wallet/core/src/wasm/wallet/storage.rs @@ -18,17 +18,17 @@ extern "C" { // initialize wallet storage #[wasm_bindgen(method, js_name = "create")] - async fn create(this: &Storage, ctx: &Arc, args: CreateArgs) -> Result<()>; + async fn create(this: &Storage, wallet_secret: &Secret, args: CreateArgs) -> Result<()>; // async fn is_open(&self) -> Result; // establish an open state (load wallet data cache, connect to the database etc.) #[wasm_bindgen(method, js_name = "open")] - async fn open(this: &Storage, ctx: &Arc, args: OpenArgs) -> Result<()>; + async fn open(this: &Storage, wallet_secret: &Secret, args: OpenArgs) -> Result<()>; // flush writable operations (invoked after multiple store and remove operations) #[wasm_bindgen(method, js_name = "commit")] - async fn commit(this: &Storage, ctx: &Arc) -> Result<()>; + async fn commit(this: &Storage, wallet_secret: &Secret) -> Result<()>; // stop the storage subsystem #[wasm_bindgen(method, js_name = "close")] @@ -52,13 +52,13 @@ extern "C" { #[wasm_bindgen(method, js_name = "loadKeyInfo")] async fn load_key_info(this: &Storage, id: &PrvKeyDataId) -> Result>>; #[wasm_bindgen(method, js_name = "loadKeyData")] - async fn load_key_data(this: &Storage, ctx: &Arc, id: &PrvKeyDataId) -> Result>; + async fn load_key_data(this: &Storage, wallet_secret: &Secret, id: &PrvKeyDataId) -> Result>; #[wasm_bindgen(method, js_name = "storeKeyInfo")] - async fn store_key_info(this: &Storage, ctx: &Arc, data: PrvKeyData) -> Result<()>; + async fn store_key_info(this: &Storage, wallet_secret: &Secret, data: PrvKeyData) -> Result<()>; #[wasm_bindgen(method, js_name = "storeKeyData")] - async fn store_key_data(this: &Storage, ctx: &Arc, data: PrvKeyData) -> Result<()>; + async fn store_key_data(this: &Storage, wallet_secret: &Secret, data: PrvKeyData) -> Result<()>; #[wasm_bindgen(method, js_name = "removeKeyData")] - async fn remove_key_data(this: &Storage, ctx: &Arc, id: &PrvKeyDataId) -> Result<()>; + async fn remove_key_data(this: &Storage, wallet_secret: &Secret, id: &PrvKeyDataId) -> Result<()>; #[wasm_bindgen(method, js_name = "getAccountRange")] async fn get_account_range(this: &Storage, prv_key_data_id_filter: Option) -> Result>; @@ -126,7 +126,7 @@ impl Interface for Proxy { store.exists().await } - async fn create(&self, ctx: &Arc, args: CreateArgs) -> Result<()> { + async fn create(&self, wallet_secret: &Secret, args: CreateArgs) -> Result<()> { let location = self.location.lock().unwrap().clone().unwrap(); let inner = Arc::new(Inner::try_create(ctx, &location.folder, args).await?); self.inner.lock().unwrap().replace(inner); @@ -134,7 +134,7 @@ impl Interface for Proxy { Ok(()) } - async fn open(&self, ctx: &Arc, args: OpenArgs) -> Result<()> { + async fn open(&self, wallet_secret: &Secret, args: OpenArgs) -> Result<()> { let location = self.location.lock().unwrap().clone().unwrap(); let inner = Arc::new(Inner::try_load(ctx, &location.folder, args).await?); self.inner.lock().unwrap().replace(inner); @@ -149,7 +149,7 @@ impl Interface for Proxy { Ok(Some(self.inner()?.store.filename_as_string())) } - async fn commit(&self, ctx: &Arc) -> Result<()> { + async fn commit(&self, wallet_secret: &Secret) -> Result<()> { // log_info!("--== committing storage ==--"); self.inner()?.store(ctx).await?; Ok(()) @@ -189,13 +189,13 @@ impl PrvKeyDataStore for Inner { Ok(self.cache().prv_key_data_info.map.get(prv_key_data_id).cloned()) } - async fn load_key_data(&self, ctx: &Arc, prv_key_data_id: &PrvKeyDataId) -> Result> { + async fn load_key_data(&self, wallet_secret: &Secret, prv_key_data_id: &PrvKeyDataId) -> Result> { let wallet_secret = ctx.wallet_secret().await; let prv_key_data_map: Decrypted = self.cache().prv_key_data.decrypt(wallet_secret)?; Ok(prv_key_data_map.get(prv_key_data_id).cloned()) } - async fn store(&self, ctx: &Arc, prv_key_data: PrvKeyData) -> Result<()> { + async fn store(&self, wallet_secret: &Secret, prv_key_data: PrvKeyData) -> Result<()> { let wallet_secret = ctx.wallet_secret().await; let prv_key_data_info = Arc::new((&prv_key_data).into()); self.cache().prv_key_data_info.insert(prv_key_data.id, prv_key_data_info)?; @@ -206,7 +206,7 @@ impl PrvKeyDataStore for Inner { Ok(()) } - async fn remove(&self, ctx: &Arc, prv_key_data_id: &PrvKeyDataId) -> Result<()> { + async fn remove(&self, wallet_secret: &Secret, prv_key_data_id: &PrvKeyDataId) -> Result<()> { let wallet_secret = ctx.wallet_secret().await; let mut prv_key_data_map: Decrypted = self.cache().prv_key_data.decrypt(wallet_secret.clone())?; prv_key_data_map.remove(prv_key_data_id); diff --git a/wallet/core/src/wasm/wallet/wallet.rs b/wallet/core/src/wasm/wallet/wallet.rs index bee8bc102..52575aa93 100644 --- a/wallet/core/src/wasm/wallet/wallet.rs +++ b/wallet/core/src/wasm/wallet/wallet.rs @@ -1,22 +1,19 @@ use crate::imports::*; use crate::result::Result; -use crate::runtime; -use crate::secret::Secret; use crate::storage::local::interface::LocalStore; -use crate::storage::PrvKeyDataId; -use crate::storage::{self, Hint}; +use crate::storage::{PrvKeyDataId, WalletDescriptor}; +use crate::wallet as native; use crate::wasm::wallet::account::Account; use crate::wasm::wallet::keydata::PrvKeyDataInfo; use kash_wrpc_client::wasm::RpcClient; use kash_wrpc_client::WrpcEncoding; -use runtime::AccountKind; use workflow_core::sendable::Sendable; use workflow_wasm::channel::EventDispatcher; #[wasm_bindgen(inspectable)] #[derive(Clone)] pub struct Wallet { - pub(crate) wallet: Arc, + pub(crate) wallet: Arc, #[wasm_bindgen(getter_with_clone)] pub rpc: RpcClient, #[wasm_bindgen(getter_with_clone)] @@ -38,7 +35,7 @@ impl Wallet { let rpc_api: Arc = rpc.client().rpc_api().clone(); let rpc_ctl = rpc.client().rpc_ctl().clone(); let rpc_binding = Rpc::new(rpc_api, rpc_ctl); - let wallet = Arc::new(runtime::Wallet::try_with_rpc(Some(rpc_binding), store, network_id)?); + let wallet = Arc::new(native::Wallet::try_with_rpc(Some(rpc_binding), store, network_id)?); let events = EventDispatcher::default(); Ok(Self { wallet, events, rpc }) @@ -103,11 +100,8 @@ impl Wallet { } #[wasm_bindgen(js_name = "descriptor")] - pub fn descriptor(&self) -> Result { - match self.wallet.descriptor()? { - Some(desc) => Ok(JsValue::from(desc)), - None => Ok(JsValue::UNDEFINED), - } + pub fn descriptor(&self) -> Option { + self.wallet.descriptor() } pub async fn exists(&self, name: JsValue) -> Result { @@ -123,45 +117,45 @@ impl Wallet { self.wallet.exists(name.as_deref()).await } - #[wasm_bindgen(js_name = "createWallet")] - pub async fn create_wallet(&self, wallet_args: &JsValue) -> Result { - let wallet_args: WalletCreateArgs = wallet_args.try_into()?; - let descriptor = self.wallet.create_wallet(wallet_args.into()).await?; - Ok(descriptor.unwrap_or_default()) - } - - #[wasm_bindgen(js_name = "createPrvKeyData")] - pub async fn create_prv_key_data(&self, args: &JsValue) -> Result { - let prv_key_data_args: PrvKeyDataCreateArgs = args.try_into()?; - let (prv_key_data_id, mnemonic) = self.wallet.create_prv_key_data(prv_key_data_args.into()).await?; - let object = Object::new(); - object.set("id", &JsValue::from(prv_key_data_id.to_hex()))?; - object.set("mnemonic", &JsValue::from(mnemonic.phrase_string()))?; - Ok(object) - } - - #[wasm_bindgen(js_name = "createAccount")] - pub async fn create_account(&self, prv_key_data_id: String, account_args: &JsValue) -> Result { - let _account_args: AccountCreateArgs = account_args.try_into()?; - let _prv_key_data_id = - PrvKeyDataId::from_hex(&prv_key_data_id).map_err(|err| Error::KeyId(format!("{} : {err}", prv_key_data_id)))?; - - todo!() - - // match account_args.account_kind { - // AccountKind::Bip32 | AccountKind::Legacy => { - // let account = self.wallet.create_bip32_account(prv_key_data_id, account_args.into()).await?; - // Ok(Account::try_new(account).await?.into()) - // } - // AccountKind::MultiSig => { - // todo!() - // } - // } - } - - pub async fn ping(&self) -> bool { - self.wallet.ping().await - } + // #[wasm_bindgen(js_name = "createWallet")] + // pub async fn create_wallet(&self, wallet_args: &JsValue) -> Result<()> { + // let wallet_args: WalletCreateArgs = wallet_args.try_into()?; + // let (_wallet_descriptor, _storage_descriptor) = self.wallet.create_wallet(wallet_args.into()).await?; + // Ok(()) + // } + + // #[wasm_bindgen(js_name = "createPrvKeyData")] + // pub async fn create_prv_key_data(&self, args: &JsValue) -> Result { + // let prv_key_data_args: PrvKeyDataCreateArgs = args.try_into()?; + // let (prv_key_data_id, mnemonic) = self.wallet.create_prv_key_data(prv_key_data_args.into()).await?; + // let object = Object::new(); + // object.set("id", &JsValue::from(prv_key_data_id.to_hex()))?; + // object.set("mnemonic", &JsValue::from(mnemonic.phrase_string()))?; + // Ok(object) + // } + + // #[wasm_bindgen(js_name = "createAccount")] + // pub async fn create_account(&self, prv_key_data_id: String, account_args: &JsValue) -> Result { + // let _account_args: AccountCreateArgs = account_args.try_into()?; + // let _prv_key_data_id = + // PrvKeyDataId::from_hex(&prv_key_data_id).map_err(|err| Error::KeyId(format!("{} : {err}", prv_key_data_id)))?; + + // todo!() + + // // match account_args.account_kind { + // // AccountKind::Bip32 | AccountKind::Legacy => { + // // let account = self.wallet.create_bip32_account(prv_key_data_id, account_args.into()).await?; + // // Ok(Account::try_new(account).await?.into()) + // // } + // // AccountKind::MultiSig => { + // // todo!() + // // } + // // } + // } + + // pub async fn ping(&self) -> bool { + // self.wallet.ping().await.is_ok() + // } pub async fn start(&self) -> Result<()> { self.events.start_notification_task(self.wallet.multiplexer()).await?; @@ -212,81 +206,99 @@ impl TryFrom for WalletCtorArgs { } } -struct WalletCreateArgs { - pub title: Option, - pub filename: Option, - pub user_hint: Option, - pub wallet_secret: Secret, - pub overwrite_wallet_storage: bool, -} +// struct WalletCreateArgs { +// pub title: Option, +// pub filename: Option, +// pub user_hint: Option, +// pub wallet_secret: Secret, +// pub overwrite_wallet_storage: bool, +// } -impl TryFrom<&JsValue> for WalletCreateArgs { - type Error = Error; - fn try_from(js_value: &JsValue) -> std::result::Result { - if let Some(object) = Object::try_from(js_value) { - Ok(WalletCreateArgs { - title: object.try_get_string("title")?, - filename: object.try_get_string("filename")?, - user_hint: object.try_get_string("hint")?.map(Hint::from), - wallet_secret: object.get_string("walletSecret")?.into(), - overwrite_wallet_storage: object.try_get_bool("overwrite")?.unwrap_or(false), - }) - } else if let Some(secret) = js_value.as_string() { - Ok(WalletCreateArgs { - title: None, - filename: None, - user_hint: None, - wallet_secret: secret.into(), - overwrite_wallet_storage: false, - }) - } else { - Err("WalletCreateArgs argument must be an object or a secret".into()) - } - } -} +// impl TryFrom<&JsValue> for WalletCreateArgs { +// type Error = Error; +// fn try_from(js_value: &JsValue) -> std::result::Result { +// if let Some(object) = Object::try_from(js_value) { +// Ok(WalletCreateArgs { +// title: object.try_get_string("title")?, +// filename: object.try_get_string("filename")?, +// user_hint: object.try_get_string("hint")?.map(Hint::from), +// wallet_secret: object.get_string("walletSecret")?.into(), +// overwrite_wallet_storage: object.try_get_bool("overwrite")?.unwrap_or(false), +// }) +// } else if let Some(secret) = js_value.as_string() { +// Ok(WalletCreateArgs { +// title: None, +// filename: None, +// user_hint: None, +// wallet_secret: secret.into(), +// overwrite_wallet_storage: false, +// }) +// } else { +// Err("WalletCreateArgs argument must be an object or a secret".into()) +// } +// } +// } -impl From for runtime::WalletCreateArgs { - fn from(args: WalletCreateArgs) -> Self { - Self { - title: args.title, - filename: args.filename, - user_hint: args.user_hint, - wallet_secret: args.wallet_secret, - overwrite_wallet_storage: args.overwrite_wallet_storage, - } - } -} +// impl From for native::WalletCreateArgs { +// fn from(args: WalletCreateArgs) -> Self { +// Self { +// title: args.title, +// filename: args.filename, +// user_hint: args.user_hint, +// wallet_secret: args.wallet_secret, +// overwrite_wallet_storage: args.overwrite_wallet_storage, +// } +// } +// } -struct PrvKeyDataCreateArgs { - pub name: Option, - pub wallet_secret: Secret, - pub payment_secret: Option, - pub mnemonic: Option, -} +// struct PrvKeyDataCreateArgs { +// pub name: Option, +// pub wallet_secret: Secret, +// pub payment_secret: Option, +// pub mnemonic: MnemonicVariant, +// } -impl TryFrom<&JsValue> for PrvKeyDataCreateArgs { - type Error = Error; - fn try_from(js_value: &JsValue) -> std::result::Result { - if let Some(object) = Object::try_from(js_value) { - Ok(PrvKeyDataCreateArgs { - name: object.try_get_string("name")?, - wallet_secret: object.get_string("walletSecret")?.into(), - payment_secret: object.try_get_string("paymentSecret")?.map(|s| s.into()), - mnemonic: object.try_get_string("mnemonic")?, - }) - } else if let Some(secret) = js_value.as_string() { - Ok(PrvKeyDataCreateArgs { name: None, wallet_secret: secret.into(), payment_secret: None, mnemonic: None }) - } else { - Err("PrvKeyDataCreateArgs argument must be an object or a secret".into()) - } - } -} +// impl TryFrom<&JsValue> for PrvKeyDataCreateArgs { +// type Error = Error; +// fn try_from(js_value: &JsValue) -> std::result::Result { +// if let Some(object) = Object::try_from(js_value) { +// let mnemonic = object.get_value("mnemonic")?; +// let mnemonic = if let Some(mnemonic) = mnemonic.as_string() { +// MnemonicVariant::Phrase(mnemonic) +// } else if let Some(words) = mnemonic.as_f64().map(|n| n as usize) { +// let word_count = WordCount::try_from(words)?; +// MnemonicVariant::Random(word_count) +// } else { +// return Err( +// "PrvKeyDataCreateArgs argument must be a phrase or a number of words for random mnemonic generation (12 or 24)" +// .into(), +// ); +// }; + +// Ok(PrvKeyDataCreateArgs { +// name: object.try_get_string("name")?, +// wallet_secret: object.get_string("walletSecret")?.into(), +// payment_secret: object.try_get_string("paymentSecret")?.map(|s| s.into()), +// mnemonic, +// }) +// } else if let Some(secret) = js_value.as_string() { +// Ok(PrvKeyDataCreateArgs { +// name: None, +// wallet_secret: secret.into(), +// payment_secret: None, +// mnemonic: MnemonicVariant::Random(WordCount::Words12), +// }) +// } else { +// Err("PrvKeyDataCreateArgs argument must be an object or a secret".into()) +// } +// } +// } -impl From for runtime::PrvKeyDataCreateArgs { - fn from(args: PrvKeyDataCreateArgs) -> Self { - Self { name: args.name, wallet_secret: args.wallet_secret, payment_secret: args.payment_secret, mnemonic: args.mnemonic } - } -} +// impl From for runtime::PrvKeyDataCreateArgs { +// fn from(args: PrvKeyDataCreateArgs) -> Self { +// Self { name: args.name, wallet_secret: args.wallet_secret, payment_secret: args.payment_secret, mnemonic: args.mnemonic } +// } +// } // impl Drop for PrvKeyDataCreateArgs { // fn drop(&mut self) { @@ -296,50 +308,47 @@ impl From for runtime::PrvKeyDataCreateArgs { // } // } -struct AccountCreateArgs { - pub name: Option, - pub title: Option, - pub account_kind: storage::AccountKind, - pub wallet_secret: Secret, - pub payment_secret: Option, -} +// struct AccountCreateArgs { +// pub name: Option, +// pub account_kind: storage::AccountKind, +// pub wallet_secret: Secret, +// pub payment_secret: Option, +// } -impl TryFrom<&JsValue> for AccountCreateArgs { - type Error = Error; - fn try_from(js_value: &JsValue) -> std::result::Result { - if let Some(object) = Object::try_from(js_value) { - let account_kind = object.get::("accountKind")?; - // let account_kind = if let Some(kind) = kind.as_f64() { - // AccountKind::try_from(kind as u8)? - // } else if let Some(kind) = kind.as_string() { - // AccountKind::from_str(kind.as_str())? - // } else if kind.is_undefined() { - // AccountKind::default() - // } else { - // return Err(Error::Custom("AccountCreateArgs is missing `accountKind` property".to_string())); - // }; - - Ok(AccountCreateArgs { - name: object.try_get_string("name")?, - title: object.try_get_string("title")?, - account_kind, - wallet_secret: object.get_string("walletSecret")?.into(), - payment_secret: object.try_get_string("paymentSecret")?, - }) - } else { - Err("AccountCreateArgs argument must be an object".into()) - } - } -} +// impl TryFrom<&JsValue> for AccountCreateArgs { +// type Error = Error; +// fn try_from(js_value: &JsValue) -> std::result::Result { +// if let Some(object) = Object::try_from(js_value) { +// let account_kind = object.get::("accountKind")?; +// // let account_kind = if let Some(kind) = kind.as_f64() { +// // AccountKind::try_from(kind as u8)? +// // } else if let Some(kind) = kind.as_string() { +// // AccountKind::from_str(kind.as_str())? +// // } else if kind.is_undefined() { +// // AccountKind::default() +// // } else { +// // return Err(Error::Custom("AccountCreateArgs is missing `accountKind` property".to_string())); +// // }; + +// Ok(AccountCreateArgs { +// name: object.try_get_string("name")?, +// account_kind, +// wallet_secret: object.get_string("walletSecret")?.into(), +// payment_secret: object.try_get_string("paymentSecret")?, +// }) +// } else { +// Err("AccountCreateArgs argument must be an object".into()) +// } +// } +// } -impl From for runtime::AccountCreateArgs { - fn from(args: AccountCreateArgs) -> Self { - runtime::AccountCreateArgs { - name: args.name, - title: args.title, - account_kind: args.account_kind, - wallet_secret: args.wallet_secret, - payment_secret: args.payment_secret.map(|s| s.into()), - } - } -} +// impl From for runtime::AssocAccountCreateArgs { +// fn from(args: AccountCreateArgs) -> Self { +// runtime::AssocAccountCreateArgs { +// account_name: args.name, +// account_kind: args.account_kind, +// wallet_secret: args.wallet_secret, +// payment_secret: args.payment_secret.map(|s| s.into()), +// } +// } +// } diff --git a/wallet/core/src/wasm/xprivatekey.rs b/wallet/core/src/wasm/xprivatekey.rs index 7748b7665..966dc5490 100644 --- a/wallet/core/src/wasm/xprivatekey.rs +++ b/wallet/core/src/wasm/xprivatekey.rs @@ -1,5 +1,5 @@ use crate::derivation::gen1::WalletDerivationManager; -use crate::Result; +use crate::result::Result; use kash_bip32::{ChildNumber, ExtendedPrivateKey, SecretKey}; use kash_consensus_wasm::PrivateKey; use std::str::FromStr; diff --git a/wallet/core/src/wasm/xpublickey.rs b/wallet/core/src/wasm/xpublickey.rs index 3f37e87ef..8be356512 100644 --- a/wallet/core/src/wasm/xpublickey.rs +++ b/wallet/core/src/wasm/xpublickey.rs @@ -1,6 +1,6 @@ use crate::derivation::gen1::WalletDerivationManager; use crate::derivation::traits::WalletDerivationManagerTrait; -use crate::Result; +use crate::result::Result; use kash_bip32::ExtendedPublicKey; use kash_bip32::{ExtendedPrivateKey, SecretKey}; use std::str::FromStr; diff --git a/wallet/macros/Cargo.toml b/wallet/macros/Cargo.toml new file mode 100644 index 000000000..e2f8c4ccf --- /dev/null +++ b/wallet/macros/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "kash-wallet-macros" +authors.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true +keywords = ["rpc"] +categories = [] +exclude = ["/.*", "/test"] +description = """ +Macros for the Kash Wallet infrastructure +""" + +[lib] +proc-macro = true + +[dependencies] +proc-macro-error = { version = "1", default-features = false } +proc-macro2 = { version = "1.0.43" } +quote = "1.0.21" +syn = {version="1.0.99",features=["full","fold","extra-traits","parsing","proc-macro"]} +convert_case = "0.5.0" +regex.workspace = true +xxhash-rust = { workspace = true, features = ["xxh32"] } diff --git a/wallet/macros/src/handler.rs b/wallet/macros/src/handler.rs new file mode 100644 index 000000000..9fb6b0f1c --- /dev/null +++ b/wallet/macros/src/handler.rs @@ -0,0 +1,60 @@ +use convert_case::{Case, Casing}; +// use proc_macro::Literal; +use proc_macro2::{Ident, Literal, Span}; +use quote::ToTokens; +use syn::{Error, Expr, ExprArray, Result}; +use xxhash_rust::xxh3::xxh3_64; +use xxhash_rust::xxh32::xxh32; + +pub struct Handler { + pub name: String, + pub hash_32: Literal, + pub hash_64: Literal, + pub ident: Literal, + pub fn_call: Ident, + pub fn_with_suffix: Option, + pub fn_no_suffix: Ident, + pub fn_camel: Ident, + pub request_type: Ident, + pub response_type: Ident, +} + +impl Handler { + pub fn new(handler: &Expr) -> Handler { + Handler::new_with_args(handler, None) + } + + pub fn new_with_args(handler: &Expr, fn_suffix: Option<&str>) -> Handler { + let name = handler.to_token_stream().to_string(); + let hash_32 = Literal::u32_suffixed(xxh32(name.as_bytes(), 0)); + let hash_64 = Literal::u64_suffixed(xxh3_64(name.as_bytes())); + let ident = Literal::string(name.to_case(Case::Kebab).as_str()); + let fn_call = Ident::new(&format!("{}_call", name.to_case(Case::Snake)), Span::call_site()); + let fn_with_suffix = fn_suffix.map(|suffix| Ident::new(&format!("{}_{suffix}", name.to_case(Case::Snake)), Span::call_site())); + let fn_no_suffix = Ident::new(&name.to_case(Case::Snake), Span::call_site()); + let fn_camel = Ident::new(&name.to_case(Case::Camel), Span::call_site()); + let request_type = Ident::new(&format!("{name}Request"), Span::call_site()); + let response_type = Ident::new(&format!("{name}Response"), Span::call_site()); + Handler { name, hash_32, hash_64, ident, fn_call, fn_with_suffix, fn_no_suffix, fn_camel, request_type, response_type } + } +} + +pub fn get_handlers(handlers: Expr) -> Result { + let handlers = match handlers { + Expr::Array(array) => array, + _ => { + return Err(Error::new_spanned(handlers, "the argument must be an array of enum variants".to_string())); + } + }; + + for ph in handlers.elems.iter() { + match ph { + Expr::Path(_exp_path) => {} + _ => { + return Err(Error::new_spanned(ph, "handlers should contain enum variants".to_string())); + } + } + } + + Ok(handlers) +} diff --git a/wallet/macros/src/lib.rs b/wallet/macros/src/lib.rs new file mode 100644 index 000000000..4434b4cc1 --- /dev/null +++ b/wallet/macros/src/lib.rs @@ -0,0 +1,28 @@ +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; +mod handler; +mod wallet; + +#[proc_macro] +#[proc_macro_error] +pub fn build_wallet_client_transport_interface(input: TokenStream) -> TokenStream { + wallet::client::build_transport_interface(input) +} + +#[proc_macro] +#[proc_macro_error] +pub fn build_wallet_server_transport_interface(input: TokenStream) -> TokenStream { + wallet::server::build_transport_interface(input) +} + +// #[proc_macro] +// #[proc_macro_error] +// pub fn build_wrpc_wasm_bindgen_interface(input: TokenStream) -> TokenStream { +// wallet::wasm::build_wrpc_wasm_bindgen_interface(input) +// } + +// #[proc_macro] +// #[proc_macro_error] +// pub fn build_wrpc_wasm_bindgen_subscriptions(input: TokenStream) -> TokenStream { +// wallet::wasm::build_wrpc_wasm_bindgen_subscriptions(input) +// } diff --git a/wallet/macros/src/wallet/client.rs b/wallet/macros/src/wallet/client.rs new file mode 100644 index 000000000..440c163a4 --- /dev/null +++ b/wallet/macros/src/wallet/client.rs @@ -0,0 +1,94 @@ +use crate::handler::*; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use std::convert::Into; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + Error, Expr, ExprArray, Result, Token, +}; + +#[derive(Debug)] +struct RpcTable { + // rpc_api_ops: Expr, + handlers: ExprArray, +} + +impl Parse for RpcTable { + fn parse(input: ParseStream) -> Result { + let parsed = Punctuated::::parse_terminated(input).unwrap(); + if parsed.len() != 1 { + return Err(Error::new_spanned(parsed, "usage: build_wrpc_client_interface!([XxxRequest, ..])".to_string())); + } + + let mut iter = parsed.iter(); + // Intake the enum name + // let rpc_api_ops = iter.next().unwrap().clone(); + // Intake enum variants as an array + let handlers = get_handlers(iter.next().unwrap().clone())?; + + Ok(RpcTable { handlers }) + } +} + +impl ToTokens for RpcTable { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut targets = Vec::new(); + // let rpc_api_ops = &self.rpc_api_ops; + + for handler in self.handlers.elems.iter() { + let Handler { hash_64, ident, fn_call, request_type, response_type, .. } = Handler::new(handler); + + targets.push(quote! { + fn #fn_call<'async_trait>( + self: Arc, + request: #request_type, + ) -> ::core::pin::Pin< + Box> + ::core::marker::Send + 'async_trait>, + > + where + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::> { + return __ret; + } + let op: u64 = #hash_64; + let __self = self; + let request = request; + let __ret: Result<#response_type> = + { + match __self.transport { + Transport::Borsh(ref transport) => { + Ok(#response_type::try_from_slice(&transport.call(op, request.try_to_vec()?).await?)?) + }, + Transport::Serde(ref transport) => { + let request = serde_json::to_string(&request)?; + let response = transport.call(#ident, request.as_str()).await?; + Ok(serde_json::from_str::<#response_type>(response.as_str())?) + }, + } + // Ok(#response_type::try_from_slice(&__self.transport.call(op, &request.try_to_vec()?).await?)?) + }; + #[allow(unreachable_code)] + __ret + }) + } + + }); + } + + quote! { + #(#targets)* + } + .to_tokens(tokens); + } +} + +pub fn build_transport_interface(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let rpc_table = parse_macro_input!(input as RpcTable); + let ts = rpc_table.to_token_stream(); + // println!("MACRO: {}", ts.to_string()); + ts.into() +} diff --git a/wallet/macros/src/wallet/mod.rs b/wallet/macros/src/wallet/mod.rs new file mode 100644 index 000000000..b3ebc3984 --- /dev/null +++ b/wallet/macros/src/wallet/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod server; +// TODO @aspect (placeholder for wasm bindings) +// pub mod wasm; diff --git a/wallet/macros/src/wallet/server.rs b/wallet/macros/src/wallet/server.rs new file mode 100644 index 000000000..6a25ffb15 --- /dev/null +++ b/wallet/macros/src/wallet/server.rs @@ -0,0 +1,93 @@ +use crate::handler::*; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use std::convert::Into; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + Error, Expr, ExprArray, Result, Token, +}; + +#[derive(Debug)] +struct RpcTable { + handlers: ExprArray, +} + +impl Parse for RpcTable { + fn parse(input: ParseStream) -> Result { + let parsed = Punctuated::::parse_terminated(input).unwrap(); + if parsed.len() != 1 { + return Err(Error::new_spanned(parsed, "usage: build_wrpc_server_interface!([XxxOp, ..])".to_string())); + } + + let mut iter = parsed.iter(); + let handlers = get_handlers(iter.next().unwrap().clone())?; + + Ok(RpcTable { handlers }) + } +} + +impl ToTokens for RpcTable { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut targets_borsh = Vec::new(); + let mut targets_serde = Vec::new(); + + for handler in self.handlers.elems.iter() { + let Handler { hash_64, ident, fn_call, request_type, .. } = Handler::new(handler); + + targets_borsh.push(quote! { + #hash_64 => { + Ok(self.wallet_api.clone().#fn_call(#request_type::try_from_slice(&request)?).await?.try_to_vec()?) + } + }); + + targets_serde.push(quote! { + #ident => { + let request: #request_type = serde_json::from_str(request)?; + let response = self.wallet_api.clone().#fn_call(request).await?; + Ok(serde_json::to_string(&response)?) + } + }); + + // targets_serde_wasm.push(quote! { + // #ident => { + // Ok(self.wallet_api.clone().#fn_call(#request_type::try_from_slice(&request)?).await?.try_to_vec()?) + // } + // }); + } + + quote! { + + pub async fn call_with_borsh(&self, op: u64, request: &[u8]) -> Result> { + match op { + #(#targets_borsh)* + _ => { Err(Error::NotImplemented) } + } + } + + pub async fn call_with_serde(&self, op: &str, request: &str) -> Result { + match op { + #(#targets_serde)* + _ => { Err(Error::NotImplemented) } + } + } + + // async fn call_with_serde_wasm(&self, op: &str, request : &JsValue) -> Result { + // match op { + // #(#targets_serde_wasm)* + // _ => { Err(Error::NotImplemented) } + // } + // } + + } + .to_tokens(tokens); + } +} + +pub fn build_transport_interface(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let rpc_table = parse_macro_input!(input as RpcTable); + let ts = rpc_table.to_token_stream(); + // println!("MACRO: {}", ts.to_string()); + ts.into() +} diff --git a/wallet/macros/src/wallet/wasm.rs b/wallet/macros/src/wallet/wasm.rs new file mode 100644 index 000000000..89802ad87 --- /dev/null +++ b/wallet/macros/src/wallet/wasm.rs @@ -0,0 +1,172 @@ +use crate::handler::*; +use convert_case::{Case, Casing}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, ToTokens}; +use regex::Regex; +use std::convert::Into; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + Error, Expr, ExprArray, Result, Token, +}; + +#[derive(Debug)] +struct RpcHandlers { + handlers_no_args: ExprArray, + handlers_with_args: ExprArray, +} + +impl Parse for RpcHandlers { + fn parse(input: ParseStream) -> Result { + let parsed = Punctuated::::parse_terminated(input).unwrap(); + if parsed.len() != 2 { + return Err(Error::new_spanned( + parsed, + "usage: build_wrpc_wasm_bindgen_interface!([fn no args, ..],[fn with args, ..])".to_string(), + )); + } + + let mut iter = parsed.iter(); + let handlers_no_args = get_handlers(iter.next().unwrap().clone())?; + let handlers_with_args = get_handlers(iter.next().unwrap().clone())?; + + let handlers = RpcHandlers { handlers_no_args, handlers_with_args }; + Ok(handlers) + } +} + +impl ToTokens for RpcHandlers { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut targets_no_args = Vec::new(); + let mut targets_with_args = Vec::new(); + + for handler in self.handlers_no_args.elems.iter() { + let Handler { fn_call, fn_camel, fn_no_suffix, request_type, response_type, .. } = Handler::new(handler); + + targets_no_args.push(quote! { + + #[wasm_bindgen(js_name = #fn_camel)] + pub async fn #fn_no_suffix(&self) -> Result { + let value: JsValue = js_sys::Object::new().into(); + let request: #request_type = from_value(value)?; + // log_info!("request: {:#?}",request); + let result: RpcResult<#response_type> = self.client.#fn_call(request).await; + // log_info!("result: {:#?}",result); + + let response: #response_type = result.map_err(|err|wasm_bindgen::JsError::new(&err.to_string()))?; + //log_info!("response: {:#?}",response); + workflow_wasm::serde::to_value(&response).map_err(|err|err.into()) + } + + }); + } + + for handler in self.handlers_with_args.elems.iter() { + let Handler { fn_call, fn_camel, fn_no_suffix, request_type, response_type, .. } = Handler::new(handler); + + targets_with_args.push(quote! { + + #[wasm_bindgen(js_name = #fn_camel)] + pub async fn #fn_no_suffix(&self, request: JsValue) -> Result { + let request: #request_type = from_value(request)?; + let result: RpcResult<#response_type> = self.client.#fn_call(request).await; + let response: #response_type = result.map_err(|err|wasm_bindgen::JsError::new(&err.to_string()))?; + workflow_wasm::serde::to_value(&response).map_err(|err|err.into()) + } + + }); + } + + quote! { + #[wasm_bindgen] + impl RpcClient { + #(#targets_no_args)* + #(#targets_with_args)* + } + } + .to_tokens(tokens); + } +} + +pub fn build_wrpc_wasm_bindgen_interface(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let rpc_table = parse_macro_input!(input as RpcHandlers); + let ts = rpc_table.to_token_stream(); + // println!("MACRO: {}", ts.to_string()); + ts.into() +} + +// ##################################################################### + +#[derive(Debug)] +struct RpcSubscriptions { + handlers: ExprArray, +} + +impl Parse for RpcSubscriptions { + fn parse(input: ParseStream) -> Result { + let parsed = Punctuated::::parse_terminated(input).unwrap(); + if parsed.len() != 1 { + return Err(Error::new_spanned( + parsed, + "usage: build_wrpc_wasm_bindgen_interface!([fn no args, ..],[fn with args, ..])".to_string(), + )); + } + + let mut iter = parsed.iter(); + let handlers = get_handlers(iter.next().unwrap().clone())?; + + Ok(RpcSubscriptions { handlers }) + } +} + +impl ToTokens for RpcSubscriptions { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut targets = Vec::new(); + + for handler in self.handlers.elems.iter() { + let name = format!("Notify{}", handler.to_token_stream().to_string().as_str()); + let regex = Regex::new(r"^Notify").unwrap(); + let blank = regex.replace(&name, ""); + let subscribe = regex.replace(&name, "Subscribe"); + let unsubscribe = regex.replace(&name, "Unsubscribe"); + let scope = Ident::new(&blank, Span::call_site()); + let sub_scope = Ident::new(format!("{blank}Scope").as_str(), Span::call_site()); + let fn_subscribe_snake = Ident::new(&subscribe.to_case(Case::Snake), Span::call_site()); + let fn_subscribe_camel = Ident::new(&subscribe.to_case(Case::Camel), Span::call_site()); + let fn_unsubscribe_snake = Ident::new(&unsubscribe.to_case(Case::Snake), Span::call_site()); + let fn_unsubscribe_camel = Ident::new(&unsubscribe.to_case(Case::Camel), Span::call_site()); + + targets.push(quote! { + + #[wasm_bindgen(js_name = #fn_subscribe_camel)] + pub async fn #fn_subscribe_snake(&self) -> Result<()> { + self.client.start_notify(ListenerId::default(), Scope::#scope(#sub_scope {})).await?; + Ok(()) + } + + #[wasm_bindgen(js_name = #fn_unsubscribe_camel)] + pub async fn #fn_unsubscribe_snake(&self) -> Result<()> { + self.client.stop_notify(ListenerId::default(), Scope::#scope(#sub_scope {})).await?; + Ok(()) + } + + }); + } + + quote! { + #[wasm_bindgen] + impl RpcClient { + #(#targets)* + } + } + .to_tokens(tokens); + } +} + +pub fn build_wrpc_wasm_bindgen_subscriptions(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let rpc_table = parse_macro_input!(input as RpcSubscriptions); + let ts = rpc_table.to_token_stream(); + // println!("MACRO: {}", ts.to_string()); + ts.into() +} diff --git a/wasm/npm/package.json b/wasm/npm/package.json index 7311eb0aa..4ed6462ec 100644 --- a/wasm/npm/package.json +++ b/wasm/npm/package.json @@ -1,6 +1,6 @@ { "name": "kash", - "version": "0.1.2", + "version": "0.13.0", "description": "Kash SDK", "main": "index.js", "scripts": { @@ -23,7 +23,7 @@ "homepage": "https://github.com/Kash-Protocol/rusty-kash#readme", "dependencies": { "isomorphic-ws": "5.0.0", - "ws": "8.13.0", - "kash-wasm": "0.1.2" + "ws": "8.14.2", + "kash-wasm": "0.13.0" } }