From 4364f37fac816ccc16848a81433c8effec7bb9de Mon Sep 17 00:00:00 2001 From: aspect Date: Sun, 5 May 2024 21:26:14 +0300 Subject: [PATCH] WASM SDK update, public node resolver and TypeScript bindings (#459) * using IConnectOptions struct instead of Jsvalue for socket connection * Cargo.lock * WIP - initial Wallet API typescript bindings * WIP - Wallet API typescript bindings * typescript setup * TS related issue * relocate TypeScript StringArray to kaspa_consensus_wasm * Update deploy GitHub Action to use folders inside WASM archives * WIP: TSDoc setup * WIP: TSdoc * build-docs updated for typedoc uses * XPublicKey toString/fromString support for JS * WASM + Wallet API refactoring for TypeScript * rearrange nodejs JS/TS examples * moved ts files to src dir * rpc frame size * utxo context listener tests * WIP - public node wRPC beacons * fix TS merge * refactor/restructure past typedoc merge * Add a warning on build-node-dev * cleanup * utxo events issue * rename README * rename README * WIP - typescript bindings and related refactoring * code formatting * wasm32-sdk feature gate * wasm32-sdk feature gating TS/JS-side APIs and types; typedoc handling in macros; relocate wasm RpcClient API into kaspa-wrpc-wasm; * documentation parsing * refactor typescript types & JS/TS examples * simplify HexString (typescript type) * doc processing in macros * wRPC Beacon * #!/bin/bash headers for bash scripts * wRPC Beacon Cargo.toml update * WASM wRPC Beacon * INetworkId type * toString for XPrv * renaming SerdeJson to Json * fix wasm32-sdk propagation * fix wasm32-sdk propagation * WIP JS examples * code formatting * fix beacon config loading * updating examples init script * SerdeJson renaming * WIP TS: rename XPxxKey to PxxKeyGenerator * module rename * finish merging with master * build*-dev warning text * WIP: Wallet API updates * wasm32-sdk infra * fix IndexMap "deprecated" warning in orphan processing * kaspa-beacon CORS handling * Prefix : ktrv, ktub * key generator (xprv,xpub, signer, keypair) moved to wallet/core/wasm * XPrv creation from string (xprvxxxx), examples * change wallet xpub example * restructure docs location * hex serialization for RPC components * WASM32 SDK examples (WIP) * code formatting * set /test and /check bash scripts to use -e for immediate exit on failure * rename from_str to from_xprv_str (clippy) * wasm32-sdk changelog * CI updates for wasm32-sdk * WASM32 SDK CHANGELOG updates * WIP RPC & Wallet event types * code formatting * docs * Segregating wallet core from keys crates (WASM32 WIP) * WIP WASM32 release * WIP WASM32 release * WIP WASM32 structure * WASM32 rename packages * docs * fix build scripts * script updates * fix gitignore * update kaspa-beacon; fix misc issues, improve election algorithm; * display error in case of metrics failure * rename client-side Beacon to Resolver; impl related WASM32 bindings and handling for RpcClient and Resolver * WASM32 wRPC addEventListener() handling & misc bug fixes * mix fixes, docs, update WASM32 examples to use updated APIs * update WASM32 examples * replace EventDispatcher with localized event handlers * Refactor wasm event listeners (RPC, UtxoProcessor, Wallet) + update LICENSE * Support contexts in event listeners * additional TypeScript interfaces and docs * WASM32 changelog updates * WASM32 release scripts * Kaspa Beacon - tentative access lists * Kaspa Beacon ACL * Kaspa Beacon ACLs * Fix parsing issues with typedoc * Fix TypeDoc parsing issues * WASM32 updates to UtxoProcessor (ctors, UTXO access) * misc UtxoContext updates * fix typo in cli wizard * WASM32 - refactor key APIs; fix GetBalancesByAddresses conversion; rename IXxx types to XxxT (convention). * WASM32 / client - additional key management APIs * WASM32 Wallet API * CryptoBox wrapper (+ WASM32 interface) * WASM32 - PublicKeyGenerator functions, hash functions to support plain text hashing, CryptoBox API, fix RPC shutdown/disconnect mishandling in certain cases. * Fix broken typedoc link * fix createAddresses() TS type handling * WASM32 improve UtxoProcessor restart messaging * WASM - Fix Generator priorityFee type binding * WIP - WASM CastFromJS+TryCastFromJS derive macros and traits. Refactoring JsValue casting to Rust objects. secp256k1 update to latest (pending warning resolution) * WASM - TryCastFromJs updates * Update PrvKeyDataCreateArgs to use Secret for mnemonic + related CLI changes * Fix IGeneratorSettingsObject "entries" ingest order * logging for generator testing * Generator logs * browser extension scaffolding * cleanup logging after debugging * update WASM changelog * gate wasm binding by feature * put comments back * add and rename crate * wallet transport * Change decrypt error to be more generic * impl and isolate WASM Header bindings from consensus Header * Fix From for JsValue recursion (fixes WASM message signing) * lowercase hex * Change decryption failure error message to be more generic * Update attribute cleaner deps * Cleanup * add --weak-refs to wasm-pack * implement Wallet API accounts_ensure_default() helper * Update WASM examples/init script to the latest API changes * Fix typescript mixed array types * remove sync monitor logs * Fix mixed typescript array declarations * Add Debug to WASM event sinks * Fix grammar * re-order NetworkTypeT to NetworkType conversion due to failing native WBG enum ABI resolution during type mix (try_from_js_value) * WASM: fix multi-listener handling for RPC-subscribed events * Replace from_slice with from_digest_slice to handle warnings * cleanup logs * Fix incorrect call on wRPC client disconnect * WIP - transaction serialization and input signing * Fix txscript collision due to multiple implementations * Fix incorrect opcode enum variant name * WIP transaction serialization * PendingTransaction -> serializeXXX impl * Adapter examples (#30) * wip: wallet extension connection * dark theme * Update Cargo.toml * docs + TransactionInput.utxo() * Fix handling of passing arrays to Generator * WIP - signing * code formatting * WASM RPC: accept extraData as hex or vec in GetBlockTemplateRequest * WASM RPC: GetBlockTemplateRequest - accept extraData as plain text instead of a hex string * WASM: remove ? from header hash interface * RpcClient: Refactor notification handling in wRPC client + add various guards around connect/disconnect and start/stop API calls * unify WASM logs with workflow-rs logger * Split connect/disconnect guards to allow concurrent disconnect while connect is pending... * cleanup logs * Stop RPC client services on connection error in fallback mode * Fix misc issues post-merge with master * wRPC add prelude to the kaspa-wrpc-client crate * add kaspa-wrpc-example-subscriber example * cleanup * add TransactionRecord.has_address() helper to check if an address is used in the transaction record * Update WASM TypeScript example for event casing and ITransactionRecord access * addEventListener event union (#31) * Cargo.lock * rename kaspa-beacon app to kaspa-resolver * WASM wasm_bindgen bindings for TransactionRecord and a dedicated path on * Wallet Metrics event; retain_context(), change_network_id() and account_select() api calls; get_status_call() updates; * Cleanup * TS wallet events (#32) * Apply map-based event typing on wallet events (following UtxoProcessor type design) * WASM: implement event type maps on RpcClient event notifications * WASM TS events - remove "All" variants; Wallet::set_network_id() -> wrpc_client.set_network_id(); set_network_id() updates; * Cargo.lock * Cleanup * add Wallet::retain_context() to WASM Wallet Class * cleanup * Migrate WASM functions that use SignableTransaction to Transaction (#33) * Migrate WASM functions that use SignableTransaction to Transaction * invalid data handling via returning error * Cargo.lock * WASM: declare events that receive undefined data * typo (#34) * transaction.addresses getter added, SignableTransaction struct removed (#35) * transaction.addresses getter * SignableTransaction struct removed * docs * docs * WASM changelog * master merge * docs * todo (#36) * Todo (#37) * Todo (#38) * transaction.addresses getter * SignableTransaction struct removed * docs * Address.isValid(string) : a static method for address validation * typescript docs: Optional NetworkId * Address.isValid -> Address.validate * tx.addresses getter to tx.addresses() method * transaction.addresses() updated for address creation from SPK * using NetworkTypeT instead of NetworkType * minor doc updates * Update to published workflow-rs 0.11.1 * Fix unit tests and WASM SDK build process * cleanup * Fix symbols missed during the publicKey() to toPublicKey() rename. * Update README info for building WASM SDK * Update README info for building WASM SDK * Rename RPC open/close events to connect/disconnect * Add try getters for rpc in UtxoProcessor, Wallet, Wallet API * Update WASM SDK README * DIsable Account class (but keep the scaffolding for potential future re-integration) * Update WASM SDK README with instructions on how to run NodeJs examples * WalletEvent structure, wallet api example update, (#40) * transaction.addresses getter * SignableTransaction struct removed * docs * Address.isValid(string) : a static method for address validation * typescript docs: Optional NetworkId * Address.isValid -> Address.validate * tx.addresses getter to tx.addresses() method * transaction.addresses() updated for address creation from SPK * using NetworkTypeT instead of NetworkType * batch mode issue * Delete mod copy.rs * AccountKind constructor, WalletApiObjectExtension helper method * TS IAccountsCreateRequest type updated using union * IWalletEvent "event" property renamed to "type" * examples/wallet.js WIP * TransactionRecord notification structure * TransactionRecord notification structure * wallet example update * ecdsa address creation issue * create_address : ecdsa param use case * Update CHANGELOG.md * changelogs, deps version update * Update wallet.js * Update package.json * Update CLI to use a public node Resolver (#39) * Move Tracker memory configuration message from info!() to debug!() * cleanup * cleanup * support public resolver in CLI (default server now is 'public') * lints * Update connect command to print a notice when connecting to a public node. Rename the local url variable. * cleanup * cleanup * Remove optional from ISerializableTransactionInput::utxo field and cleanup previousOutpoint comment * Cleanup and comments * UtxoEntry and UtxoEntryReference: toString override for js (#41) * Fix resolver docs, improve resolver URL argument handling. * Remove IUtxosByAddressesEntry as in WASM we optimize by converting GetUtxosByAddress entries directly to UtxoEntryReference * bump MSRV to 1.78.0 * CI: update arduino/setup-protoc to v3 --------- Co-authored-by: Surinder Singh Matoo Co-authored-by: max143672 Co-authored-by: coderofstuff <114628839+coderofstuff@users.noreply.github.com> --- .github/workflows/ci.yaml | 24 +- .github/workflows/deploy.yaml | 49 +- .gitignore | 3 + Cargo.lock | 2269 ++++++++++------- Cargo.toml | 187 +- LICENSE | 2 +- README.md | 60 +- check | 37 +- cli/Cargo.toml | 1 + cli/src/cli.rs | 20 +- cli/src/error.rs | 3 + cli/src/imports.rs | 2 +- cli/src/modules/connect.rs | 44 +- cli/src/modules/disconnect.rs | 4 +- cli/src/modules/metrics.rs | 2 +- cli/src/modules/monitor.rs | 4 +- cli/src/modules/network.rs | 2 +- cli/src/modules/node.rs | 4 +- cli/src/wizards/account.rs | 2 +- cli/src/wizards/wallet.rs | 8 +- consensus/client/Cargo.toml | 42 + consensus/client/src/error.rs | 106 + consensus/client/src/hash.rs | 55 + consensus/client/src/header.rs | 290 +++ consensus/{wasm => client}/src/imports.rs | 1 - consensus/client/src/input.rs | 220 ++ consensus/client/src/lib.rs | 33 + consensus/{wasm => client}/src/outpoint.rs | 45 +- consensus/{wasm => client}/src/output.rs | 75 +- consensus/client/src/result.rs | 1 + consensus/client/src/script.rs | 457 ++++ consensus/client/src/serializable/mod.rs | 79 + consensus/client/src/serializable/numeric.rs | 349 +++ consensus/client/src/serializable/string.rs | 346 +++ consensus/client/src/sign.rs | 73 + consensus/client/src/signing.rs | 216 ++ consensus/client/src/transaction.rs | 438 ++++ consensus/{wasm => client}/src/utxo.rs | 196 +- consensus/client/src/vtx.rs | 35 + consensus/core/Cargo.toml | 8 +- consensus/core/src/block.rs | 3 +- consensus/core/src/hashing/mod.rs | 2 +- consensus/core/src/hashing/sighash.rs | 16 +- consensus/core/src/hashing/sighash_type.rs | 3 + consensus/core/src/header.rs | 199 +- consensus/core/src/network.rs | 151 +- consensus/core/src/sign.rs | 16 +- consensus/core/src/subnets.rs | 37 +- consensus/core/src/tx.rs | 3 +- consensus/core/src/tx/script_public_key.rs | 42 +- consensus/pow/Cargo.toml | 6 +- consensus/pow/src/lib.rs | 4 +- consensus/pow/src/wasm.rs | 34 +- .../transaction_validator_populated.rs | 2 +- consensus/wasm/Cargo.toml | 18 +- consensus/wasm/src/error.rs | 3 + consensus/wasm/src/input.rs | 129 - consensus/wasm/src/keypair.rs | 256 -- consensus/wasm/src/lib.rs | 31 +- consensus/wasm/src/signable.rs | 154 -- consensus/wasm/src/signer.rs | 90 - consensus/wasm/src/transaction.rs | 265 -- consensus/wasm/src/txscript.rs | 91 - core/src/log/mod.rs | 50 +- crypto/addresses/Cargo.toml | 4 + crypto/addresses/src/lib.rs | 72 +- crypto/hashes/src/lib.rs | 35 +- crypto/txscript/src/lib.rs | 4 +- crypto/txscript/src/standard/multisig.rs | 14 +- kos/.gitignore | 4 - kos/Cargo.toml | 68 - kos/README.md | 117 - kos/app/index.html | 29 - kos/app/metrics.html | 23 - kos/build | 3 - kos/build.ps1 | 7 - kos/index.js | 11 - kos/linux-deps.sh | 2 - kos/nw.toml | 65 - kos/package | 24 - kos/package.json | 7 - kos/package.ps1 | 6 - kos/resources/icons/tray-icon@2x.png | Bin 3046 -> 0 bytes kos/resources/setup/application.png | Bin 76414 -> 0 bytes kos/resources/setup/document.png | Bin 76932 -> 0 bytes .../setup/innosetup-wizard-large.png | Bin 7606 -> 0 bytes .../setup/innosetup-wizard-small.png | Bin 3640 -> 0 bytes kos/resources/setup/macos-application.png | Bin 76237 -> 0 bytes .../setup/macos-disk-image-background.png | Bin 21770 -> 0 bytes kos/run | 6 - kos/src/core/core.rs | 654 ----- kos/src/core/ipc.rs | 202 -- kos/src/core/mod.rs | 6 - kos/src/core/settings.rs | 14 - kos/src/error.rs | 107 - kos/src/imports.rs | 30 - kos/src/ipc.rs | 18 - kos/src/layout.rs | 69 - kos/src/lib.rs | 9 - kos/src/metrics/ipc.rs | 25 - kos/src/metrics/metrics.rs | 186 -- kos/src/metrics/mod.rs | 7 - kos/src/metrics/settings.rs | 14 - kos/src/metrics/toolbar.css | 36 - kos/src/metrics/toolbar.rs | 356 --- kos/src/modules/exit.rs | 20 - kos/src/modules/metrics.rs | 91 - kos/src/modules/mod.rs | 12 - kos/src/result.rs | 1 - kos/src/terminal/ipc.rs | 69 - kos/src/terminal/mod.rs | 6 - kos/src/terminal/settings.rs | 16 - kos/src/terminal/terminal.rs | 285 --- metrics/core/src/lib.rs | 26 +- notify/src/address/tracker.rs | 4 +- notify/src/broadcaster.rs | 2 +- notify/src/error.rs | 3 + notify/src/events.rs | 55 +- protocol/flows/src/flowcontext/orphans.rs | 6 +- protocol/p2p/src/convert/error.rs | 8 +- rothschild/src/main.rs | 8 +- rpc/core/Cargo.toml | 11 + rpc/core/src/api/ctl.rs | 30 +- rpc/core/src/api/notifications.rs | 2 +- rpc/core/src/error.rs | 20 +- rpc/core/src/model/block.rs | 41 +- rpc/core/src/model/mempool.rs | 20 + rpc/core/src/model/message.rs | 4 +- rpc/core/src/model/tx.rs | 2 + rpc/core/src/wasm/convert.rs | 77 + rpc/core/src/wasm/message.rs | 1385 ++++++++++ rpc/core/src/wasm/mod.rs | 83 +- rpc/macros/src/handler.rs | 24 +- rpc/macros/src/lib.rs | 6 + rpc/macros/src/wrpc/wasm.rs | 205 +- rpc/wrpc/client/Cargo.toml | 6 +- rpc/wrpc/client/Resolvers.toml | 11 + rpc/wrpc/client/src/client.rs | 428 +++- rpc/wrpc/client/src/error.rs | 21 +- rpc/wrpc/client/src/imports.rs | 7 + rpc/wrpc/client/src/lib.rs | 6 +- rpc/wrpc/client/src/node.rs | 44 + rpc/wrpc/client/src/prelude.rs | 7 + rpc/wrpc/client/src/resolver.rs | 120 + rpc/wrpc/client/src/wasm.rs | 383 --- rpc/wrpc/examples/subscriber/Cargo.toml | 22 + rpc/wrpc/examples/subscriber/src/main.rs | 289 +++ rpc/wrpc/resolver/Cargo.toml | 41 + rpc/wrpc/resolver/src/args.rs | 54 + rpc/wrpc/resolver/src/connection.rs | 262 ++ rpc/wrpc/resolver/src/error.rs | 53 + rpc/wrpc/resolver/src/imports.rs | 28 + rpc/wrpc/resolver/src/log.rs | 44 + rpc/wrpc/resolver/src/main.rs | 41 + rpc/wrpc/resolver/src/monitor.rs | 241 ++ rpc/wrpc/resolver/src/node.rs | 75 + rpc/wrpc/resolver/src/panic.rs | 10 + rpc/wrpc/resolver/src/params.rs | 146 ++ rpc/wrpc/resolver/src/result.rs | 1 + rpc/wrpc/resolver/src/server.rs | 149 ++ rpc/wrpc/resolver/src/transport.rs | 8 + rpc/wrpc/wasm/Cargo.toml | 29 + rpc/wrpc/wasm/build-node | 4 +- rpc/wrpc/wasm/build-web | 4 +- rpc/wrpc/wasm/nodejs/index.js | 21 +- rpc/wrpc/wasm/src/client.rs | 1052 ++++++++ rpc/wrpc/wasm/src/imports.rs | 40 + rpc/wrpc/wasm/src/lib.rs | 18 +- rpc/wrpc/wasm/src/notify.rs | 239 ++ rpc/wrpc/wasm/src/resolver.rs | 208 ++ rpc/wrpc/wasm/web/index.html | 3 +- simpa/src/simulator/miner.rs | 2 +- test | 3 + testing/integration/src/common/utils.rs | 6 +- .../src/daemon_integration_tests.rs | 2 +- testing/integration/src/mempool_benchmarks.rs | 4 +- .../integration/src/subscribe_benchmarks.rs | 2 +- testing/integration/src/tasks/daemon.rs | 2 +- utils/Cargo.toml | 1 + utils/src/networking.rs | 30 + wallet/bip32/src/lib.rs | 1 - wallet/bip32/src/mnemonic/language.rs | 9 +- wallet/bip32/src/mnemonic/phrase.rs | 6 + wallet/bip32/src/prefix.rs | 3 + wallet/bip32/src/wasm/mod.rs | 7 - wallet/bip32/src/wasm/xprv.rs | 49 - wallet/bip32/src/wasm/xpub.rs | 51 - wallet/bip32/src/xprivate_key.rs | 2 +- wallet/bip32/src/xpublic_key.rs | 2 +- wallet/core/Cargo.toml | 24 +- wallet/core/src/account/descriptor.rs | 76 +- wallet/core/src/account/kind.rs | 20 +- wallet/core/src/account/mod.rs | 26 +- wallet/core/src/api/message.rs | 122 +- wallet/core/src/api/traits.rs | 76 +- wallet/core/src/api/transport.rs | 140 +- .../gen0/import.rs => compat/gen0.rs} | 3 +- wallet/core/src/compat/gen1.rs | 224 ++ wallet/core/src/compat/mod.rs | 4 + wallet/core/src/cryptobox.rs | 41 + .../src/{derivation/mod.rs => derivation.rs} | 57 +- wallet/core/src/derivation/gen1/import.rs | 7 - wallet/core/src/deterministic.rs | 21 + wallet/core/src/encryption.rs | 48 +- wallet/core/src/error.rs | 49 +- wallet/core/src/events.rs | 212 +- wallet/core/src/imports.rs | 16 +- wallet/core/src/lib.rs | 42 +- wallet/core/src/message.rs | 10 +- wallet/core/src/metrics.rs | 44 + wallet/core/src/prelude.rs | 3 +- wallet/core/src/rpc.rs | 1 + wallet/core/src/settings.rs | 2 +- wallet/core/src/storage/interface.rs | 34 +- wallet/core/src/storage/keydata/id.rs | 14 + wallet/core/src/storage/keydata/info.rs | 24 + wallet/core/src/storage/local/cache.rs | 2 - wallet/core/src/storage/local/interface.rs | 10 +- wallet/core/src/storage/local/mod.rs | 6 +- wallet/core/src/storage/local/payload.rs | 2 - wallet/core/src/storage/local/storage.rs | 17 +- .../src/storage/local/transaction/fsio.rs | 2 - wallet/core/src/storage/local/wallet.rs | 7 +- wallet/core/src/storage/mod.rs | 2 - wallet/core/src/storage/transaction/data.rs | 22 +- wallet/core/src/storage/transaction/kind.rs | 43 + wallet/core/src/storage/transaction/record.rs | 356 ++- wallet/core/src/storage/transaction/utxo.rs | 6 +- wallet/core/src/tx/fees.rs | 15 - wallet/core/src/tx/generator/generator.rs | 44 +- wallet/core/src/tx/generator/pending.rs | 16 +- wallet/core/src/tx/generator/settings.rs | 18 + wallet/core/src/tx/generator/signer.rs | 14 +- wallet/core/src/tx/generator/test.rs | 8 +- wallet/core/src/tx/mass.rs | 2 +- wallet/core/src/tx/mod.rs | 10 +- wallet/core/src/tx/payment.rs | 105 +- wallet/core/src/utxo/balance.rs | 60 +- wallet/core/src/utxo/context.rs | 35 +- wallet/core/src/utxo/iterator.rs | 1 + wallet/core/src/utxo/mod.rs | 2 +- wallet/core/src/utxo/outgoing.rs | 4 +- wallet/core/src/utxo/processor.rs | 132 +- wallet/core/src/utxo/reference.rs | 2 +- wallet/core/src/utxo/scan.rs | 2 +- wallet/core/src/utxo/settings.rs | 1 + wallet/core/src/utxo/sync.rs | 4 +- wallet/core/src/wallet/api.rs | 143 +- wallet/core/src/wallet/args.rs | 30 +- wallet/core/src/wallet/mod.rs | 435 ++-- wallet/core/src/wasm/api/extensions.rs | 72 + wallet/core/src/wasm/api/message.rs | 1764 +++++++++++++ wallet/core/src/wasm/api/mod.rs | 55 + wallet/core/src/wasm/balance.rs | 36 +- wallet/core/src/wasm/cryptobox.rs | 138 + wallet/core/src/wasm/encryption.rs | 89 + wallet/core/src/wasm/events.rs | 19 + wallet/core/src/wasm/message.rs | 70 +- wallet/core/src/wasm/mod.rs | 48 +- wallet/core/src/wasm/notify.rs | 733 ++++++ wallet/core/src/wasm/signer.rs | 81 + wallet/core/src/wasm/tx/consensus.rs | 3 + wallet/core/src/wasm/tx/fees.rs | 54 + .../core/src/wasm/tx/generator/generator.rs | 152 +- wallet/core/src/wasm/tx/generator/pending.rs | 64 +- wallet/core/src/wasm/tx/generator/summary.rs | 9 + wallet/core/src/wasm/tx/mass.rs | 20 +- wallet/core/src/wasm/tx/mod.rs | 10 +- wallet/core/src/wasm/tx/utils.rs | 86 +- wallet/core/src/wasm/utils.rs | 37 +- wallet/core/src/wasm/utxo/context.rs | 226 +- wallet/core/src/wasm/utxo/processor.rs | 289 ++- wallet/core/src/wasm/wallet/account.rs | 33 +- wallet/core/src/wasm/wallet/keydata.rs | 1 + wallet/core/src/wasm/wallet/mod.rs | 3 +- wallet/core/src/wasm/wallet/storage.rs | 2 +- wallet/core/src/wasm/wallet/wallet.rs | 566 ++-- wallet/core/src/wasm/xpublickey.rs | 58 - wallet/keys/Cargo.toml | 50 + .../{core => keys}/src/derivation/gen0/hd.rs | 15 +- .../{core => keys}/src/derivation/gen0/mod.rs | 1 - .../{core => keys}/src/derivation/gen1/hd.rs | 25 +- .../{core => keys}/src/derivation/gen1/mod.rs | 1 - wallet/keys/src/derivation/mod.rs | 3 + .../{core => keys}/src/derivation/traits.rs | 0 .../src/wasm => keys/src}/derivation_path.rs | 35 +- wallet/keys/src/error.rs | 117 + wallet/keys/src/imports.rs | 28 + wallet/keys/src/keypair.rs | 106 + wallet/keys/src/lib.rs | 15 + wallet/keys/src/prelude.rs | 10 + wallet/keys/src/privatekey.rs | 108 + .../xprivatekey.rs => keys/src/privkeygen.rs} | 30 +- wallet/keys/src/pubkeygen.rs | 219 ++ wallet/keys/src/publickey.rs | 251 ++ wallet/keys/src/result.rs | 6 + wallet/{core => keys}/src/secret.rs | 8 +- wallet/{core => keys}/src/types.rs | 0 wallet/keys/src/xprv.rs | 98 + wallet/keys/src/xpub.rs | 83 + wallet/macros/src/handler.rs | 34 +- wallet/macros/src/lib.rs | 16 +- wallet/macros/src/wallet/client.rs | 12 +- wallet/macros/src/wallet/mod.rs | 3 +- wallet/macros/src/wallet/server.rs | 4 +- wallet/macros/src/wallet/wasm.rs | 226 +- wasm/.gitignore | 10 +- wasm/CHANGELOG.md | 94 +- wasm/Cargo.toml | 28 +- wasm/LICENSE | 15 + wasm/README.md | 150 +- wasm/build-docs | 30 +- wasm/build-node | 5 +- wasm/build-node-dev | 8 + wasm/build-release | 90 + wasm/build-web | 29 +- wasm/build-web-dev | 12 + wasm/build/docs/kaspa-core.ts | 1 + wasm/build/docs/kaspa-keygen.ts | 1 + wasm/build/docs/kaspa-rpc.ts | 1 + wasm/build/docs/kaspa.ts | 1 + wasm/build/docs/tsconfig.json | 109 + wasm/build/docs/typedoc.json | 7 + wasm/build/package-sizes.js | 56 + wasm/core/Cargo.toml | 20 + wasm/core/src/events.rs | 65 + wasm/core/src/lib.rs | 4 + wasm/core/src/types.rs | 57 + wasm/examples/.gitignore | 1 + wasm/examples/.nojekyll | 1 + wasm/examples/browser-extension/README.md | 0 wasm/examples/browser-extension/index.html | 102 + .../browser-extension/resources/style.css | 18 + wasm/examples/browser-extension/server.js | 1 + wasm/examples/data/.gitignore | 1 + wasm/examples/init.js | 119 + wasm/examples/jsconfig.json | 9 + wasm/examples/nodejs/javascript/.gitignore | 2 + .../nodejs/javascript/general}/addresses.js | 6 +- .../nodejs/javascript/general/derivation.js | 67 + .../nodejs/javascript/general}/encryption.js | 2 +- .../general/get-balances-by-addresses.js | 36 + .../javascript/general/message-signing.js} | 6 +- .../javascript/general}/mining-header.js | 6 +- .../javascript/general}/mining-state.js | 4 +- .../nodejs/javascript/general}/mnemonic.js | 2 +- .../nodejs/javascript/general/resolver.js | 44 + .../nodejs/javascript/general}/rpc.js | 19 +- .../javascript/general/subscribe-daa-score.js | 51 + .../nodejs/javascript}/refactoring/storage.js | 0 .../javascript}/refactoring/tx-create.js | 6 +- .../javascript}/refactoring/tx-script-sign.js | 8 +- .../javascript/transactions}/estimate.js | 19 +- .../javascript/transactions}/generator.js | 26 +- .../transactions}/simple-transaction.js | 20 +- .../transactions/single-transaction-demo.js} | 28 +- .../transactions}/utxo-context-generator.js | 28 +- .../transactions}/utxo-context-listener.js | 44 +- wasm/examples/nodejs/javascript/utils.js | 90 + wasm/examples/nodejs/javascript/version.js | 2 + .../nodejs/javascript/wallet/wallet.js | 256 ++ wasm/examples/nodejs/typescript/.gitignore | 1 + wasm/examples/nodejs/typescript/README.md | 29 + .../examples/nodejs/typescript/src/address.ts | 68 + .../nodejs/typescript/src/utils.ts} | 22 +- .../typescript/src/utxo-context-listener.ts | 109 + .../examples/nodejs/typescript/src/version.ts | 3 + wasm/examples/nodejs/typescript/src/wallet.ts | 16 + wasm/examples/nodejs/typescript/tsconfig.json | 113 + wasm/examples/package.json | 12 + wasm/examples/web/browser-extension.html | 106 + wasm/examples/web/get-block-dag-info.html | 37 + wasm/examples/web/get-server-info.html | 37 + wasm/examples/web/index.html | 89 + wasm/examples/web/resources/ferris.svg | 22 + wasm/examples/web/resources/kaspa.svg | 24 + wasm/examples/web/resources/rust.svg | 57 + wasm/examples/web/resources/style.css | 116 + wasm/examples/web/resources/utils.js | 136 + wasm/examples/web/resources/wasm.svg | 8 + wasm/examples/web/subscribe-block-added.html | 64 + wasm/examples/web/subscribe-daa-changed.html | 58 + wasm/examples/web/utxo-context.html | 97 + wasm/examples/web/xpub.html | 54 + wasm/index.html | 9 + wasm/nodejs/.gitignore | 3 +- wasm/nodejs/derivation.js | 33 - wasm/nodejs/package.json | 7 - wasm/npm/LICENSE | 210 +- wasm/npm/README.md | 25 +- wasm/npm/index.js | 3 +- wasm/npm/package.json | 9 +- wasm/src/lib.rs | 117 +- wasm/src/utils.rs | 30 - wasm/src/version.rs | 8 + wasm/web/.gitignore | 1 + 396 files changed, 21297 insertions(+), 7823 deletions(-) create mode 100644 consensus/client/Cargo.toml create mode 100644 consensus/client/src/error.rs create mode 100644 consensus/client/src/hash.rs create mode 100644 consensus/client/src/header.rs rename consensus/{wasm => client}/src/imports.rs (92%) create mode 100644 consensus/client/src/input.rs create mode 100644 consensus/client/src/lib.rs rename consensus/{wasm => client}/src/outpoint.rs (77%) rename consensus/{wasm => client}/src/output.rs (61%) create mode 100644 consensus/client/src/result.rs create mode 100644 consensus/client/src/script.rs create mode 100644 consensus/client/src/serializable/mod.rs create mode 100644 consensus/client/src/serializable/numeric.rs create mode 100644 consensus/client/src/serializable/string.rs create mode 100644 consensus/client/src/sign.rs create mode 100644 consensus/client/src/signing.rs create mode 100644 consensus/client/src/transaction.rs rename consensus/{wasm => client}/src/utxo.rs (57%) create mode 100644 consensus/client/src/vtx.rs delete mode 100644 consensus/wasm/src/input.rs delete mode 100644 consensus/wasm/src/keypair.rs delete mode 100644 consensus/wasm/src/signable.rs delete mode 100644 consensus/wasm/src/signer.rs delete mode 100644 consensus/wasm/src/transaction.rs delete mode 100644 consensus/wasm/src/txscript.rs delete mode 100644 kos/.gitignore delete mode 100644 kos/Cargo.toml delete mode 100644 kos/README.md delete mode 100644 kos/app/index.html delete mode 100644 kos/app/metrics.html delete mode 100755 kos/build delete mode 100644 kos/build.ps1 delete mode 100644 kos/index.js delete mode 100644 kos/linux-deps.sh delete mode 100644 kos/nw.toml delete mode 100755 kos/package delete mode 100644 kos/package.json delete mode 100644 kos/package.ps1 delete mode 100644 kos/resources/icons/tray-icon@2x.png delete mode 100644 kos/resources/setup/application.png delete mode 100644 kos/resources/setup/document.png delete mode 100644 kos/resources/setup/innosetup-wizard-large.png delete mode 100644 kos/resources/setup/innosetup-wizard-small.png delete mode 100644 kos/resources/setup/macos-application.png delete mode 100644 kos/resources/setup/macos-disk-image-background.png delete mode 100755 kos/run delete mode 100644 kos/src/core/core.rs delete mode 100644 kos/src/core/ipc.rs delete mode 100644 kos/src/core/mod.rs delete mode 100644 kos/src/core/settings.rs delete mode 100644 kos/src/error.rs delete mode 100644 kos/src/imports.rs delete mode 100644 kos/src/ipc.rs delete mode 100644 kos/src/layout.rs delete mode 100644 kos/src/lib.rs delete mode 100644 kos/src/metrics/ipc.rs delete mode 100644 kos/src/metrics/metrics.rs delete mode 100644 kos/src/metrics/mod.rs delete mode 100644 kos/src/metrics/settings.rs delete mode 100644 kos/src/metrics/toolbar.css delete mode 100644 kos/src/metrics/toolbar.rs delete mode 100644 kos/src/modules/exit.rs delete mode 100644 kos/src/modules/metrics.rs delete mode 100644 kos/src/modules/mod.rs delete mode 100644 kos/src/result.rs delete mode 100644 kos/src/terminal/ipc.rs delete mode 100644 kos/src/terminal/mod.rs delete mode 100644 kos/src/terminal/settings.rs delete mode 100644 kos/src/terminal/terminal.rs create mode 100644 rpc/core/src/wasm/convert.rs create mode 100644 rpc/core/src/wasm/message.rs create mode 100644 rpc/wrpc/client/Resolvers.toml create mode 100644 rpc/wrpc/client/src/node.rs create mode 100644 rpc/wrpc/client/src/prelude.rs create mode 100644 rpc/wrpc/client/src/resolver.rs delete mode 100644 rpc/wrpc/client/src/wasm.rs create mode 100644 rpc/wrpc/examples/subscriber/Cargo.toml create mode 100644 rpc/wrpc/examples/subscriber/src/main.rs create mode 100644 rpc/wrpc/resolver/Cargo.toml create mode 100644 rpc/wrpc/resolver/src/args.rs create mode 100644 rpc/wrpc/resolver/src/connection.rs create mode 100644 rpc/wrpc/resolver/src/error.rs create mode 100644 rpc/wrpc/resolver/src/imports.rs create mode 100644 rpc/wrpc/resolver/src/log.rs create mode 100644 rpc/wrpc/resolver/src/main.rs create mode 100644 rpc/wrpc/resolver/src/monitor.rs create mode 100644 rpc/wrpc/resolver/src/node.rs create mode 100644 rpc/wrpc/resolver/src/panic.rs create mode 100644 rpc/wrpc/resolver/src/params.rs create mode 100644 rpc/wrpc/resolver/src/result.rs create mode 100644 rpc/wrpc/resolver/src/server.rs create mode 100644 rpc/wrpc/resolver/src/transport.rs create mode 100644 rpc/wrpc/wasm/src/client.rs create mode 100644 rpc/wrpc/wasm/src/imports.rs create mode 100644 rpc/wrpc/wasm/src/notify.rs create mode 100644 rpc/wrpc/wasm/src/resolver.rs delete mode 100644 wallet/bip32/src/wasm/mod.rs delete mode 100644 wallet/bip32/src/wasm/xprv.rs delete mode 100644 wallet/bip32/src/wasm/xpub.rs rename wallet/core/src/{derivation/gen0/import.rs => compat/gen0.rs} (99%) create mode 100644 wallet/core/src/compat/gen1.rs create mode 100644 wallet/core/src/compat/mod.rs create mode 100644 wallet/core/src/cryptobox.rs rename wallet/core/src/{derivation/mod.rs => derivation.rs} (90%) delete mode 100644 wallet/core/src/derivation/gen1/import.rs create mode 100644 wallet/core/src/metrics.rs create mode 100644 wallet/core/src/wasm/api/extensions.rs create mode 100644 wallet/core/src/wasm/api/message.rs create mode 100644 wallet/core/src/wasm/api/mod.rs create mode 100644 wallet/core/src/wasm/cryptobox.rs create mode 100644 wallet/core/src/wasm/encryption.rs create mode 100644 wallet/core/src/wasm/events.rs create mode 100644 wallet/core/src/wasm/notify.rs create mode 100644 wallet/core/src/wasm/signer.rs create mode 100644 wallet/core/src/wasm/tx/fees.rs delete mode 100644 wallet/core/src/wasm/xpublickey.rs create mode 100644 wallet/keys/Cargo.toml rename wallet/{core => keys}/src/derivation/gen0/hd.rs (98%) rename wallet/{core => keys}/src/derivation/gen0/mod.rs (90%) rename wallet/{core => keys}/src/derivation/gen1/hd.rs (97%) rename wallet/{core => keys}/src/derivation/gen1/mod.rs (90%) create mode 100644 wallet/keys/src/derivation/mod.rs rename wallet/{core => keys}/src/derivation/traits.rs (100%) rename wallet/{bip32/src/wasm => keys/src}/derivation_path.rs (62%) create mode 100644 wallet/keys/src/error.rs create mode 100644 wallet/keys/src/imports.rs create mode 100644 wallet/keys/src/keypair.rs create mode 100644 wallet/keys/src/lib.rs create mode 100644 wallet/keys/src/prelude.rs create mode 100644 wallet/keys/src/privatekey.rs rename wallet/{core/src/wasm/xprivatekey.rs => keys/src/privkeygen.rs} (52%) create mode 100644 wallet/keys/src/pubkeygen.rs create mode 100644 wallet/keys/src/publickey.rs create mode 100644 wallet/keys/src/result.rs rename wallet/{core => keys}/src/secret.rs (91%) rename wallet/{core => keys}/src/types.rs (100%) create mode 100644 wallet/keys/src/xprv.rs create mode 100644 wallet/keys/src/xpub.rs create mode 100644 wasm/LICENSE create mode 100755 wasm/build-node-dev create mode 100755 wasm/build-release create mode 100755 wasm/build-web-dev create mode 100644 wasm/build/docs/kaspa-core.ts create mode 100644 wasm/build/docs/kaspa-keygen.ts create mode 100644 wasm/build/docs/kaspa-rpc.ts create mode 100644 wasm/build/docs/kaspa.ts create mode 100644 wasm/build/docs/tsconfig.json create mode 100644 wasm/build/docs/typedoc.json create mode 100644 wasm/build/package-sizes.js create mode 100644 wasm/core/Cargo.toml create mode 100644 wasm/core/src/events.rs create mode 100644 wasm/core/src/lib.rs create mode 100644 wasm/core/src/types.rs create mode 100644 wasm/examples/.gitignore create mode 100644 wasm/examples/.nojekyll create mode 100644 wasm/examples/browser-extension/README.md create mode 100644 wasm/examples/browser-extension/index.html create mode 100644 wasm/examples/browser-extension/resources/style.css create mode 100644 wasm/examples/browser-extension/server.js create mode 100644 wasm/examples/data/.gitignore create mode 100644 wasm/examples/init.js create mode 100644 wasm/examples/jsconfig.json create mode 100644 wasm/examples/nodejs/javascript/.gitignore rename wasm/{nodejs => examples/nodejs/javascript/general}/addresses.js (96%) create mode 100644 wasm/examples/nodejs/javascript/general/derivation.js rename wasm/{nodejs => examples/nodejs/javascript/general}/encryption.js (85%) create mode 100644 wasm/examples/nodejs/javascript/general/get-balances-by-addresses.js rename wasm/{nodejs/message_signing.js => examples/nodejs/javascript/general/message-signing.js} (74%) rename wasm/{nodejs => examples/nodejs/javascript/general}/mining-header.js (92%) rename wasm/{nodejs => examples/nodejs/javascript/general}/mining-state.js (94%) rename wasm/{nodejs => examples/nodejs/javascript/general}/mnemonic.js (93%) create mode 100644 wasm/examples/nodejs/javascript/general/resolver.js rename wasm/{nodejs => examples/nodejs/javascript/general}/rpc.js (54%) create mode 100644 wasm/examples/nodejs/javascript/general/subscribe-daa-score.js rename wasm/{nodejs => examples/nodejs/javascript}/refactoring/storage.js (100%) rename wasm/{nodejs => examples/nodejs/javascript}/refactoring/tx-create.js (97%) rename wasm/{nodejs => examples/nodejs/javascript}/refactoring/tx-script-sign.js (94%) rename wasm/{nodejs => examples/nodejs/javascript/transactions}/estimate.js (82%) rename wasm/{nodejs => examples/nodejs/javascript/transactions}/generator.js (79%) rename wasm/{nodejs => examples/nodejs/javascript/transactions}/simple-transaction.js (76%) rename wasm/{nodejs/demo.js => examples/nodejs/javascript/transactions/single-transaction-demo.js} (76%) rename wasm/{nodejs => examples/nodejs/javascript/transactions}/utxo-context-generator.js (77%) rename wasm/{nodejs => examples/nodejs/javascript/transactions}/utxo-context-listener.js (51%) create mode 100644 wasm/examples/nodejs/javascript/utils.js create mode 100644 wasm/examples/nodejs/javascript/version.js create mode 100644 wasm/examples/nodejs/javascript/wallet/wallet.js create mode 100644 wasm/examples/nodejs/typescript/.gitignore create mode 100644 wasm/examples/nodejs/typescript/README.md create mode 100644 wasm/examples/nodejs/typescript/src/address.ts rename wasm/{nodejs/utils.js => examples/nodejs/typescript/src/utils.ts} (88%) create mode 100644 wasm/examples/nodejs/typescript/src/utxo-context-listener.ts create mode 100644 wasm/examples/nodejs/typescript/src/version.ts create mode 100644 wasm/examples/nodejs/typescript/src/wallet.ts create mode 100644 wasm/examples/nodejs/typescript/tsconfig.json create mode 100644 wasm/examples/package.json create mode 100644 wasm/examples/web/browser-extension.html create mode 100644 wasm/examples/web/get-block-dag-info.html create mode 100644 wasm/examples/web/get-server-info.html create mode 100644 wasm/examples/web/index.html create mode 100644 wasm/examples/web/resources/ferris.svg create mode 100644 wasm/examples/web/resources/kaspa.svg create mode 100644 wasm/examples/web/resources/rust.svg create mode 100644 wasm/examples/web/resources/style.css create mode 100644 wasm/examples/web/resources/utils.js create mode 100644 wasm/examples/web/resources/wasm.svg create mode 100644 wasm/examples/web/subscribe-block-added.html create mode 100644 wasm/examples/web/subscribe-daa-changed.html create mode 100644 wasm/examples/web/utxo-context.html create mode 100644 wasm/examples/web/xpub.html create mode 100644 wasm/index.html delete mode 100644 wasm/nodejs/derivation.js delete mode 100644 wasm/nodejs/package.json delete mode 100644 wasm/src/utils.rs create mode 100644 wasm/src/version.rs create mode 100644 wasm/web/.gitignore diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 81b821c93..b6830560b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v3 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -87,7 +87,7 @@ jobs: run: git config --global core.autocrlf false - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -183,7 +183,7 @@ jobs: uses: actions/checkout@v3 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -218,7 +218,7 @@ jobs: uses: actions/checkout@v3 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -281,7 +281,7 @@ jobs: uses: actions/checkout@v3 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -317,6 +317,14 @@ jobs: - name: Add wasm32 target run: rustup target add wasm32-unknown-unknown + - name: Install NodeJS + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install NodeJS dependencies + run: npm install --global typedoc typescript + - name: Cache uses: actions/cache@v3 with: @@ -328,8 +336,8 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Build wasm pack for nodejs - run: cd wasm && wasm-pack build --target nodejs --out-dir nodejs/kaspa --features full + - name: Build wasm release + run: cd wasm && bash build-release build-release: name: Build Ubuntu Release @@ -339,7 +347,7 @@ jobs: uses: actions/checkout@v3 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index f84d22375..567adb557 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v3 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -116,7 +116,7 @@ jobs: uses: actions/checkout@v3 - name: Install Protoc - uses: arduino/setup-protoc@v1 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -153,6 +153,14 @@ jobs: - name: Add wasm32 target run: rustup target add wasm32-unknown-unknown + - name: Install NodeJS + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install NodeJS dependencies + run: npm install --global typedoc typescript + - name: Cache uses: actions/cache@v3 with: @@ -167,37 +175,20 @@ jobs: - name: Build WASM32 SDK run: | cd wasm - wasm-pack build --target nodejs --out-dir wasm32-nodejs-sdk/kaspa - wasm-pack build --target web --out-dir wasm32-web-sdk/kaspa - cd .. - mkdir release || true - archive_nodejs="release/rusty-kaspa-wasm32-sdk-${{ github.event.release.tag_name }}-nodejs.zip" - archive_web="release/rusty-kaspa-wasm32-sdk-${{ github.event.release.tag_name }}-web.zip" - asset_name_nodejs="rusty-kaspa-wasm32-sdk-${{ github.event.release.tag_name }}-nodejs.zip" - asset_name_web="rusty-kaspa-wasm32-sdk-${{ github.event.release.tag_name }}-web.zip" - zip -jr "${archive_nodejs}" ./wasm/wasm32-nodejs-sdk/* - zip -jr "${archive_web}" ./wasm/wasm32-web-sdk/* - echo "archive_nodejs=${archive_nodejs}" >> $GITHUB_ENV - echo "archive_web=${archive_web}" >> $GITHUB_ENV - echo "asset_name_nodejs=${asset_name_nodejs}" >> $GITHUB_ENV - echo "asset_name_web=${asset_name_web}" >> $GITHUB_ENV - - - name: Upload WASM32 NODEJS SDK - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: "./${{ env.archive_nodejs }}" - asset_name: "${{ env.asset_name_nodejs }}" - asset_content_type: application/zip + bash build-release + mv release/kaspa-wasm32-sdk.zip ../kaspa-wasm32-sdk-${{ github.event.release.tag_name }}.zip - - name: Upload WASM WEB SDK + archive="kaspa-wasm32-sdk-${{ github.event.release.tag_name }}.zip" + asset_name="kaspa-wasm32-sdk-${{ github.event.release.tag_name }}.zip" + echo "archive=${archive}" >> $GITHUB_ENV + echo "asset_name=${asset_name}" >> $GITHUB_ENV + + - name: Upload WASM32 SDK uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} - asset_path: "./${{ env.archive_web }}" - asset_name: "${{ env.asset_name_web }}" + asset_path: "./${{ env.archive }}" + asset_name: "${{ env.asset_name }}" asset_content_type: application/zip diff --git a/.gitignore b/.gitignore index 631e3651d..0199232fe 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ analyzer-target *.code-workspace /setup testing/integration/perflogs* +Servers.toml +release +package-sizes.js diff --git a/Cargo.lock b/Cargo.lock index 12be5a4e1..22cd64f4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -41,9 +41,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if 1.0.0", "cipher", @@ -52,23 +52,23 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if 1.0.0", - "getrandom 0.2.11", + "getrandom 0.2.14", "once_cell", "version_check", "zerocopy", @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -126,47 +126,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -174,9 +175,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "approx" @@ -189,15 +190,15 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "argon2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", @@ -240,28 +241,27 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" dependencies = [ "concurrent-queue", - "event-listener 4.0.0", - "event-listener-strategy", + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ - "async-lock 3.2.0", "async-task", "concurrent-queue", - "fastrand 2.0.1", - "futures-lite 2.1.0", + "fastrand 2.1.0", + "futures-lite 2.3.0", "slab", ] @@ -271,12 +271,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-executor", - "async-io 2.2.2", - "async-lock 3.2.0", + "async-io 2.3.2", + "async-lock 3.3.0", "blocking", - "futures-lite 2.1.0", + "futures-lite 2.3.0", "once_cell", ] @@ -302,18 +302,18 @@ dependencies = [ [[package]] name = "async-io" -version = "2.2.2" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afaa937395a620e33dc6a742c593c01aced20aa376ffb0f628121198578ccc7" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" dependencies = [ - "async-lock 3.2.0", + "async-lock 3.3.0", "cfg-if 1.0.0", "concurrent-queue", "futures-io", - "futures-lite 2.1.0", + "futures-lite 2.3.0", "parking", - "polling 3.3.1", - "rustix 0.38.28", + "polling 3.7.0", + "rustix 0.38.34", "slab", "tracing", "windows-sys 0.52.0", @@ -330,12 +330,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ - "event-listener 4.0.0", - "event-listener-strategy", + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", "pin-project-lite", ] @@ -385,24 +385,24 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "async-task" -version = "4.6.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -420,19 +420,13 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atomic_float" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62af46d040ba9df09edc6528dae9d8e49f5f3e82f55b7d2ec31a733c38dbc49d" - [[package]] name = "attohttpc" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" dependencies = [ - "http", + "http 0.2.12", "log", "url", ] @@ -450,9 +444,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -461,13 +455,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 0.1.2", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-util", "itoa", "matchit", "memchr", @@ -476,10 +499,15 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -491,19 +519,40 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -516,15 +565,15 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.21.5" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -582,7 +631,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -593,9 +642,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "blake2" @@ -628,18 +677,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ - "async-channel 2.1.1", - "async-lock 3.2.0", + "async-channel 2.2.1", + "async-lock 3.3.0", "async-task", - "fastrand 2.0.1", "futures-io", - "futures-lite 2.1.0", + "futures-lite 2.3.0", "piper", - "tracing", ] [[package]] @@ -689,9 +736,9 @@ dependencies = [ [[package]] name = "bs58" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ "sha2", "tinyvec", @@ -699,9 +746,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -711,9 +758,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bzip2-sys" @@ -726,6 +773,38 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cast" version = "0.3.0" @@ -734,12 +813,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -772,6 +852,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chacha20" version = "0.9.1" @@ -798,9 +884,9 @@ dependencies = [ [[package]] name = "chrome-sys" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011ddd3011a285f208817fc8fc6db655cda2e17f89de31d34b90559b5f2a8870" +checksum = "01c631c2cf4b95746cf065f732219ec0f2eb1497cd4c7fe07cb336ddf0d7c503" dependencies = [ "js-sys", "thiserror", @@ -810,9 +896,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -820,14 +906,14 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "ciborium" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -836,15 +922,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", @@ -863,9 +949,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", @@ -889,9 +975,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.11" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -899,60 +985,60 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.10.0", + "strsim 0.11.1", ] [[package]] name = "clap_derive" -version = "4.4.7" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -1010,18 +1096,18 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if 1.0.0", ] @@ -1035,7 +1121,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.11", + "clap 4.5.4", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1068,45 +1154,37 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "crossbeam-channel" -version = "0.5.9" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c3242926edf34aec4ac3a77108ad4854bffaa2e4ddc1824124ce59231302d5" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if 1.0.0", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if 1.0.0", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if 1.0.0", "crossbeam-utils", - "memoffset", ] [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" -dependencies = [ - "cfg-if 1.0.0", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" @@ -1114,7 +1192,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "crossterm_winapi", "libc", "mio", @@ -1133,6 +1211,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1144,14 +1228,72 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto_box" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" +dependencies = [ + "aead", + "chacha20", + "crypto_secretbox", + "curve25519-dalek", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "chacha20", + "cipher", + "generic-array 0.14.7", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + [[package]] name = "ctrlc" -version = "3.4.1" +version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ "nix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", ] [[package]] @@ -1175,7 +1317,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -1186,7 +1328,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -1196,7 +1338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if 1.0.0", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1204,9 +1346,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "delegate-display" @@ -1217,14 +1359,14 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -1262,15 +1404,15 @@ checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" [[package]] name = "deunicode" -version = "1.4.2" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a" +checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" [[package]] name = "dhat" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2aaf837aaf456f6706cb46386ba8dffd4013a757e36f4ea05c20dd46b209a3" +checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" dependencies = [ "backtrace", "lazy_static", @@ -1322,9 +1464,9 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "duration-string" @@ -1334,21 +1476,9 @@ checksum = "6fcc1d9ae294a15ed05aeae8e11ee5f2b3fe971c077d45a42fb20825fba6ee13" [[package]] name = "either" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - -[[package]] -name = "embed-doc-image" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af36f591236d9d822425cb6896595658fa558fcebf5ee8accac1d4b92c47166e" -dependencies = [ - "base64 0.13.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "encode_unicode" @@ -1356,6 +1486,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "enum-primitive-derive" version = "0.2.2" @@ -1404,9 +1543,20 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.0" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" dependencies = [ "concurrent-queue", "parking", @@ -1419,7 +1569,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.0", + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", "pin-project-lite", ] @@ -1441,7 +1601,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -1452,9 +1612,9 @@ checksum = "51e2ce894d53b295cf97b05685aa077950ff3e8541af83217fc720a6437169f8" [[package]] name = "faster-hex" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239f7bfb930f820ab16a9cd95afc26f88264cf6905c960b340a615384aa3338a" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" dependencies = [ "serde", ] @@ -1470,9 +1630,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fiat-crypto" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" [[package]] name = "filetime" @@ -1482,7 +1648,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -1494,18 +1660,18 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fixedstr" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f9274e2c5ec70bcbd4b7c181ebfcfdff70f23e480cfa0d446efc1033c7200e" +checksum = "5f4e4dfef7b590ab7d11e531d602fdfb6a3413b09924db1428902bbc4410a9a8" dependencies = [ "serde", ] [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1543,9 +1709,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1558,9 +1724,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1568,15 +1734,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1585,9 +1751,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1606,11 +1772,11 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -1619,32 +1785,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1675,6 +1841,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1690,9 +1857,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1727,17 +1894,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.22" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 2.1.0", + "http 0.2.12", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1746,9 +1913,13 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if 1.0.0", + "crunchy", +] [[package]] name = "hash32" @@ -1765,7 +1936,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash 0.7.7", + "ahash 0.7.8", ] [[package]] @@ -1776,9 +1947,12 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", +] [[package]] name = "heapless" @@ -1795,9 +1969,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1810,15 +1984,18 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-literal" @@ -1856,9 +2033,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1866,14 +2043,48 @@ dependencies = [ ] [[package]] -name = "http-body" -version = "0.4.6" +name = "http" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", - "http", - "pin-project-lite", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", + "pin-project-lite", ] [[package]] @@ -1911,36 +2122,84 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.7", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.28", "pin-project-lite", "tokio", "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.28", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2 0.5.7", + "tokio", +] + [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1977,16 +2236,16 @@ dependencies = [ [[package]] name = "igd-next" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e065e90a518ab5fedf79aa1e4b784e10f8e484a834f6bda85c42633a2cb7af" +checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4" dependencies = [ "async-trait", "attohttpc", "bytes", "futures", - "http", - "hyper", + "http 0.2.12", + "hyper 0.14.28", "log", "rand 0.8.5", "tokio", @@ -2024,12 +2283,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -2083,7 +2342,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -2096,15 +2355,21 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.3", - "rustix 0.38.28", - "windows-sys 0.48.0", + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -2123,33 +2388,42 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "kaspa-addresses" -version = "0.14.0" +version = "0.14.1" dependencies = [ "borsh", "criterion", @@ -2160,12 +2434,13 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-test", "web-sys", + "workflow-log", "workflow-wasm", ] [[package]] name = "kaspa-addressmanager" -version = "0.14.0" +version = "0.14.1" dependencies = [ "borsh", "igd-next", @@ -2188,19 +2463,19 @@ dependencies = [ [[package]] name = "kaspa-alloc" -version = "0.14.0" +version = "0.14.1" dependencies = [ "mimalloc", ] [[package]] name = "kaspa-bip32" -version = "0.14.0" +version = "0.14.1" dependencies = [ "borsh", "bs58", "faster-hex 0.6.1", - "getrandom 0.2.11", + "getrandom 0.2.14", "hmac", "js-sys", "kaspa-utils", @@ -2221,7 +2496,7 @@ dependencies = [ [[package]] name = "kaspa-cli" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "borsh", @@ -2241,6 +2516,7 @@ dependencies = [ "kaspa-rpc-core", "kaspa-utils", "kaspa-wallet-core", + "kaspa-wallet-keys", "kaspa-wrpc-client", "nw-sys", "pad", @@ -2248,7 +2524,7 @@ dependencies = [ "separator", "serde", "serde_json", - "textwrap 0.16.0", + "textwrap 0.16.1", "thiserror", "tokio", "wasm-bindgen", @@ -2265,7 +2541,7 @@ dependencies = [ [[package]] name = "kaspa-connectionmanager" -version = "0.14.0" +version = "0.14.1" dependencies = [ "duration-string", "futures-util", @@ -2282,17 +2558,17 @@ dependencies = [ [[package]] name = "kaspa-consensus" -version = "0.14.0" +version = "0.14.1" dependencies = [ "arc-swap", - "async-channel 2.1.1", + "async-channel 2.2.1", "bincode", "criterion", "crossbeam-channel", "faster-hex 0.6.1", "flate2", "futures-util", - "indexmap 2.1.0", + "indexmap 2.2.6", "itertools 0.11.0", "kaspa-consensus-core", "kaspa-consensus-notify", @@ -2323,9 +2599,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "kaspa-consensus-client" +version = "0.14.1" +dependencies = [ + "ahash 0.8.11", + "cfg-if 1.0.0", + "faster-hex 0.6.1", + "hex", + "itertools 0.11.0", + "js-sys", + "kaspa-addresses", + "kaspa-consensus-core", + "kaspa-hashes", + "kaspa-math", + "kaspa-txscript", + "kaspa-utils", + "kaspa-wasm-core", + "rand 0.8.5", + "secp256k1", + "serde", + "serde-wasm-bindgen", + "serde_json", + "thiserror", + "wasm-bindgen", + "workflow-log", + "workflow-wasm", +] + [[package]] name = "kaspa-consensus-core" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "bincode", @@ -2334,7 +2638,7 @@ dependencies = [ "criterion", "faster-hex 0.6.1", "futures-util", - "getrandom 0.2.11", + "getrandom 0.2.14", "itertools 0.11.0", "js-sys", "kaspa-addresses", @@ -2362,9 +2666,9 @@ dependencies = [ [[package]] name = "kaspa-consensus-notify" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "cfg-if 1.0.0", "derive_more", "futures", @@ -2381,11 +2685,13 @@ dependencies = [ [[package]] name = "kaspa-consensus-wasm" -version = "0.14.0" +version = "0.14.1" dependencies = [ + "cfg-if 1.0.0", "faster-hex 0.6.1", "js-sys", "kaspa-addresses", + "kaspa-consensus-client", "kaspa-consensus-core", "kaspa-hashes", "kaspa-txscript", @@ -2403,7 +2709,7 @@ dependencies = [ [[package]] name = "kaspa-consensusmanager" -version = "0.14.0" +version = "0.14.1" dependencies = [ "duration-string", "futures", @@ -2421,7 +2727,7 @@ dependencies = [ [[package]] name = "kaspa-core" -version = "0.14.0" +version = "0.14.1" dependencies = [ "cfg-if 1.0.0", "ctrlc", @@ -2439,7 +2745,7 @@ dependencies = [ [[package]] name = "kaspa-daemon" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "borsh", @@ -2461,12 +2767,12 @@ dependencies = [ [[package]] name = "kaspa-database" -version = "0.14.0" +version = "0.14.1" dependencies = [ "bincode", "enum-primitive-derive", "faster-hex 0.6.1", - "indexmap 2.1.0", + "indexmap 2.2.6", "itertools 0.11.0", "kaspa-hashes", "kaspa-utils", @@ -2483,9 +2789,9 @@ dependencies = [ [[package]] name = "kaspa-grpc-client" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-stream", "async-trait", "faster-hex 0.6.1", @@ -2514,9 +2820,9 @@ dependencies = [ [[package]] name = "kaspa-grpc-core" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-stream", "async-trait", "faster-hex 0.6.1", @@ -2543,9 +2849,9 @@ dependencies = [ [[package]] name = "kaspa-grpc-server" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-stream", "async-trait", "faster-hex 0.6.1", @@ -2578,7 +2884,7 @@ dependencies = [ [[package]] name = "kaspa-hashes" -version = "0.14.0" +version = "0.14.1" dependencies = [ "blake2b_simd", "borsh", @@ -2599,9 +2905,9 @@ dependencies = [ [[package]] name = "kaspa-index-core" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-trait", "derive_more", "futures", @@ -2618,9 +2924,9 @@ dependencies = [ [[package]] name = "kaspa-index-processor" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-trait", "derive_more", "futures", @@ -2646,7 +2952,7 @@ dependencies = [ [[package]] name = "kaspa-math" -version = "0.14.0" +version = "0.14.1" dependencies = [ "borsh", "criterion", @@ -2667,14 +2973,14 @@ dependencies = [ [[package]] name = "kaspa-merkle" -version = "0.14.0" +version = "0.14.1" dependencies = [ "kaspa-hashes", ] [[package]] name = "kaspa-metrics-core" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "borsh", @@ -2690,7 +2996,7 @@ dependencies = [ [[package]] name = "kaspa-mining" -version = "0.14.0" +version = "0.14.1" dependencies = [ "criterion", "futures-util", @@ -2716,7 +3022,7 @@ dependencies = [ [[package]] name = "kaspa-mining-errors" -version = "0.14.0" +version = "0.14.1" dependencies = [ "kaspa-consensus-core", "thiserror", @@ -2724,7 +3030,7 @@ dependencies = [ [[package]] name = "kaspa-muhash" -version = "0.14.0" +version = "0.14.1" dependencies = [ "criterion", "kaspa-hashes", @@ -2737,16 +3043,16 @@ dependencies = [ [[package]] name = "kaspa-notify" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-trait", "borsh", "criterion", "derive_more", "futures", "futures-util", - "indexmap 2.1.0", + "indexmap 2.2.6", "itertools 0.11.0", "kaspa-addresses", "kaspa-alloc", @@ -2770,48 +3076,14 @@ dependencies = [ "workflow-perf-monitor", ] -[[package]] -name = "kaspa-os" -version = "0.14.0" -dependencies = [ - "async-trait", - "borsh", - "cfg-if 1.0.0", - "downcast", - "futures", - "js-sys", - "kaspa-cli", - "kaspa-consensus-core", - "kaspa-core", - "kaspa-daemon", - "kaspa-metrics-core", - "kaspa-rpc-core", - "kaspa-wallet-core", - "nw-sys", - "regex", - "serde", - "serde_json", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "workflow-core", - "workflow-d3", - "workflow-dom", - "workflow-log", - "workflow-nw", - "workflow-terminal", - "workflow-wasm", -] - [[package]] name = "kaspa-p2p-flows" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "chrono", "futures", - "indexmap 2.1.0", + "indexmap 2.2.6", "itertools 0.11.0", "kaspa-addressmanager", "kaspa-connectionmanager", @@ -2837,7 +3109,7 @@ dependencies = [ [[package]] name = "kaspa-p2p-lib" -version = "0.14.0" +version = "0.14.1" dependencies = [ "borsh", "ctrlc", @@ -2868,7 +3140,7 @@ dependencies = [ [[package]] name = "kaspa-perf-monitor" -version = "0.14.0" +version = "0.14.1" dependencies = [ "kaspa-core", "log", @@ -2880,29 +3152,65 @@ dependencies = [ [[package]] name = "kaspa-pow" -version = "0.14.0" +version = "0.14.1" dependencies = [ "criterion", "js-sys", + "kaspa-consensus-client", "kaspa-consensus-core", "kaspa-hashes", "kaspa-math", "kaspa-utils", + "num", "wasm-bindgen", "workflow-wasm", ] +[[package]] +name = "kaspa-resolver" +version = "0.14.1" +dependencies = [ + "ahash 0.8.11", + "axum 0.7.5", + "cfg-if 1.0.0", + "clap 4.5.4", + "console", + "convert_case 0.6.0", + "futures", + "kaspa-consensus-core", + "kaspa-rpc-core", + "kaspa-utils", + "kaspa-wrpc-client", + "mime", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml 0.8.12", + "tower", + "tower-http 0.5.2", + "tracing-subscriber", + "workflow-core", + "workflow-http", + "workflow-log", + "xxhash-rust", +] + [[package]] name = "kaspa-rpc-core" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-trait", "borsh", + "cfg-if 1.0.0", "derive_more", "downcast", "faster-hex 0.6.1", + "hex", + "js-sys", "kaspa-addresses", + "kaspa-consensus-client", "kaspa-consensus-core", "kaspa-consensus-notify", "kaspa-consensus-wasm", @@ -2912,6 +3220,7 @@ dependencies = [ "kaspa-math", "kaspa-mining-errors", "kaspa-notify", + "kaspa-rpc-macros", "kaspa-txscript", "kaspa-utils", "log", @@ -2929,7 +3238,7 @@ dependencies = [ [[package]] name = "kaspa-rpc-macros" -version = "0.14.0" +version = "0.14.1" dependencies = [ "convert_case 0.6.0", "proc-macro-error", @@ -2941,7 +3250,7 @@ dependencies = [ [[package]] name = "kaspa-rpc-service" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "kaspa-addresses", @@ -2970,20 +3279,20 @@ dependencies = [ [[package]] name = "kaspa-testing-integration" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-trait", "bincode", "chrono", - "clap 4.4.11", + "clap 4.5.4", "criterion", "crossbeam-channel", "dhat", "faster-hex 0.6.1", "flate2", "futures-util", - "indexmap 2.1.0", + "indexmap 2.2.6", "itertools 0.11.0", "kaspa-addresses", "kaspa-alloc", @@ -3030,13 +3339,13 @@ dependencies = [ [[package]] name = "kaspa-txscript" -version = "0.14.0" +version = "0.14.1" dependencies = [ "blake2b_simd", "borsh", "criterion", "hex", - "indexmap 2.1.0", + "indexmap 2.2.6", "itertools 0.11.0", "kaspa-addresses", "kaspa-consensus-core", @@ -3056,7 +3365,7 @@ dependencies = [ [[package]] name = "kaspa-txscript-errors" -version = "0.14.0" +version = "0.14.1" dependencies = [ "secp256k1", "thiserror", @@ -3064,9 +3373,9 @@ dependencies = [ [[package]] name = "kaspa-utils" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-trait", "bincode", "borsh", @@ -3088,25 +3397,26 @@ dependencies = [ "tokio", "triggered", "uuid 1.6.1", + "wasm-bindgen", ] [[package]] name = "kaspa-utils-tower" -version = "0.14.0" +version = "0.14.1" dependencies = [ "cfg-if 1.0.0", "futures", - "hyper", + "hyper 0.14.28", "log", "pin-project-lite", "tokio", "tower", - "tower-http", + "tower-http 0.4.4", ] [[package]] name = "kaspa-utxoindex" -version = "0.14.0" +version = "0.14.1" dependencies = [ "futures", "kaspa-consensus", @@ -3127,7 +3437,7 @@ dependencies = [ [[package]] name = "kaspa-wallet" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-std", "async-trait", @@ -3139,7 +3449,7 @@ dependencies = [ [[package]] name = "kaspa-wallet-cli-wasm" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "js-sys", @@ -3153,19 +3463,21 @@ dependencies = [ [[package]] name = "kaspa-wallet-core" -version = "0.14.0" +version = "0.14.1" dependencies = [ "aes", - "ahash 0.8.6", + "ahash 0.8.11", "argon2", - "async-channel 2.1.1", + "async-channel 2.2.1", "async-std", "async-trait", - "base64 0.21.5", + "base64 0.21.7", "borsh", "cfb-mode", "cfg-if 1.0.0", "chacha20poly1305", + "convert_case 0.6.0", + "crypto_box", "dashmap", "derivative", "downcast", @@ -3182,17 +3494,22 @@ dependencies = [ "js-sys", "kaspa-addresses", "kaspa-bip32", + "kaspa-consensus-client", "kaspa-consensus-core", "kaspa-consensus-wasm", "kaspa-core", "kaspa-hashes", + "kaspa-metrics-core", "kaspa-notify", "kaspa-rpc-core", "kaspa-txscript", "kaspa-txscript-errors", "kaspa-utils", + "kaspa-wallet-keys", "kaspa-wallet-macros", + "kaspa-wasm-core", "kaspa-wrpc-client", + "kaspa-wrpc-wasm", "md-5", "pad", "pbkdf2", @@ -3224,9 +3541,42 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kaspa-wallet-keys" +version = "0.14.1" +dependencies = [ + "async-trait", + "borsh", + "downcast", + "faster-hex 0.6.1", + "hmac", + "js-sys", + "kaspa-addresses", + "kaspa-bip32", + "kaspa-consensus-core", + "kaspa-txscript", + "kaspa-txscript-errors", + "kaspa-utils", + "kaspa-wasm-core", + "rand 0.8.5", + "ripemd", + "secp256k1", + "serde", + "serde-wasm-bindgen", + "serde_json", + "sha2", + "thiserror", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "workflow-core", + "workflow-wasm", + "zeroize", +] + [[package]] name = "kaspa-wallet-macros" -version = "0.14.0" +version = "0.14.1" dependencies = [ "convert_case 0.5.0", "proc-macro-error", @@ -3239,8 +3589,9 @@ dependencies = [ [[package]] name = "kaspa-wasm" -version = "0.14.0" +version = "0.14.1" dependencies = [ + "cfg-if 1.0.0", "js-sys", "kaspa-addresses", "kaspa-consensus-core", @@ -3251,19 +3602,34 @@ dependencies = [ "kaspa-rpc-core", "kaspa-utils", "kaspa-wallet-core", + "kaspa-wallet-keys", + "kaspa-wasm-core", "kaspa-wrpc-client", + "kaspa-wrpc-wasm", "num", "wasm-bindgen", + "workflow-core", "workflow-log", + "workflow-wasm", +] + +[[package]] +name = "kaspa-wasm-core" +version = "0.14.1" +dependencies = [ + "faster-hex 0.6.1", + "js-sys", + "wasm-bindgen", ] [[package]] name = "kaspa-wrpc-client" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-std", "async-trait", "borsh", + "cfg-if 1.0.0", "futures", "js-sys", "kaspa-addresses", @@ -3273,26 +3639,44 @@ dependencies = [ "kaspa-rpc-core", "kaspa-rpc-macros", "paste", + "rand 0.8.5", "regex", "serde", "serde-wasm-bindgen", "serde_json", "thiserror", + "toml 0.8.12", "wasm-bindgen", "wasm-bindgen-futures", "workflow-core", "workflow-dom", + "workflow-http", "workflow-log", "workflow-rpc", "workflow-wasm", ] +[[package]] +name = "kaspa-wrpc-example-subscriber" +version = "0.14.1" +dependencies = [ + "ctrlc", + "futures", + "kaspa-consensus-core", + "kaspa-notify", + "kaspa-rpc-core", + "kaspa-wrpc-client", + "tokio", + "workflow-core", + "workflow-log", +] + [[package]] name = "kaspa-wrpc-proxy" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", - "clap 4.4.11", + "clap 4.5.4", "kaspa-consensus-core", "kaspa-grpc-client", "kaspa-rpc-core", @@ -3308,7 +3692,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-server" -version = "0.14.0" +version = "0.14.1" dependencies = [ "async-trait", "borsh", @@ -3335,17 +3719,39 @@ dependencies = [ [[package]] name = "kaspa-wrpc-wasm" -version = "0.14.0" +version = "0.14.1" dependencies = [ + "ahash 0.8.11", + "async-std", + "cfg-if 1.0.0", + "futures", + "js-sys", + "kaspa-addresses", + "kaspa-consensus-client", + "kaspa-consensus-core", + "kaspa-consensus-wasm", + "kaspa-notify", + "kaspa-rpc-core", + "kaspa-rpc-macros", + "kaspa-wasm-core", "kaspa-wrpc-client", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "workflow-core", + "workflow-log", + "workflow-rpc", + "workflow-wasm", ] [[package]] name = "kaspad" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", - "clap 4.4.11", + "async-channel 2.2.1", + "clap 4.5.4", "dhat", "dirs", "futures-util", @@ -3381,15 +3787,15 @@ dependencies = [ "tempfile", "thiserror", "tokio", - "toml 0.8.11", + "toml 0.8.12", "workflow-log", ] [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] @@ -3417,18 +3823,18 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if 1.0.0", - "winapi", + "windows-targets 0.52.5", ] [[package]] @@ -3439,9 +3845,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libmimalloc-sys" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664" +checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7" dependencies = [ "cc", "libc", @@ -3449,13 +3855,12 @@ dependencies = [ [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "libc", - "redox_syscall", ] [[package]] @@ -3476,21 +3881,15 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.12" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linkme" version = "0.2.10" @@ -3519,15 +3918,15 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "local-ip-address" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66357e687a569abca487dc399a9c9ac19beb3f13991ed49f00c144e02cbd42ab" +checksum = "612ed4ea9ce5acfb5d26339302528a5e1e59dfed95e9e11af3c083236ff1d15d" dependencies = [ "libc", "neli", @@ -3537,9 +3936,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -3547,9 +3946,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "serde", "value-bag", @@ -3563,9 +3962,9 @@ checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" [[package]] name = "log4rs" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd" +checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" dependencies = [ "anyhow", "arc-swap", @@ -3577,7 +3976,9 @@ dependencies = [ "libc", "log", "log-mdc", + "once_cell", "parking_lot", + "rand 0.8.5", "serde", "serde-value", "serde_json", @@ -3627,7 +4028,7 @@ dependencies = [ "cfg-if 1.0.0", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -3638,7 +4039,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -3651,27 +4052,29 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "malachite-base" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6538136c5daf04126d6be4899f7fe4879b7f8de896dd1b4210fe6de5b94f2555" +checksum = "d073a3d1e4e037975af5ef176a2632672e25e8ddbe8e1811745c2e0726b6ad94" dependencies = [ + "hashbrown 0.14.5", "itertools 0.11.0", + "libm", "ryu", ] [[package]] name = "malachite-nz" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0b05577b7a3f09433106460b10304f97fc572f0baabf6640e6cb1e23f5fc52" +checksum = "2546fc6ae29728079e87a2a0f011509e6060713b65e62ee46ba5d413b495ebc7" dependencies = [ - "embed-doc-image", "itertools 0.11.0", + "libm", "malachite-base", ] @@ -3711,24 +4114,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mimalloc" -version = "0.1.39" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c" +checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d" dependencies = [ "libmimalloc-sys", ] @@ -3747,28 +4141,24 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mintex" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7c5ba1c3b5a23418d7bbf98c71c3d4946a0125002129231da8d6b723d559cb" -dependencies = [ - "once_cell", - "sys-info", -] +checksum = "9bec4598fddb13cc7b528819e697852653252b760f1228b7642679bf2ff2cd07" [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", @@ -3778,9 +4168,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "nalgebra" @@ -3867,12 +4257,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if 1.0.0", + "cfg_aliases", "libc", ] @@ -3899,14 +4290,24 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" dependencies = [ "num-bigint", - "num-complex 0.4.4", + "num-complex 0.4.5", "num-integer", "num-iter", "num-rational 0.4.1", @@ -3936,28 +4337,33 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -3989,9 +4395,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -4003,15 +4409,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -4037,9 +4443,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -4058,17 +4464,17 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.61" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -4085,7 +4491,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -4096,18 +4502,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.1+3.2.0" +version = "300.2.3+3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -4131,6 +4537,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pad" version = "0.1.6" @@ -4148,9 +4560,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -4158,15 +4570,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -4186,7 +4598,7 @@ checksum = "70df726c43c645ef1dde24c7ae14692036ebe5457c92c5f0ec4cfceb99634ff6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -4235,34 +4647,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.1.0", + "indexmap 2.2.6", ] [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -4277,15 +4689,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-io", ] [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "platforms" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "polling" @@ -4305,14 +4723,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.3.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" dependencies = [ "cfg-if 1.0.0", "concurrent-queue", + "hermit-abi 0.3.9", "pin-project-lite", - "rustix 0.38.28", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] @@ -4348,12 +4767,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ "proc-macro2", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -4390,18 +4809,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -4409,13 +4828,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +checksum = "80b776a1b2dc779f5ee0641f8ade0125bc1298dd41a9a0c16d8bd57b42d222b1" dependencies = [ "bytes", "heck", - "itertools 0.11.0", + "itertools 0.12.1", "log", "multimap", "once_cell", @@ -4424,38 +4843,37 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.49", + "syn 2.0.60", "tempfile", - "which", ] [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" dependencies = [ "prost", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -4519,7 +4937,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", ] [[package]] @@ -4558,9 +4976,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -4568,9 +4986,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -4585,22 +5003,31 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -4610,9 +5037,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -4621,22 +5048,63 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", - "getrandom 0.2.11", + "cfg-if 1.0.0", + "getrandom 0.2.14", "libc", "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4669,10 +5137,10 @@ dependencies = [ [[package]] name = "rothschild" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", - "clap 4.4.11", + "async-channel 2.2.1", + "clap 4.5.4", "criterion", "faster-hex 0.6.1", "itertools 0.11.0", @@ -4728,22 +5196,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.12", + "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", @@ -4757,7 +5225,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] [[package]] @@ -4772,15 +5240,24 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "salsa20" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] [[package]] name = "same-file" @@ -4793,11 +5270,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4824,9 +5301,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.24.3" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "rand 0.8.5", "secp256k1-sys", @@ -4835,20 +5312,20 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.6.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" dependencies = [ "cc", ] [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -4857,9 +5334,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -4867,9 +5344,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] [[package]] name = "separator" @@ -4888,9 +5368,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.193" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] @@ -4907,9 +5387,9 @@ dependencies = [ [[package]] name = "serde-wasm-bindgen" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ "js-sys", "serde", @@ -4918,35 +5398,45 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -4958,17 +5448,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.1.0", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -4978,26 +5480,27 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] name = "serde_yaml" -version = "0.8.26" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.6", + "itoa", "ryu", "serde", - "yaml-rust", + "unsafe-libyaml", ] [[package]] @@ -5032,11 +5535,20 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" @@ -5061,23 +5573,23 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "simpa" -version = "0.14.0" +version = "0.14.1" dependencies = [ - "async-channel 2.1.1", - "clap 4.4.11", + "async-channel 2.2.1", + "clap 4.5.4", "dhat", "futures", "futures-util", - "indexmap 2.1.0", + "indexmap 2.2.6", "itertools 0.11.0", "kaspa-alloc", "kaspa-consensus", @@ -5118,9 +5630,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] @@ -5143,12 +5655,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5205,6 +5717,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -5224,9 +5742,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.49" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -5240,33 +5758,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "sys-info" -version = "0.9.1" +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "cc", + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", "libc", ] [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if 1.0.0", - "fastrand 2.0.1", - "redox_syscall", - "rustix 0.38.28", - "windows-sys 0.48.0", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", ] [[package]] name = "termcolor" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] @@ -5282,9 +5816,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", @@ -5293,22 +5827,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -5327,15 +5861,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + [[package]] name = "time" -version = "0.3.30" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -5351,10 +5896,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -5385,9 +5931,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -5396,7 +5942,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.48.0", ] @@ -5419,7 +5965,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -5444,9 +5990,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -5455,9 +6001,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", @@ -5469,16 +6015,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5492,9 +6037,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", @@ -5513,11 +6058,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.7" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -5532,14 +6077,14 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", - "axum", - "base64 0.21.5", + "axum 0.6.20", + "base64 0.21.7", "bytes", "flate2", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", @@ -5565,7 +6110,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -5594,18 +6139,34 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "http-range-header", "pin-project-lite", "tower-layer", "tower-service", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.5.0", + "bytes", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -5638,7 +6199,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -5648,6 +6209,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -5664,14 +6251,14 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "native-tls", @@ -5699,9 +6286,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -5717,24 +6304,24 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "universal-hash" @@ -5755,6 +6342,12 @@ dependencies = [ "destructure_traitobject", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -5790,7 +6383,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", ] [[package]] @@ -5799,17 +6392,23 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", "rand 0.8.5", "serde", "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "value-bag" -version = "1.4.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" [[package]] name = "vcpkg" @@ -5825,11 +6424,14 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "vergen" -version = "8.2.6" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1290fd64cc4e7d3c9b07d7f333ce0ce0007253e32870e632624835cc80b83939" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" dependencies = [ "anyhow", + "cargo_metadata", + "cfg-if 1.0.0", + "regex", "rustc_version", "rustversion", "time", @@ -5849,9 +6451,9 @@ checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -5880,9 +6482,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if 1.0.0", "serde", @@ -5892,24 +6494,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -5919,9 +6521,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5929,28 +6531,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-bindgen-test" -version = "0.3.37" +version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" +checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" dependencies = [ "console_error_panic_hook", "js-sys", @@ -5962,19 +6564,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.37" +version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" +checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", + "syn 2.0.60", ] [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -5989,7 +6592,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.28", + "rustix 0.38.34", ] [[package]] @@ -6010,11 +6613,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -6025,20 +6628,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -6056,22 +6650,7 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -6091,25 +6670,20 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 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", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6118,15 +6692,9 @@ 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" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -6136,15 +6704,9 @@ 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" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -6154,15 +6716,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -6172,15 +6734,9 @@ 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" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -6190,15 +6746,9 @@ 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" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -6208,15 +6758,9 @@ 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" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -6226,35 +6770,34 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" dependencies = [ "memchr", ] [[package]] -name = "workflow-async-trait" -version = "0.1.68" +name = "winreg" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbe1707662eb8888c69bbd8e38a9648168360cf71f26631a947099cc6fa4a2" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "cfg-if 1.0.0", + "windows-sys 0.48.0", ] [[package]] name = "workflow-chrome" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ff271d74ba14118dcbe12514f802c81337fc3dbe3c95857a2d8e79aaf35e0b" +checksum = "109b6289f65b3e1cdfa6f2d9e8eb454453d5763c5061350e2300473c48d91b99" dependencies = [ "cfg-if 1.0.0", "chrome-sys", @@ -6267,20 +6810,20 @@ dependencies = [ [[package]] name = "workflow-core" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0411aa61118b144354b84731f35236c076a18f92963d8cccf263f8bebb48375" +checksum = "bcea01cb6122ac3f20dc14f8e4104e2c0cd9c718c17ddb3fc115f9b2ed99f9ae" dependencies = [ - "async-channel 2.1.1", + "async-channel 2.2.1", "async-std", "borsh", "bs58", "cfg-if 1.0.0", "chrono", "dirs", - "faster-hex 0.8.1", + "faster-hex 0.9.0", "futures", - "getrandom 0.2.11", + "getrandom 0.2.14", "instant", "js-sys", "rand 0.8.5", @@ -6293,15 +6836,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "workflow-async-trait", "workflow-core-macros", ] [[package]] name = "workflow-core-macros" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fd121aca38325946aa9be04c9beb78757ed0f271ce59d168521db058475cda" +checksum = "fe24820a62e2b544c75c000cff72781383495a0e05157ec3e29b2abafe1ca2cb" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -6315,45 +6857,44 @@ dependencies = [ ] [[package]] -name = "workflow-d3" -version = "0.10.3" +name = "workflow-dom" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6295dd583587112e3753f6a901c20a7c5410328fe16ad170d5f4a489edcddcd" +checksum = "91264d4e789f23c6730c2f3adede04a24b6a9eb9797f9d4ab23de370ba04c27f" dependencies = [ - "atomic_float", + "futures", "js-sys", + "regex", "thiserror", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", "workflow-core", - "workflow-dom", "workflow-log", "workflow-wasm", ] [[package]] -name = "workflow-dom" -version = "0.10.3" +name = "workflow-http" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8be65dc57d835f4e5bc3b169e89177437b35f34379e6fc51357a78b92dc82e9" +checksum = "b5b191def1625c3aa5e7d62d1ebbbb3e639113a4a2f122418e4cf8d3379374f8" dependencies = [ - "futures", - "js-sys", - "regex", + "cfg-if 1.0.0", + "reqwest", + "serde", + "serde_json", "thiserror", + "tokio", "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", "workflow-core", - "workflow-log", - "workflow-wasm", ] [[package]] name = "workflow-log" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ab8bce457fbf5f1be3b189d4eb1ed729a58c0f38caeadc6b6474e5de73aa1d" +checksum = "077a8f720aa45c8cd867de1ccc73e068c4084d9fea46d11be7697a108e6a00ba" dependencies = [ "cfg-if 1.0.0", "console", @@ -6367,9 +6908,9 @@ dependencies = [ [[package]] name = "workflow-macro-tools" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f266c9407588c6b90f62f242530db3949be00e716587ba1c361a905c0c1dc997" +checksum = "f5a8af8b8951fa0cf94df4057b8cf583e067a525d3d997370db7797f33ba201f" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -6380,9 +6921,9 @@ dependencies = [ [[package]] name = "workflow-node" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357c25fbb5e6b0efda1baaafae45125a76af4d9d500758cd742cda1aeba5fe45" +checksum = "7748eb6c76779993ed7f4457356d6b57f48f97f9e264c64c3405098330bcb8c7" dependencies = [ "borsh", "futures", @@ -6401,11 +6942,11 @@ dependencies = [ [[package]] name = "workflow-nw" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f01df906849fa3c53b876d7fd58a64f4842dd3cda5efdc86adda1419c407475a" +checksum = "010fff3468303b39fb0d5d267847a3d293ed083afbf83f4184fb1a749be56010" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "async-trait", "borsh", "futures", @@ -6425,9 +6966,9 @@ dependencies = [ [[package]] name = "workflow-panic-hook" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e45ddff3f2c311d8d86d637a9ddd23e16528a3d89956242bb91b3ca4471155" +checksum = "71c1ed51290daf255e5fd83dfe6bd754b108e371b971afbb5c5fd1ea8fe148af" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen", @@ -6450,18 +6991,18 @@ dependencies = [ [[package]] name = "workflow-rpc" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24bf4677d64e142bf9117bb652e44c32d4fb093360120c10a2c7a65ee1d1ce1e" +checksum = "14784fbad27d0403fc752d835c4c4683cfc6af970a484ea83f40ce7ad6dc7745" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "async-std", "async-trait", "borsh", "downcast-rs", "futures", "futures-util", - "getrandom 0.2.11", + "getrandom 0.2.14", "manual_future", "rand 0.8.5", "serde", @@ -6474,14 +7015,15 @@ dependencies = [ "workflow-log", "workflow-rpc-macros", "workflow-task", + "workflow-wasm", "workflow-websocket", ] [[package]] name = "workflow-rpc-macros" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20d1d1636a2084ec61074854c5e6ac2c46b01d3fc1a382d21d3585e74402918" +checksum = "c372e99d1336a137b907274a3c50fc195e30141c87fc6da4dba54e7d4b09b8ec" dependencies = [ "parse-variants", "proc-macro-error", @@ -6492,15 +7034,15 @@ dependencies = [ [[package]] name = "workflow-store" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f227f89661e283e813514f24dda79fd6a591a46eb0b8782f83448ca0316a4d1" +checksum = "762861614298160b9205302bec4f2b7eb45853413d10a90ad8edca44bafc324b" dependencies = [ "async-std", - "base64 0.21.5", + "base64 0.21.7", "cfg-if 1.0.0", "chrome-sys", - "faster-hex 0.8.1", + "faster-hex 0.9.0", "filetime", "home", "js-sys", @@ -6520,9 +7062,9 @@ dependencies = [ [[package]] name = "workflow-task" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e884d575ac521034e307eac3310960e8860ca59eed2e1d19fbf1d15abe4908b9" +checksum = "a4023e2598734e04aa4e968a4dd1cd2b5d0c344edc38b40970926d5742f5afa0" dependencies = [ "futures", "thiserror", @@ -6532,9 +7074,9 @@ dependencies = [ [[package]] name = "workflow-task-macros" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0282a25fcc6e2ff74e03227a8baf6fd2bd3f65a412ff2a8c01c28021443007" +checksum = "057801365ce04c520a2a694bc5bfdf1784f1a33fff97af4cd735f94eb12947b1" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -6548,9 +7090,9 @@ dependencies = [ [[package]] name = "workflow-terminal" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66f08883fffb4063456ead33ce2aa2b82e7529d70dd8bf63beee25f65d1f5cc1" +checksum = "895c236dd5cf493e01fc31733c4687b3e67032f610d594ce3b8e5cafd14eaf33" dependencies = [ "async-std", "async-trait", @@ -6563,7 +7105,7 @@ dependencies = [ "nw-sys", "pad", "regex", - "textwrap 0.16.0", + "textwrap 0.16.1", "thiserror", "wasm-bindgen", "wasm-bindgen-futures", @@ -6577,9 +7119,9 @@ dependencies = [ [[package]] name = "workflow-terminal-macros" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4cdf04791ee48cc30896f6ed5a125902e90a6774f2bc3fa95b171988b63a64" +checksum = "eb1fe67beb12d31f2e69715898aa32abd2349ffc8fe0555617f0d77500cebc56" dependencies = [ "convert_case 0.6.0", "parse-variants", @@ -6593,12 +7135,12 @@ dependencies = [ [[package]] name = "workflow-wasm" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256633203bccf1e20bac3bf4b61cbe77b17e696e8690944dc7ea46bb2c0f5e8f" +checksum = "93ffbd1de665304ba6040a1ab4e0867fd9174446491d257bc6a1474ae25d4a6c" dependencies = [ "cfg-if 1.0.0", - "faster-hex 0.8.1", + "faster-hex 0.9.0", "futures", "js-sys", "serde", @@ -6614,9 +7156,9 @@ dependencies = [ [[package]] name = "workflow-wasm-macros" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b56f2853a9b5757dee92b269f0ccf071728f9bdd9c06debc99b5e705afebd564" +checksum = "082644f52215ecc86b4b8a20a763e482adee52c338208ade268f47fe25eb07ca" dependencies = [ "js-sys", "proc-macro-error", @@ -6628,12 +7170,12 @@ dependencies = [ [[package]] name = "workflow-websocket" -version = "0.10.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3c60e8bcd787dff9e5e33584bcbf3d8b82d203cfcd821f1667804a5f507139" +checksum = "6967baf2bd85deb2a014a32d34c1664ded9333e10d11d43ffc179fa09cc55db8" dependencies = [ - "ahash 0.8.6", - "async-channel 2.1.1", + "ahash 0.8.11", + "async-channel 2.2.1", "async-std", "async-trait", "cfg-if 1.0.0", @@ -6657,9 +7199,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" [[package]] name = "xmltree" @@ -6672,37 +7214,28 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" - -[[package]] -name = "yaml-rust" -version = "0.4.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] +checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.60", ] [[package]] @@ -6713,9 +7246,9 @@ checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 25872b3ac..283981df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,15 +9,17 @@ members = [ "wallet/native", "wallet/wasm", "wallet/bip32", + "wallet/keys", "consensus", "consensus/core", + "consensus/client", "consensus/notify", "consensus/pow", "consensus/wasm", - "kos", "kaspad", "simpa", "wasm", + "wasm/core", "math", "crypto/hashes", "crypto/muhash", @@ -33,10 +35,12 @@ members = [ "rpc/grpc/core", "rpc/grpc/client", "rpc/grpc/server", + "rpc/wrpc/resolver", "rpc/wrpc/server", "rpc/wrpc/client", "rpc/wrpc/proxy", "rpc/wrpc/wasm", + "rpc/wrpc/examples/subscriber", "mining", "mining/errors", "protocol/p2p", @@ -57,10 +61,10 @@ members = [ ] [workspace.package] -rust-version = "1.77.0" -version = "0.14.0" +rust-version = "1.78.0" +version = "0.14.1" authors = ["Kaspa developers"] -license = "MIT/Apache-2.0" +license = "ISC" repository = "https://github.com/kaspanet/rusty-kaspa" edition = "2021" include = [ @@ -75,59 +79,62 @@ include = [ ] [workspace.dependencies] -# kaspa-testing-integration = { version = "0.14.0", path = "testing/integration" } -kaspa-addresses = { version = "0.14.0", path = "crypto/addresses" } -kaspa-addressmanager = { version = "0.14.0", path = "components/addressmanager" } -kaspa-bip32 = { version = "0.14.0", path = "wallet/bip32" } -kaspa-cli = { version = "0.14.0", path = "cli" } -kaspa-connectionmanager = { version = "0.14.0", path = "components/connectionmanager" } -kaspa-consensus = { version = "0.14.0", path = "consensus" } -kaspa-consensus-core = { version = "0.14.0", path = "consensus/core" } -kaspa-consensus-notify = { version = "0.14.0", path = "consensus/notify" } -kaspa-consensus-wasm = { version = "0.14.0", path = "consensus/wasm" } -kaspa-consensusmanager = { version = "0.14.0", path = "components/consensusmanager" } -kaspa-core = { version = "0.14.0", path = "core" } -kaspa-daemon = { version = "0.14.0", path = "daemon" } -kaspa-database = { version = "0.14.0", path = "database" } -kaspa-grpc-client = { version = "0.14.0", path = "rpc/grpc/client" } -kaspa-grpc-core = { version = "0.14.0", path = "rpc/grpc/core" } -kaspa-grpc-server = { version = "0.14.0", path = "rpc/grpc/server" } -kaspa-hashes = { version = "0.14.0", path = "crypto/hashes" } -kaspa-index-core = { version = "0.14.0", path = "indexes/core" } -kaspa-index-processor = { version = "0.14.0", path = "indexes/processor" } -kaspa-math = { version = "0.14.0", path = "math" } -kaspa-merkle = { version = "0.14.0", path = "crypto/merkle" } -kaspa-metrics-core = { version = "0.14.0", path = "metrics/core" } -kaspa-mining = { version = "0.14.0", path = "mining" } -kaspa-mining-errors = { version = "0.14.0", path = "mining/errors" } -kaspa-muhash = { version = "0.14.0", path = "crypto/muhash" } -kaspa-notify = { version = "0.14.0", path = "notify" } -kaspa-os = { version = "0.14.0", path = "kaspa-os" } -kaspa-p2p-flows = { version = "0.14.0", path = "protocol/flows" } -kaspa-p2p-lib = { version = "0.14.0", path = "protocol/p2p" } -kaspa-perf-monitor = { version = "0.14.0", path = "metrics/perf_monitor" } -kaspa-pow = { version = "0.14.0", path = "consensus/pow" } -kaspa-rpc-core = { version = "0.14.0", path = "rpc/core" } -kaspa-rpc-macros = { version = "0.14.0", path = "rpc/macros" } -kaspa-rpc-service = { version = "0.14.0", path = "rpc/service" } -kaspa-txscript = { version = "0.14.0", path = "crypto/txscript" } -kaspa-txscript-errors = { version = "0.14.0", path = "crypto/txscript/errors" } -kaspa-utils = { version = "0.14.0", path = "utils" } -kaspa-utils-tower = { version = "0.14.0", path = "utils/tower" } -kaspa-utxoindex = { version = "0.14.0", path = "indexes/utxoindex" } -kaspa-wallet = { version = "0.14.0", path = "wallet/native" } -kaspa-wallet-cli-wasm = { version = "0.14.0", path = "wallet/wasm" } -kaspa-wallet-core = { version = "0.14.0", path = "wallet/core" } -kaspa-wallet-macros = { version = "0.14.0", path = "wallet/macros" } -kaspa-wasm = { version = "0.14.0", path = "wasm" } -kaspa-wrpc-client = { version = "0.14.0", path = "rpc/wrpc/client" } -kaspa-wrpc-core = { version = "0.14.0", path = "rpc/wrpc/core" } -kaspa-wrpc-proxy = { version = "0.14.0", path = "rpc/wrpc/proxy" } -kaspa-wrpc-server = { version = "0.14.0", path = "rpc/wrpc/server" } -kaspa-wrpc-wasm = { version = "0.14.0", path = "rpc/wrpc/wasm" } -kaspad = { version = "0.14.0", path = "kaspad" } -kaspa-alloc = { version = "0.14.0", path = "utils/alloc" } - +# kaspa-testing-integration = { version = "0.14.1", path = "testing/integration" } +kaspa-addresses = { version = "0.14.1", path = "crypto/addresses" } +kaspa-addressmanager = { version = "0.14.1", path = "components/addressmanager" } +kaspa-bip32 = { version = "0.14.1", path = "wallet/bip32" } +kaspa-resolver = { version = "0.14.1", path = "rpr/wrpc/resolver" } +kaspa-cli = { version = "0.14.1", path = "cli" } +kaspa-connectionmanager = { version = "0.14.1", path = "components/connectionmanager" } +kaspa-consensus = { version = "0.14.1", path = "consensus" } +kaspa-consensus-core = { version = "0.14.1", path = "consensus/core" } +kaspa-consensus-client = { version = "0.14.1", path = "consensus/client" } +kaspa-consensus-notify = { version = "0.14.1", path = "consensus/notify" } +kaspa-consensus-wasm = { version = "0.14.1", path = "consensus/wasm" } +kaspa-consensusmanager = { version = "0.14.1", path = "components/consensusmanager" } +kaspa-core = { version = "0.14.1", path = "core" } +kaspa-daemon = { version = "0.14.1", path = "daemon" } +kaspa-database = { version = "0.14.1", path = "database" } +kaspa-grpc-client = { version = "0.14.1", path = "rpc/grpc/client" } +kaspa-grpc-core = { version = "0.14.1", path = "rpc/grpc/core" } +kaspa-grpc-server = { version = "0.14.1", path = "rpc/grpc/server" } +kaspa-hashes = { version = "0.14.1", path = "crypto/hashes" } +kaspa-index-core = { version = "0.14.1", path = "indexes/core" } +kaspa-index-processor = { version = "0.14.1", path = "indexes/processor" } +kaspa-math = { version = "0.14.1", path = "math" } +kaspa-merkle = { version = "0.14.1", path = "crypto/merkle" } +kaspa-metrics-core = { version = "0.14.1", path = "metrics/core" } +kaspa-mining = { version = "0.14.1", path = "mining" } +kaspa-mining-errors = { version = "0.14.1", path = "mining/errors" } +kaspa-muhash = { version = "0.14.1", path = "crypto/muhash" } +kaspa-notify = { version = "0.14.1", path = "notify" } +kaspa-p2p-flows = { version = "0.14.1", path = "protocol/flows" } +kaspa-p2p-lib = { version = "0.14.1", path = "protocol/p2p" } +kaspa-perf-monitor = { version = "0.14.1", path = "metrics/perf_monitor" } +kaspa-pow = { version = "0.14.1", path = "consensus/pow" } +kaspa-rpc-core = { version = "0.14.1", path = "rpc/core" } +kaspa-rpc-macros = { version = "0.14.1", path = "rpc/macros" } +kaspa-rpc-service = { version = "0.14.1", path = "rpc/service" } +kaspa-txscript = { version = "0.14.1", path = "crypto/txscript" } +kaspa-txscript-errors = { version = "0.14.1", path = "crypto/txscript/errors" } +kaspa-utils = { version = "0.14.1", path = "utils" } +kaspa-utils-tower = { version = "0.14.1", path = "utils/tower" } +kaspa-utxoindex = { version = "0.14.1", path = "indexes/utxoindex" } +kaspa-wallet = { version = "0.14.1", path = "wallet/native" } +kaspa-wallet-cli-wasm = { version = "0.14.1", path = "wallet/wasm" } +kaspa-wallet-keys = { version = "0.14.1", path = "wallet/keys" } +kaspa-wallet-core = { version = "0.14.1", path = "wallet/core" } +kaspa-wallet-macros = { version = "0.14.1", path = "wallet/macros" } +kaspa-wasm = { version = "0.14.1", path = "wasm" } +kaspa-wasm-core = { version = "0.14.1", path = "wasm/core" } +kaspa-wrpc-client = { version = "0.14.1", path = "rpc/wrpc/client" } +kaspa-wrpc-core = { version = "0.14.1", path = "rpc/wrpc/core" } +kaspa-wrpc-proxy = { version = "0.14.1", path = "rpc/wrpc/proxy" } +kaspa-wrpc-server = { version = "0.14.1", path = "rpc/wrpc/server" } +kaspa-wrpc-wasm = { version = "0.14.1", path = "rpc/wrpc/wasm" } +kaspa-wrpc-example-subscriber = { version = "0.14.1", path = "rpc/wrpc/examples/subscriber" } +kaspad = { version = "0.14.1", path = "kaspad" } +kaspa-alloc = { version = "0.14.1", path = "utils/alloc" } # external aes = "0.8.3" @@ -151,7 +158,8 @@ clap = { version = "4.4.7", features = ["derive", "string", "cargo"] } convert_case = "0.6.0" criterion = { version = "0.5.1", default-features = false } crossbeam-channel = "0.5.8" -ctrlc = "3.4.1" # 3.2 +ctrlc = "3.4.1" +crypto_box = { version = "0.9.1", features = ["chacha20"] } dashmap = "5.5.3" derivative = "2.2.0" derive_more = "0.99.17" @@ -173,7 +181,7 @@ futures-util = { version = "0.3.29", default-features = false, features = [ getrandom = { version = "0.2.10", features = ["js"] } h2 = "0.3.21" heapless = "0.7.16" -hex = "0.4.3" +hex = { version = "0.4.3", features = ["serde"] } hex-literal = "0.4.1" hmac = { version = "0.12.1", default-features = false } home = "0.5.5" @@ -182,7 +190,7 @@ indexmap = "2.1.0" intertrait = "0.2.2" ipnet = "2.9.0" itertools = "0.11.0" -js-sys = "=0.3.64" +js-sys = "0.3.67" keccak = "0.1.4" local-ip-address = "0.5.6" log = "0.4.20" @@ -209,7 +217,7 @@ regex = "1.10.2" ripemd = { version = "0.1.3", default-features = false } rlimit = "0.10.1" rocksdb = "0.21.0" -secp256k1 = { version = "0.24.3", features = [ +secp256k1 = { version = "0.28.2", features = [ "global-context", "rand-std", "serde", @@ -235,14 +243,15 @@ textwrap = "0.16.0" thiserror = "1.0.50" tokio = { version = "1.33.0", features = ["sync", "rt-multi-thread"] } tokio-stream = "0.1.14" +toml = "0.8.8" tonic = { version = "0.10.2", features = ["tls", "gzip", "transport"] } tonic-build = { version = "0.10.2", features = ["prost"] } triggered = "0.1.2" uuid = { version = "1.5.0", features = ["v4", "fast-rng", "serde"] } -wasm-bindgen = { version = "=0.2.87", features = ["serde-serialize"] } -wasm-bindgen-futures = "=0.4.37" -wasm-bindgen-test = "=0.3.37" -web-sys = "=0.3.64" +wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] } +wasm-bindgen-futures = "0.4.40" +wasm-bindgen-test = "0.3.37" +web-sys = "0.3.67" xxhash-rust = { version = "0.8.7", features = ["xxh3"] } zeroize = { version = "1.6.0", default-features = false, features = ["alloc"] } pin-project-lite = "0.2.13" @@ -257,45 +266,49 @@ indexed_db_futures = "0.4.1" # workflow dependencies that are not a part of core libraries # workflow-perf-monitor = { path = "../../../workflow-perf-monitor-rs" } -workflow-perf-monitor = { version = "0.0.2" } +workflow-perf-monitor = "0.0.2" +nw-sys = "0.1.6" # workflow dependencies -workflow-d3 = { version = "0.10.3" } -workflow-nw = { version = "0.10.3" } -workflow-log = { version = "0.10.3" } -workflow-core = { version = "0.10.3" } -workflow-wasm = { version = "0.10.3" } -workflow-dom = { version = "0.10.3" } -workflow-rpc = { version = "0.10.3" } -workflow-node = { version = "0.10.3" } -workflow-store = { version = "0.10.3" } -workflow-terminal = { version = "0.10.3" } -nw-sys = "0.1.6" +workflow-core = { version = "0.12.1" } +workflow-d3 = { version = "0.12.1" } +workflow-dom = { version = "0.12.1" } +workflow-http = { version = "0.12.1" } +workflow-log = { version = "0.12.1" } +workflow-node = { version = "0.12.1" } +workflow-nw = { version = "0.12.1" } +workflow-rpc = { version = "0.12.1" } +workflow-store = { version = "0.12.1" } +workflow-terminal = { version = "0.12.1" } +workflow-wasm = { version = "0.12.1" } # if below is enabled, this means that there is an ongoing work # on the workflow-rs crate. This requires that you clone workflow-rs # into a sibling folder from https://github.com/workflow-rs/workflow-rs -# workflow-d3 = { path = "../workflow-rs/d3" } -# workflow-nw = { path = "../workflow-rs/nw" } -# workflow-log = { path = "../workflow-rs/log" } # workflow-core = { path = "../workflow-rs/core" } -# workflow-wasm = { path = "../workflow-rs/wasm" } +# workflow-d3 = { path = "../workflow-rs/d3" } # workflow-dom = { path = "../workflow-rs/dom" } -# workflow-rpc = { path = "../workflow-rs/rpc" } +# workflow-http = { path = "../workflow-rs/http" } +# workflow-log = { path = "../workflow-rs/log" } # workflow-node = { path = "../workflow-rs/node" } +# workflow-nw = { path = "../workflow-rs/nw" } +# workflow-rpc = { path = "../workflow-rs/rpc" } # workflow-store = { path = "../workflow-rs/store" } # workflow-terminal = { path = "../workflow-rs/terminal" } +# workflow-wasm = { path = "../workflow-rs/wasm" } + # --- -# 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-d3 = { 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-http = { 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-node = { 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-rpc = { 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" } +# workflow-wasm = { 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/LICENSE b/LICENSE index 5b87a24c0..b66757abc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2022-2023 The kaspanet developers +Copyright (c) 2022-2024 Kaspa developers Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/README.md b/README.md index be552c678..a0da46b6a 100644 --- a/README.md +++ b/README.md @@ -151,55 +151,40 @@ To build WASM on MacOS you need to install `llvm` from homebrew (at the time of
- Building WASM framework + Building WASM32 SDK - Rust WebAssembly (Wasm) refers to the use of the Rust programming language to write code that can be compiled into WebAssembly, a binary instruction format that runs in web browsers. This allows for easy development using JS/TS while retaining the benefits of Rust. + Rust WebAssembly (WASM) refers to the use of the Rust programming language to write code that can be compiled into WebAssembly, a binary instruction format that runs in web browsers and NodeJs. This allows for easy development using JavaScript and TypeScript programming languages while retaining the benefits of Rust. - The library can be build in for `NodeJS`, `React Native` and as an `ES6 Module` + WASM SDK components can be built from sources by running: + - `./build-release` - build a full release package (includes both release and debug builds for web and nodejs targets) + - `./build-docs` - build TypeScript documentation + - `./build-web` - release web build + - `./build-web-dev` - development web build + - `./build-nodejs` - release nodejs build + - `./build-nodejs-dev` - development nodejs build -
+ IMPORTANT: do not use `dev` builds in production. They are significantly larger, slower and include debug symbols. - - NodeJS - +### Requirements - ```bash - cd rusty-kaspa - cd wasm - ./build-node - cd nodejs - npm install - ``` - -
- -
- - - ES6 - - - ```bash - cd rusty-kaspa - cd wasm - ./build-web - ``` - -
- -
- This will produce a folder: "nodejs", "web" or "react-native" library in `/wasm` directory depending on your selection. + - NodeJs (v20+): https://nodejs.org/en + - TypeDoc: https://typedoc.org/ +### Builds & documentation + - Release builds: https://github.com/kaspanet/rusty-kaspa/releases + - Developer builds: https://kaspa.aspectron.org/nightly/downloads/ + - Developer TypeScript documentation: https://kaspa.aspectron.org/docs/
-
-Wallet CLI +Kaspa CLI + Wallet - +`kaspa-cli` crate provides cli-driven RPC interface to the node and a +terminal interface to the Rusty Kaspa Wallet runtime. These wallets are +compatible with WASM SDK Wallet API and Kaspa NG projects. ```bash @@ -207,9 +192,6 @@ cd cli cargo run --release ``` -Wallet CLI is now available via the `/cli` or `/kos` projects. -For KOS, please see [`kos/README.md`](kos/README.md) -
diff --git a/check b/check index 0486fe039..1ed39bdd7 100755 --- a/check +++ b/check @@ -1,3 +1,5 @@ +#!/bin/bash + cargo fmt --all cargo clippy --workspace --tests --benches @@ -9,12 +11,27 @@ if [ $status -ne 0 ]; then exit $status fi +declare -a crates=( + "kaspa-wasm" +) + +for crate in "${crates[@]}" +do + cargo clippy -p $crate --target wasm32-unknown-unknown --tests --benches --features wasm32-sdk + status=$? + if [ $status -ne 0 ]; then + echo + echo "--> wasm32 check of $crate failed" + echo + exit $status + fi +done + declare -a crates=( "kaspa-wrpc-wasm" "kaspa-wallet-cli-wasm" "kaspa-wasm" "kaspa-cli" - "kaspa-os" "kaspa-daemon" ) @@ -29,3 +46,21 @@ do exit $status fi done + +declare -a features=( + "wasm32-rpc" + "wasm32-core" + "wasm32-sdk" +) + +for feature in "${features[@]}" +do + cargo clippy -p kaspa-wasm --target wasm32-unknown-unknown --features $feature + status=$? + if [ $status -ne 0 ]; then + echo + echo "--> wasm32 check of kaspa-wasm --features $feature has failed" + echo + exit $status + fi +done diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5b3622d95..f2c80a5fa 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -42,6 +42,7 @@ kaspa-metrics-core.workspace = true kaspa-rpc-core.workspace = true kaspa-utils.workspace = true kaspa-wallet-core.workspace = true +kaspa-wallet-keys.workspace = true kaspa-wrpc-client.workspace = true nw-sys.workspace = true pad.workspace = true diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 3ea408721..4f562e749 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -102,7 +102,7 @@ impl KaspaCli { } pub async fn try_new_arc(options: Options) -> Result> { - let wallet = Arc::new(Wallet::try_new(Wallet::local_store()?, None)?); + let wallet = Arc::new(Wallet::try_new(Wallet::local_store()?, None, None)?); let kaspa_cli = Arc::new(KaspaCli { term: Arc::new(Mutex::new(None)), @@ -160,8 +160,12 @@ impl KaspaCli { self.wallet.rpc_api().clone() } - pub fn rpc_client(&self) -> Option> { - self.wallet.wrpc_client().clone() + pub fn try_rpc_api(&self) -> Option> { + self.wallet.try_rpc_api().clone() + } + + pub fn try_rpc_client(&self) -> Option> { + self.wallet.try_wrpc_client().clone() } pub fn store(&self) -> Arc { @@ -280,6 +284,12 @@ impl KaspaCli { if let Ok(msg) = msg { match *msg { + Events::WalletPing => { + // log_info!("Kaspa NG - received wallet ping"); + }, + Events::Metrics { network_id : _, metrics : _ } => { + // log_info!("Kaspa NG - received metrics event {metrics:?}") + } Events::Error { message } => { terrorln!(this,"{message}"); }, Events::UtxoProcStart => {}, Events::UtxoProcStop => {}, @@ -363,7 +373,7 @@ impl KaspaCli { }, Events::AccountCreate { .. } => { }, Events::AccountUpdate { .. } => { }, - Events::DAAScoreChange { current_daa_score } => { + Events::DaaScoreChange { current_daa_score } => { if this.is_mutted() && this.flags.get(Track::Daa) { tprintln!(this, "{NOTIFY} DAA: {current_daa_score}"); } @@ -441,7 +451,7 @@ impl KaspaCli { 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_strings = BalanceStrings::from((&balance,&network_type, None)); + let balance_strings = BalanceStrings::from((balance.as_ref(),&network_type, None)); let id = id.short(); let mature_utxo_count = balance.as_ref().map(|balance|balance.mature_utxo_count.separated_string()).unwrap_or("N/A".to_string()); diff --git a/cli/src/error.rs b/cli/src/error.rs index 4bbcb1640..a1701be35 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -119,6 +119,9 @@ pub enum Error { #[error(transparent)] MetricsError(kaspa_metrics_core::error::Error), + + #[error(transparent)] + KaspaWalletKeys(#[from] kaspa_wallet_keys::error::Error), } impl Error { diff --git a/cli/src/imports.rs b/cli/src/imports.rs index 4b010f270..a15812804 100644 --- a/cli/src/imports.rs +++ b/cli/src/imports.rs @@ -11,7 +11,7 @@ pub use futures::stream::{Stream, StreamExt, TryStreamExt}; pub use futures::{future::FutureExt, select}; pub use kaspa_consensus_core::network::{NetworkId, NetworkType}; pub use kaspa_utils::hex::*; -pub use kaspa_wallet_core::derivation::gen0::import::*; +pub use kaspa_wallet_core::compat::*; pub use kaspa_wallet_core::prelude::*; pub use kaspa_wallet_core::settings::{DefaultSettings, SettingsStore, WalletSettings}; pub use kaspa_wallet_core::utils::*; diff --git a/cli/src/modules/connect.rs b/cli/src/modules/connect.rs index 7e63ec966..26173256b 100644 --- a/cli/src/modules/connect.rs +++ b/cli/src/modules/connect.rs @@ -1,4 +1,5 @@ use crate::imports::*; +use kaspa_wrpc_client::Resolver; #[derive(Default, Handler)] #[help("Connect to a Kaspa network")] @@ -7,14 +8,41 @@ pub struct Connect; impl Connect { async fn main(self: Arc, ctx: &Arc, argv: Vec, _cmd: &str) -> Result<()> { let ctx = ctx.clone().downcast_arc::()?; - 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 = 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())?; + if let Some(wrpc_client) = ctx.wallet().try_wrpc_client().as_ref() { + let network_id = ctx.wallet().network_id()?; + + let arg_or_server_address = argv.first().cloned().or_else(|| ctx.wallet().settings().get(WalletSettings::Server)); + let (is_public, url) = match arg_or_server_address.as_deref() { + Some("public") => { + tprintln!(ctx, "Connecting to a public node"); + (true, Resolver::default().fetch(WrpcEncoding::Borsh, network_id).await.map_err(|e| e.to_string())?.url) + } + None => { + tprintln!(ctx, "No server set, connecting to a public node"); + (true, Resolver::default().fetch(WrpcEncoding::Borsh, network_id).await.map_err(|e| e.to_string())?.url) + } + Some(url) => { + (false, wrpc_client.parse_url_with_network_type(url.to_string(), network_id.into()).map_err(|e| e.to_string())?) + } + }; + + if is_public { + tpara!( + ctx, + "Please note that default public nodes are community-operated and \ + accessing them may expose your IP address to different node providers. \ + Consider running your own node for better privacy. \ + ", + ); + } + + let options = ConnectOptions { + block_async_connect: true, + strategy: ConnectStrategy::Fallback, + url: Some(url), + ..Default::default() + }; + wrpc_client.connect(Some(options)).await.map_err(|e| e.to_string())?; } else { terrorln!(ctx, "Unable to connect with non-wRPC client"); } diff --git a/cli/src/modules/disconnect.rs b/cli/src/modules/disconnect.rs index 4687fb25c..1e6fc5b6b 100644 --- a/cli/src/modules/disconnect.rs +++ b/cli/src/modules/disconnect.rs @@ -7,8 +7,8 @@ pub struct Disconnect; impl Disconnect { async fn main(self: Arc, ctx: &Arc, _argv: Vec, _cmd: &str) -> Result<()> { let ctx = ctx.clone().downcast_arc::()?; - if let Some(wrpc_client) = ctx.wallet().wrpc_client().as_ref() { - wrpc_client.shutdown().await?; + if let Some(wrpc_client) = ctx.wallet().try_wrpc_client().as_ref() { + wrpc_client.disconnect().await?; } else { terrorln!(ctx, "Unable to disconnect from non-wRPC client"); } diff --git a/cli/src/modules/metrics.rs b/cli/src/modules/metrics.rs index 735cff5d5..54fdd300a 100644 --- a/cli/src/modules/metrics.rs +++ b/cli/src/modules/metrics.rs @@ -50,7 +50,7 @@ impl Handler for Metrics { self.mute.store(mute, Ordering::Relaxed); } - self.metrics.set_rpc(Some(ctx.wallet().rpc_api().clone())); + self.metrics.bind_rpc(Some(ctx.wallet().rpc_api().clone())); Ok(()) } diff --git a/cli/src/modules/monitor.rs b/cli/src/modules/monitor.rs index 2b2098210..5a0500c66 100644 --- a/cli/src/modules/monitor.rs +++ b/cli/src/modules/monitor.rs @@ -108,11 +108,11 @@ impl Monitor { let events = events.lock().unwrap(); events.iter().for_each(|event| match event.deref() { - Events::DAAScoreChange { .. } => {} + Events::DaaScoreChange { .. } => {} Events::Balance { balance, id } => { let network_id = wallet.network_id().expect("missing network type"); let network_type = NetworkType::from(network_id); - let balance_strings = BalanceStrings::from((balance, &network_type, None)); + let balance_strings = BalanceStrings::from((balance.as_ref(), &network_type, None)); let id = id.short(); let mature_utxo_count = diff --git a/cli/src/modules/network.rs b/cli/src/modules/network.rs index 38f221d3f..ddb0093bb 100644 --- a/cli/src/modules/network.rs +++ b/cli/src/modules/network.rs @@ -11,7 +11,7 @@ impl Network { if let Some(network_id) = argv.first() { let network_id: NetworkId = network_id.trim().parse::()?; tprintln!(ctx, "Setting network id to: {network_id}"); - ctx.wallet().set_network_id(network_id)?; + ctx.wallet().set_network_id(&network_id)?; ctx.wallet().settings().set(WalletSettings::Network, network_id).await?; } else { let network_id = ctx.wallet().network_id()?; diff --git a/cli/src/modules/node.rs b/cli/src/modules/node.rs index 468d0e8c4..c87d369f3 100644 --- a/cli/src/modules/node.rs +++ b/cli/src/modules/node.rs @@ -106,7 +106,7 @@ impl Node { tprintln!(ctx, "starting kaspa node... {}", style("(use 'node mute' to mute logging)").dim()); } - let wrpc_client = ctx.wallet().wrpc_client().ok_or(Error::custom("Unable to start node with non-wRPC client"))?; + let wrpc_client = ctx.wallet().try_wrpc_client().ok_or(Error::custom("Unable to start node with non-wRPC client"))?; kaspad.configure(self.create_config(&ctx).await?).await?; kaspad.start().await?; @@ -129,7 +129,7 @@ impl Node { }; for _ in 0..5 { sleep(Duration::from_millis(1000)).await; - if wrpc_client.connect(options.clone()).await.is_ok() { + if wrpc_client.connect(Some(options.clone())).await.is_ok() { break; } } diff --git a/cli/src/wizards/account.rs b/cli/src/wizards/account.rs index 350c5a7aa..7a3afb73b 100644 --- a/cli/src/wizards/account.rs +++ b/cli/src/wizards/account.rs @@ -64,7 +64,7 @@ async fn create_multisig(ctx: &Arc, account_name: Option, mnem let mut prv_key_data_args = Vec::with_capacity(prv_keys_len); for _ in 0..prv_keys_len { - let bip39_mnemonic = Mnemonic::random(mnemonic_phrase_word_count, Language::default())?.phrase().to_string(); + let bip39_mnemonic = Secret::from(Mnemonic::random(mnemonic_phrase_word_count, Language::default())?.phrase()); 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?; diff --git a/cli/src/wizards/wallet.rs b/cli/src/wizards/wallet.rs index f20aab08b..8563a8619 100644 --- a/cli/src/wizards/wallet.rs +++ b/cli/src/wizards/wallet.rs @@ -110,12 +110,12 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_ let prv_key_data_args = if import_with_mnemonic { let words = crate::wizards::import::prompt_for_mnemonic(&term).await?; - PrvKeyDataCreateArgs::new(None, payment_secret.clone(), words.join(" ")) + PrvKeyDataCreateArgs::new(None, payment_secret.clone(), Secret::from(words.join(" "))) } else { PrvKeyDataCreateArgs::new( None, payment_secret.clone(), - Mnemonic::random(word_count, Language::default())?.phrase().to_string(), + Secret::from(Mnemonic::random(word_count, Language::default())?.phrase()), ) }; @@ -147,7 +147,7 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_ tpara!( ctx, - "Your mnemonic phrase allows your to re-create your private key. \ + "Your mnemonic phrase allows you to re-create your private key. \ The person who has access to this mnemonic will have full control of \ the Kaspa stored in it. Keep your mnemonic safe. Write it down and \ store it in a safe, preferably in a fire-resistant location. Do not \ @@ -159,7 +159,7 @@ pub(crate) async fn create(ctx: &Arc, name: Option<&str>, import_with_ // descriptor - ["", "Never share your mnemonic with anyone!", "---", "", "Your default wallet account mnemonic:", mnemonic_phrase.as_str()] + ["", "Never share your mnemonic with anyone!", "---", "", "Your default wallet account mnemonic:", mnemonic_phrase.as_str()?] .into_iter() .for_each(|line| term.writeln(line)); } diff --git a/consensus/client/Cargo.toml b/consensus/client/Cargo.toml new file mode 100644 index 000000000..38cbed9a3 --- /dev/null +++ b/consensus/client/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "kaspa-consensus-client" +description = "Kaspa consensus client data structures" +version.workspace = true +edition.workspace = true +authors.workspace = true +include.workspace = true +license.workspace = true +repository.workspace = true + +[features] +wasm32-sdk = [] +wasm32-types = [] + +[dependencies] +kaspa-addresses.workspace = true +kaspa-consensus-core.workspace = true +kaspa-hashes.workspace = true +kaspa-math.workspace = true +kaspa-txscript.workspace = true +kaspa-utils.workspace = true +kaspa-wasm-core.workspace = true + +ahash.workspace = true +cfg-if.workspace = true +faster-hex.workspace = true +hex.workspace = true +js-sys.workspace = true +rand.workspace = true +secp256k1.workspace = true +serde_json.workspace = true +serde-wasm-bindgen.workspace = true +serde.workspace = true +thiserror.workspace = true +wasm-bindgen.workspace = true +itertools.workspace = true + +workflow-wasm.workspace = true +workflow-log.workspace = true + +[lints.clippy] +empty_docs = "allow" diff --git a/consensus/client/src/error.rs b/consensus/client/src/error.rs new file mode 100644 index 000000000..e0aab2156 --- /dev/null +++ b/consensus/client/src/error.rs @@ -0,0 +1,106 @@ +use thiserror::Error; +use wasm_bindgen::{JsError, JsValue}; +use workflow_wasm::jserror::JsErrorData; + +#[derive(Debug, Error, Clone)] +pub enum Error { + #[error("{0}")] + Custom(String), + + #[error(transparent)] + JsValue(JsErrorData), + + #[error(transparent)] + Wasm(#[from] workflow_wasm::error::Error), + + #[error(transparent)] + ScriptBuilder(#[from] kaspa_txscript::script_builder::ScriptBuilderError), + + #[error("{0}")] + ParseInt(#[from] std::num::ParseIntError), + + #[error(transparent)] + FasterHex(#[from] faster_hex::Error), + + #[error("invalid transaction outpoint: {0}")] + InvalidTransactionOutpoint(String), + + #[error(transparent)] + Secp256k1(#[from] secp256k1::Error), + + #[error(transparent)] + Sign(#[from] kaspa_consensus_core::sign::Error), + + #[error(transparent)] + SerdeWasmBindgen(JsErrorData), + + #[error(transparent)] + Address(#[from] kaspa_addresses::AddressError), + + #[error(transparent)] + NetworkType(#[from] kaspa_consensus_core::network::NetworkTypeError), + + #[error("Error converting property `{0}`: {1}")] + Convert(&'static str, String), + + #[error("Error processing JSON: {0}")] + SerdeJson(String), + + #[error("Transaction input is missing UTXO entry")] + MissingUtxoEntry, +} + +impl Error { + pub fn custom>(msg: T) -> Self { + Error::Custom(msg.into()) + } + + pub fn convert(prop: &'static str, msg: S) -> Self { + Self::Convert(prop, msg.to_string()) + } +} + +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()) + } +} + +impl From for JsValue { + fn from(value: Error) -> Self { + match value { + Error::JsValue(js_error_data) => js_error_data.into(), + _ => JsValue::from(value.to_string()), + } + } +} + +impl From for Error { + fn from(err: JsValue) -> Self { + Self::JsValue(err.into()) + } +} + +impl From for Error { + fn from(err: JsError) -> Self { + Self::JsValue(err.into()) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self::SerdeJson(err.to_string()) + } +} + +impl From for Error { + fn from(err: serde_wasm_bindgen::Error) -> Self { + Self::SerdeWasmBindgen(JsValue::from(err).into()) + } +} diff --git a/consensus/client/src/hash.rs b/consensus/client/src/hash.rs new file mode 100644 index 000000000..4402cfb1b --- /dev/null +++ b/consensus/client/src/hash.rs @@ -0,0 +1,55 @@ +use crate::imports::*; +use crate::result::Result; +use kaspa_hashes as native; +use kaspa_hashes::HasherBase; +use kaspa_wasm_core::types::BinaryT; + +/// @category Wallet SDK +#[derive(Default, Clone)] +#[wasm_bindgen] +pub struct TransactionSigningHash { + hasher: native::TransactionSigningHash, +} + +#[wasm_bindgen] +impl TransactionSigningHash { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { hasher: native::TransactionSigningHash::new() } + } + + pub fn update(&mut self, data: BinaryT) -> Result<()> { + let data = JsValue::from(data).try_as_vec_u8()?; + self.hasher.update(data); + Ok(()) + } + + pub fn finalize(&self) -> String { + self.hasher.clone().finalize().to_string() + } +} + +/// @category Wallet SDK +#[derive(Default, Clone)] +#[wasm_bindgen] +pub struct TransactionSigningHashECDSA { + hasher: native::TransactionSigningHashECDSA, +} + +#[wasm_bindgen] +impl TransactionSigningHashECDSA { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { hasher: native::TransactionSigningHashECDSA::new() } + } + + pub fn update(&mut self, data: BinaryT) -> Result<()> { + let data = JsValue::from(data).try_as_vec_u8()?; + self.hasher.update(data); + Ok(()) + } + + pub fn finalize(&self) -> String { + self.hasher.clone().finalize().to_string() + } +} diff --git a/consensus/client/src/header.rs b/consensus/client/src/header.rs new file mode 100644 index 000000000..6294d2132 --- /dev/null +++ b/consensus/client/src/header.rs @@ -0,0 +1,290 @@ +use crate::error::Error; +use js_sys::{Array, Object}; +use kaspa_consensus_core::hashing; +use kaspa_consensus_core::header as native; +use kaspa_hashes::Hash; +use kaspa_utils::hex::ToHex; +use serde::{Deserialize, Serialize}; +use serde_wasm_bindgen::*; +use wasm_bindgen::prelude::*; +use wasm_bindgen::prelude::{JsError, JsValue}; +use workflow_wasm::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_HEADER: &'static str = r#" +/** + * Interface defining the structure of a block header. + * + * @category Consensus + */ +export interface IHeader { + hash: HexString; + version: number; + parentsByLevel: Array>; + hashMerkleRoot: HexString; + acceptedIdMerkleRoot: HexString; + utxoCommitment: HexString; + timestamp: bigint; + bits: number; + nonce: bigint; + daaScore: bigint; + blueWork: bigint | HexString; + blueScore: bigint; + pruningPoint: HexString; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IHeader | Header")] + pub type IHeader; +} + +/// @category Consensus +#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] +#[serde(rename_all = "camelCase")] +#[wasm_bindgen(inspectable)] +pub struct Header { + inner: native::Header, +} + +impl Header { + #[inline] + pub fn inner(&self) -> &native::Header { + &self.inner + } + + #[inline] + pub fn inner_mut(&mut self) -> &mut native::Header { + &mut self.inner + } +} + +#[cfg(feature = "wasm32-sdk")] +#[wasm_bindgen] +impl Header { + #[wasm_bindgen(constructor)] + pub fn constructor(js_value: IHeader) -> std::result::Result { + Ok(js_value.try_into_owned()?) + } + + /// Finalizes the header and recomputes (updates) the header hash + /// @return { String } header hash + #[wasm_bindgen(js_name = finalize)] + pub fn finalize_js(&mut self) -> String { + // let inner = self.inner.lock().unwrap(); + let inner = self.inner_mut(); + inner.hash = hashing::header::hash(inner); + inner.hash.to_hex() + } + + /// Obtain `JSON` representation of the header. JSON representation + /// should be obtained using WASM, to ensure proper serialization of + /// big integers. + #[wasm_bindgen(js_name = asJSON)] + pub fn as_json(&self) -> String { + serde_json::to_string(self.inner()).unwrap() + } + + #[wasm_bindgen(getter = version)] + pub fn get_version(&self) -> u16 { + self.inner().version + } + + #[wasm_bindgen(setter = version)] + pub fn set_version(&mut self, version: u16) { + self.inner_mut().version = version + } + + #[wasm_bindgen(getter = timestamp)] + pub fn get_timestamp(&self) -> u64 { + self.inner().timestamp + } + + #[wasm_bindgen(setter = timestamp)] + pub fn set_timestamp(&mut self, timestamp: u64) { + self.inner_mut().timestamp = timestamp + } + + #[wasm_bindgen(getter = bits)] + pub fn bits(&self) -> u32 { + self.inner().bits + } + + #[wasm_bindgen(setter = bits)] + pub fn set_bits(&mut self, bits: u32) { + self.inner_mut().bits = bits + } + + #[wasm_bindgen(getter = nonce)] + pub fn nonce(&self) -> u64 { + self.inner().nonce + } + + #[wasm_bindgen(setter = nonce)] + pub fn set_nonce(&mut self, nonce: u64) { + self.inner_mut().nonce = nonce + } + + #[wasm_bindgen(getter = daaScore)] + pub fn daa_score(&self) -> u64 { + self.inner().daa_score + } + + #[wasm_bindgen(setter = daaScore)] + pub fn set_daa_score(&mut self, daa_score: u64) { + self.inner_mut().daa_score = daa_score + } + + #[wasm_bindgen(getter = blueScore)] + pub fn blue_score(&self) -> u64 { + self.inner().blue_score + } + + #[wasm_bindgen(setter = blueScore)] + pub fn set_blue_score(&mut self, blue_score: u64) { + self.inner_mut().blue_score = blue_score + } + + #[wasm_bindgen(getter = hash)] + pub fn get_hash_as_hex(&self) -> String { + self.inner().hash.to_hex() + } + + #[wasm_bindgen(getter = hashMerkleRoot)] + pub fn get_hash_merkle_root_as_hex(&self) -> String { + self.inner().hash_merkle_root.to_hex() + } + + #[wasm_bindgen(setter = hashMerkleRoot)] + pub fn set_hash_merkle_root_from_js_value(&mut self, js_value: JsValue) { + self.inner_mut().hash_merkle_root = Hash::from_slice(&js_value.try_as_vec_u8().expect("hash merkle root")); + } + + #[wasm_bindgen(getter = acceptedIdMerkleRoot)] + pub fn get_accepted_id_merkle_root_as_hex(&self) -> String { + self.inner().accepted_id_merkle_root.to_hex() + } + + #[wasm_bindgen(setter = acceptedIdMerkleRoot)] + pub fn set_accepted_id_merkle_root_from_js_value(&mut self, js_value: JsValue) { + self.inner_mut().accepted_id_merkle_root = Hash::from_slice(&js_value.try_as_vec_u8().expect("accepted id merkle root")); + } + + #[wasm_bindgen(getter = utxoCommitment)] + pub fn get_utxo_commitment_as_hex(&self) -> String { + self.inner().utxo_commitment.to_hex() + } + + #[wasm_bindgen(setter = utxoCommitment)] + pub fn set_utxo_commitment_from_js_value(&mut self, js_value: JsValue) { + self.inner_mut().utxo_commitment = Hash::from_slice(&js_value.try_as_vec_u8().expect("utxo commitment")); + } + + #[wasm_bindgen(getter = pruningPoint)] + pub fn get_pruning_point_as_hex(&self) -> String { + self.inner().pruning_point.to_hex() + } + + #[wasm_bindgen(setter = pruningPoint)] + pub fn set_pruning_point_from_js_value(&mut self, js_value: JsValue) { + self.inner_mut().pruning_point = Hash::from_slice(&js_value.try_as_vec_u8().expect("pruning point")); + } + + #[wasm_bindgen(getter = parentsByLevel)] + pub fn get_parents_by_level_as_js_value(&self) -> JsValue { + to_value(&self.inner().parents_by_level).expect("invalid parents_by_level") + } + + #[wasm_bindgen(setter = parentsByLevel)] + pub fn set_parents_by_level_from_js_value(&mut self, js_value: JsValue) { + let array = Array::from(&js_value); + self.inner_mut().parents_by_level = array + .iter() + .map(|jsv| { + Array::from(&jsv) + .to_vec() + .iter() + .map(|hash| Ok(hash.try_into_owned()?)) + .collect::, Error>>() + }) + .collect::>, Error>>() + .unwrap_or_else(|err| { + panic!("{}", err); + }); + } + + #[wasm_bindgen(getter = blueWork)] + pub fn blue_work(&self) -> js_sys::BigInt { + self.inner().blue_work.try_into().unwrap_or_else(|err| panic!("invalid blue work: {err}")) + } + + #[wasm_bindgen(js_name = getBlueWorkAsHex)] + pub fn get_blue_work_as_hex(&self) -> String { + self.inner().blue_work.to_hex() + } + + #[wasm_bindgen(setter = blueWork)] + pub fn set_blue_work_from_js_value(&mut self, js_value: JsValue) { + self.inner_mut().blue_work = js_value.try_into().unwrap_or_else(|err| panic!("invalid blue work: {err}")); + } +} + +impl TryCastFromJs for Header { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(object) = Object::try_from(value.as_ref()) { + let parents_by_level = object + .get_vec("parentsByLevel")? + .iter() + .map(|jsv| { + Array::from(jsv) + .to_vec() + .into_iter() + .map(|hash| Ok(hash.try_into_owned()?)) + .collect::, Error>>() + }) + .collect::>, Error>>()?; + + let header = native::Header { + hash: object.get_value("hash")?.try_into_owned().unwrap_or_default(), + version: object.get_u16("version")?, + parents_by_level, + hash_merkle_root: object + .get_value("hashMerkleRoot")? + .try_into_owned() + .map_err(|err| Error::convert("hashMerkleRoot", err))?, + accepted_id_merkle_root: object + .get_value("acceptedIdMerkleRoot")? + .try_into_owned() + .map_err(|err| Error::convert("acceptedIdMerkleRoot", err))?, + utxo_commitment: object + .get_value("utxoCommitment")? + .try_into_owned() + .map_err(|err| Error::convert("utxoCommitment", err))?, + nonce: object.get_u64("nonce")?, + timestamp: object.get_u64("timestamp")?, + daa_score: object.get_u64("daaScore")?, + bits: object.get_u32("bits")?, + blue_work: object.get_value("blueWork")?.try_into().map_err(|err| Error::convert("blueWork", err))?, + blue_score: object.get_u64("blueScore")?, + pruning_point: object + .get_value("pruningPoint")? + .try_into_owned() + .map_err(|err| Error::convert("pruningPoint", err))?, + }; + + Ok(header.into()) + } else { + Err(Error::Custom("supplied argument must be an object".to_string())) + } + }) + } +} + +impl From for Header { + fn from(header: native::Header) -> Self { + Self { inner: header } + } +} diff --git a/consensus/wasm/src/imports.rs b/consensus/client/src/imports.rs similarity index 92% rename from consensus/wasm/src/imports.rs rename to consensus/client/src/imports.rs index 010147c92..844753fb5 100644 --- a/consensus/wasm/src/imports.rs +++ b/consensus/client/src/imports.rs @@ -2,7 +2,6 @@ pub use crate::error::Error; pub use js_sys::{Array, Object}; pub use kaspa_consensus_core::tx as cctx; pub use kaspa_consensus_core::tx::{ScriptPublicKey, TransactionId, TransactionIndexType}; -pub use kaspa_utils::hex::*; pub use serde::{Deserialize, Serialize}; pub use std::sync::{Arc, Mutex, MutexGuard}; pub use wasm_bindgen::prelude::*; diff --git a/consensus/client/src/input.rs b/consensus/client/src/input.rs new file mode 100644 index 000000000..8b48c2d94 --- /dev/null +++ b/consensus/client/src/input.rs @@ -0,0 +1,220 @@ +use crate::imports::*; +use crate::result::Result; +use crate::TransactionOutpoint; +use crate::UtxoEntryReference; +use kaspa_utils::hex::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TRANSACTION: &'static str = r#" +/** + * Interface defines the structure of a transaction input. + * + * @category Consensus + */ +export interface ITransactionInput { + previousOutpoint: ITransactionOutpoint; + signatureScript: HexString; + sequence: bigint; + sigOpCount: number; + utxo?: UtxoEntryReference; + + /** Optional verbose data provided by RPC */ + verboseData?: ITransactionInputVerboseData; +} + +/** + * Option transaction input verbose data. + * + * @category Node RPC + */ +export interface ITransactionInputVerboseData { } + +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ITransactionInput")] + pub type ITransactionInput; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionInputInner { + pub previous_outpoint: TransactionOutpoint, + pub signature_script: Vec, + pub sequence: u64, + pub sig_op_count: u8, + pub utxo: Option, +} + +impl TransactionInputInner { + pub fn new( + previous_outpoint: TransactionOutpoint, + signature_script: Vec, + sequence: u64, + sig_op_count: u8, + utxo: Option, + ) -> Self { + Self { previous_outpoint, signature_script, sequence, sig_op_count, utxo } + } +} + +/// Represents a Kaspa transaction input +/// @category Consensus +#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] +#[wasm_bindgen(inspectable)] +pub struct TransactionInput { + inner: Arc>, +} + +impl TransactionInput { + pub fn new( + previous_outpoint: TransactionOutpoint, + signature_script: Vec, + sequence: u64, + sig_op_count: u8, + utxo: Option, + ) -> Self { + let inner = TransactionInputInner::new(previous_outpoint, signature_script, sequence, sig_op_count, utxo); + Self { inner: Arc::new(Mutex::new(inner)) } + } + + pub fn new_with_inner(inner: TransactionInputInner) -> Self { + Self { inner: Arc::new(Mutex::new(inner)) } + } + + pub fn inner(&self) -> MutexGuard<'_, TransactionInputInner> { + self.inner.lock().unwrap() + } + + pub fn sig_op_count(&self) -> u8 { + self.inner().sig_op_count + } + + pub fn utxo(&self) -> Option { + self.inner().utxo.clone() + } +} + +#[wasm_bindgen] +impl TransactionInput { + #[wasm_bindgen(constructor)] + pub fn constructor(value: &ITransactionInput) -> Result { + Self::try_owned_from(value) + } + + #[wasm_bindgen(getter = previousOutpoint)] + pub fn get_previous_outpoint(&self) -> TransactionOutpoint { + self.inner().previous_outpoint.clone() + } + + #[wasm_bindgen(setter = previousOutpoint)] + pub fn set_previous_outpoint(&mut self, js_value: &JsValue) -> Result<()> { + match js_value.try_into() { + Ok(outpoint) => { + self.inner().previous_outpoint = outpoint; + Ok(()) + } + Err(_) => Err(Error::custom("invalid outpoint script".to_string())), + } + } + + #[wasm_bindgen(getter = signatureScript)] + pub fn get_signature_script_as_hex(&self) -> String { + self.inner().signature_script.to_hex() + } + + #[wasm_bindgen(setter = signatureScript)] + pub fn set_signature_script_from_js_value(&mut self, js_value: JsValue) -> Result<()> { + match js_value.try_as_vec_u8() { + Ok(signature) => { + self.set_signature_script(signature); + Ok(()) + } + Err(_) => Err(Error::custom("invalid signature script".to_string())), + } + } + + #[wasm_bindgen(getter = sequence)] + pub fn get_sequence(&self) -> u64 { + self.inner().sequence + } + + #[wasm_bindgen(setter = sequence)] + pub fn set_sequence(&mut self, sequence: u64) { + self.inner().sequence = sequence; + } + + #[wasm_bindgen(getter = sigOpCount)] + pub fn get_sig_op_count(&self) -> u8 { + self.inner().sig_op_count + } + + #[wasm_bindgen(setter = sigOpCount)] + pub fn set_sig_op_count(&mut self, sig_op_count: u8) { + self.inner().sig_op_count = sig_op_count; + } + + #[wasm_bindgen(getter = utxo)] + pub fn get_utxo(&self) -> Option { + self.inner().utxo.clone() + } +} + +impl TransactionInput { + pub fn set_signature_script(&self, signature_script: Vec) { + self.inner().signature_script = signature_script; + } + + pub fn script_public_key(&self) -> Option { + self.utxo().map(|utxo_ref| utxo_ref.utxo.script_public_key.clone()) + } +} + +impl AsRef for TransactionInput { + fn as_ref(&self) -> &TransactionInput { + self + } +} + +impl TryCastFromJs for TransactionInput { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> std::result::Result, Self::Error> { + Self::resolve_cast(&value, || { + if let Some(object) = Object::try_from(value.as_ref()) { + let previous_outpoint: TransactionOutpoint = object.get_value("previousOutpoint")?.as_ref().try_into()?; + let signature_script = object.get_vec_u8("signatureScript")?; + let sequence = object.get_u64("sequence")?; + let sig_op_count = object.get_u8("sigOpCount")?; + let utxo = object.try_get_cast::("utxo")?.map(Cast::into_owned); + Ok(TransactionInput::new(previous_outpoint, signature_script, sequence, sig_op_count, utxo).into()) + } else { + Err("TransactionInput must be an object".into()) + } + }) + } +} + +impl From for TransactionInput { + fn from(tx_input: cctx::TransactionInput) -> Self { + TransactionInput::new( + tx_input.previous_outpoint.into(), + tx_input.signature_script, + tx_input.sequence, + tx_input.sig_op_count, + None, + ) + } +} + +impl From<&TransactionInput> for cctx::TransactionInput { + fn from(tx_input: &TransactionInput) -> Self { + let inner = tx_input.inner(); + cctx::TransactionInput::new( + inner.previous_outpoint.clone().into(), + inner.signature_script.clone(), + inner.sequence, + inner.sig_op_count, + ) + } +} diff --git a/consensus/client/src/lib.rs b/consensus/client/src/lib.rs new file mode 100644 index 000000000..4935b16f7 --- /dev/null +++ b/consensus/client/src/lib.rs @@ -0,0 +1,33 @@ +pub mod error; +mod imports; +mod outpoint; +mod output; +pub mod result; +mod utxo; +pub use outpoint::*; +pub use output::*; +pub use utxo::*; + +cfg_if::cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + mod header; + mod input; + mod transaction; + mod vtx; + mod hash; + mod sign; + mod script; + mod serializable; + + + pub use header::*; + pub use input::*; + pub use transaction::*; + pub use serializable::*; + pub use vtx::*; + pub use hash::*; + // pub use signing::*; + pub use script::*; + pub use sign::sign_with_multiple_v3; + } +} diff --git a/consensus/wasm/src/outpoint.rs b/consensus/client/src/outpoint.rs similarity index 77% rename from consensus/wasm/src/outpoint.rs rename to consensus/client/src/outpoint.rs index d0656ed36..77e17d542 100644 --- a/consensus/wasm/src/outpoint.rs +++ b/consensus/client/src/outpoint.rs @@ -1,6 +1,19 @@ use crate::imports::*; use crate::result::Result; +#[wasm_bindgen(typescript_custom_section)] +const TS_TRANSACTION_OUTPOINT: &'static str = r#" +/** + * Interface defines the structure of a transaction outpoint (used by transaction input). + * + * @category Consensus + */ +export interface ITransactionOutpoint { + transactionId: HexString; + index: number; +} +"#; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd)] #[serde(rename_all = "camelCase")] pub struct TransactionOutpointInner { @@ -44,8 +57,8 @@ impl TryFrom<&JsValue> for TransactionOutpointInner { } else { Err(Error::InvalidTransactionOutpoint(string)) } - } else if let Some(object) = Object::try_from(js_value) { - let transaction_id: TransactionId = object.get_value("transactionId")?.try_into()?; + } else if let Some(object) = js_sys::Object::try_from(js_value) { + let transaction_id: TransactionId = object.get_value("transactionId")?.try_into_owned()?; let index = object.get_u32("index")?; Ok(TransactionOutpointInner::new(transaction_id, index)) } else { @@ -58,7 +71,8 @@ impl TryFrom<&JsValue> for TransactionOutpointInner { /// NOTE: This struct is immutable - to create a custom outpoint /// use the `TransactionOutpoint::new` constructor. (in JavaScript /// use `new TransactionOutpoint(transactionId, index)`). -#[derive(Clone, Debug, Serialize, Deserialize)] +/// @category Consensus +#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] #[serde(rename_all = "camelCase")] #[wasm_bindgen(inspectable)] pub struct TransactionOutpoint { @@ -66,6 +80,10 @@ pub struct TransactionOutpoint { } impl TransactionOutpoint { + pub fn new(transaction_id: TransactionId, index: u32) -> TransactionOutpoint { + Self { inner: Arc::new(TransactionOutpointInner { transaction_id, index }) } + } + #[inline(always)] pub fn inner(&self) -> &TransactionOutpointInner { &self.inner @@ -76,6 +94,11 @@ impl TransactionOutpoint { self.inner().transaction_id } + #[inline(always)] + pub fn index(&self) -> TransactionIndexType { + self.inner().index + } + #[inline(always)] pub fn transaction_id_as_ref(&self) -> &TransactionId { &self.inner().transaction_id @@ -87,24 +110,24 @@ impl TransactionOutpoint { } } -#[wasm_bindgen] +#[cfg_attr(feature = "wasm32-sdk", wasm_bindgen)] impl TransactionOutpoint { - #[wasm_bindgen(constructor)] - pub fn new(transaction_id: TransactionId, index: u32) -> TransactionOutpoint { + #[cfg_attr(feature = "wasm32-sdk", wasm_bindgen(constructor))] + pub fn ctor(transaction_id: TransactionId, index: u32) -> TransactionOutpoint { Self { inner: Arc::new(TransactionOutpointInner { transaction_id, index }) } } - #[wasm_bindgen(js_name = "getId")] + #[cfg_attr(feature = "wasm32-sdk", wasm_bindgen(js_name = "getId"))] pub fn id_string(&self) -> String { format!("{}-{}", self.get_transaction_id_as_string(), self.get_index()) } - #[wasm_bindgen(getter, js_name = transactionId)] + #[cfg_attr(feature = "wasm32-sdk", wasm_bindgen(getter, js_name = transactionId))] pub fn get_transaction_id_as_string(&self) -> String { self.inner().transaction_id.to_string() } - #[wasm_bindgen(getter, js_name = index)] + #[cfg_attr(feature = "wasm32-sdk", wasm_bindgen(getter, js_name = index))] pub fn get_index(&self) -> TransactionIndexType { self.inner().index } @@ -117,9 +140,9 @@ impl std::fmt::Display for TransactionOutpoint { } } -impl TryFrom for TransactionOutpoint { +impl TryFrom<&JsValue> for TransactionOutpoint { type Error = Error; - fn try_from(js_value: JsValue) -> Result { + fn try_from(js_value: &JsValue) -> Result { let inner: TransactionOutpointInner = js_value.as_ref().try_into()?; Ok(TransactionOutpoint { inner: Arc::new(inner) }) } diff --git a/consensus/wasm/src/output.rs b/consensus/client/src/output.rs similarity index 61% rename from consensus/wasm/src/output.rs rename to consensus/client/src/output.rs index 64686a256..99fe38ec3 100644 --- a/consensus/wasm/src/output.rs +++ b/consensus/client/src/output.rs @@ -1,16 +1,41 @@ use crate::imports::*; +#[wasm_bindgen(typescript_custom_section)] +const TS_TRANSACTION_OUTPUT: &'static str = r#" +/** + * Interface defining the structure of a transaction output. + * + * @category Consensus + */ +export interface ITransactionOutput { + value: bigint; + scriptPublicKey: IScriptPublicKey; + + /** Optional verbose data provided by RPC */ + verboseData?: ITransactionOutputVerboseData; +} + +/** + * TransactionOutput verbose data. + * + * @category Node RPC + */ +export interface ITransactionOutputVerboseData { + scriptPublicKeyType : string; + scriptPublicKeyAddress : string; +} +"#; + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[wasm_bindgen(inspectable)] pub struct TransactionOutputInner { pub value: u64, - #[wasm_bindgen(js_name = scriptPublicKey, getter_with_clone)] pub script_public_key: ScriptPublicKey, } /// Represents a Kaspad transaction output -#[derive(Clone, Debug, Serialize, Deserialize)] +/// @category Consensus +#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] #[serde(rename_all = "camelCase")] #[wasm_bindgen(inspectable)] pub struct TransactionOutput { @@ -18,6 +43,10 @@ pub struct TransactionOutput { } impl TransactionOutput { + pub fn new(value: u64, script_public_key: ScriptPublicKey) -> TransactionOutput { + Self { inner: Arc::new(Mutex::new(TransactionOutputInner { value, script_public_key })) } + } + pub fn new_with_inner(inner: TransactionOutputInner) -> Self { Self { inner: Arc::new(Mutex::new(inner)) } } @@ -29,20 +58,13 @@ impl TransactionOutput { pub fn script_length(&self) -> usize { self.inner().script_public_key.script().len() } - - // pub fn set_amount(&self, amount: u64) { - // self.inner().value = amount; - // } - // pub fn value(&self) -> u64 { - // self.inner().value - // } } #[wasm_bindgen] impl TransactionOutput { #[wasm_bindgen(constructor)] /// TransactionOutput constructor - pub fn new(value: u64, script_public_key: &ScriptPublicKey) -> TransactionOutput { + pub fn ctor(value: u64, script_public_key: &ScriptPublicKey) -> TransactionOutput { Self { inner: Arc::new(Mutex::new(TransactionOutputInner { value, script_public_key: script_public_key.clone() })) } } @@ -65,12 +87,6 @@ impl TransactionOutput { pub fn set_script_public_key(&self, v: &ScriptPublicKey) { self.inner().script_public_key = v.clone(); } - - // TODO-WASM - // #[wasm_bindgen(js_name=isDust)] - // pub fn is_dust(&self) -> bool { - // is_transaction_output_dust(self) - // } } impl AsRef for TransactionOutput { @@ -81,7 +97,13 @@ impl AsRef for TransactionOutput { impl From for TransactionOutput { fn from(output: cctx::TransactionOutput) -> Self { - TransactionOutput::new(output.value, &output.script_public_key) + TransactionOutput::new(output.value, output.script_public_key) + } +} + +impl From<&cctx::TransactionOutput> for TransactionOutput { + fn from(output: &cctx::TransactionOutput) -> Self { + TransactionOutput::new(output.value, output.script_public_key.clone()) } } @@ -92,18 +114,25 @@ impl From<&TransactionOutput> for cctx::TransactionOutput { } } -impl TryFrom for TransactionOutput { +impl TryFrom<&JsValue> for TransactionOutput { type Error = Error; - fn try_from(js_value: JsValue) -> Result { + fn try_from(js_value: &JsValue) -> Result { // workflow_log::log_trace!("js_value->TransactionOutput: {js_value:?}"); - if let Some(object) = Object::try_from(&js_value) { + if let Some(object) = Object::try_from(js_value) { let has_address = Object::has_own(object, &JsValue::from("address")); workflow_log::log_trace!("js_value->TransactionOutput: has_address:{has_address:?}"); let value = object.get_u64("value")?; - let script_public_key = ScriptPublicKey::try_from(object.get_value("scriptPublicKey")?)?; - Ok(TransactionOutput::new(value, &script_public_key)) + let script_public_key = ScriptPublicKey::try_cast_from(object.get_value("scriptPublicKey")?)?; + Ok(TransactionOutput::new(value, script_public_key.into_owned())) } else { Err("TransactionInput must be an object".into()) } } } + +impl TryFrom for TransactionOutput { + type Error = Error; + fn try_from(js_value: JsValue) -> Result { + Self::try_from(&js_value) + } +} diff --git a/consensus/client/src/result.rs b/consensus/client/src/result.rs new file mode 100644 index 000000000..4c8cb83f5 --- /dev/null +++ b/consensus/client/src/result.rs @@ -0,0 +1 @@ +pub type Result = std::result::Result; diff --git a/consensus/client/src/script.rs b/consensus/client/src/script.rs new file mode 100644 index 000000000..7392b1d85 --- /dev/null +++ b/consensus/client/src/script.rs @@ -0,0 +1,457 @@ +use std::cell::{Ref, RefCell, RefMut}; +use std::rc::Rc; + +use kaspa_wasm_core::types::{BinaryT, HexString}; + +use crate::imports::*; +use crate::result::Result; +use kaspa_txscript::script_builder as native; + +#[wasm_bindgen(typescript_custom_section)] +const TS_SCRIPT_OPCODES: &'static str = r#" +/** + * Kaspa Transaction Script Opcodes + * @see {@link ScriptBuilder} + * @category Consensus + */ +export enum Opcode { + OpData1 = 0x01, + OpData2 = 0x02, + OpData3 = 0x03, + OpData4 = 0x04, + OpData5 = 0x05, + OpData6 = 0x06, + OpData7 = 0x07, + OpData8 = 0x08, + OpData9 = 0x09, + OpData10 = 0x0a, + OpData11 = 0x0b, + OpData12 = 0x0c, + OpData13 = 0x0d, + OpData14 = 0x0e, + OpData15 = 0x0f, + OpData16 = 0x10, + OpData17 = 0x11, + OpData18 = 0x12, + OpData19 = 0x13, + OpData20 = 0x14, + OpData21 = 0x15, + OpData22 = 0x16, + OpData23 = 0x17, + OpData24 = 0x18, + OpData25 = 0x19, + OpData26 = 0x1a, + OpData27 = 0x1b, + OpData28 = 0x1c, + OpData29 = 0x1d, + OpData30 = 0x1e, + OpData31 = 0x1f, + OpData32 = 0x20, + OpData33 = 0x21, + OpData34 = 0x22, + OpData35 = 0x23, + OpData36 = 0x24, + OpData37 = 0x25, + OpData38 = 0x26, + OpData39 = 0x27, + OpData40 = 0x28, + OpData41 = 0x29, + OpData42 = 0x2a, + OpData43 = 0x2b, + OpData44 = 0x2c, + OpData45 = 0x2d, + OpData46 = 0x2e, + OpData47 = 0x2f, + OpData48 = 0x30, + OpData49 = 0x31, + OpData50 = 0x32, + OpData51 = 0x33, + OpData52 = 0x34, + OpData53 = 0x35, + OpData54 = 0x36, + OpData55 = 0x37, + OpData56 = 0x38, + OpData57 = 0x39, + OpData58 = 0x3a, + OpData59 = 0x3b, + OpData60 = 0x3c, + OpData61 = 0x3d, + OpData62 = 0x3e, + OpData63 = 0x3f, + OpData64 = 0x40, + OpData65 = 0x41, + OpData66 = 0x42, + OpData67 = 0x43, + OpData68 = 0x44, + OpData69 = 0x45, + OpData70 = 0x46, + OpData71 = 0x47, + OpData72 = 0x48, + OpData73 = 0x49, + OpData74 = 0x4a, + OpData75 = 0x4b, + OpPushData1 = 0x4c, + OpPushData2 = 0x4d, + OpPushData4 = 0x4e, + Op1Negate = 0x4f, + /** + * Reserved + */ + OpReserved = 0x50, + Op1 = 0x51, + Op2 = 0x52, + Op3 = 0x53, + Op4 = 0x54, + Op5 = 0x55, + Op6 = 0x56, + Op7 = 0x57, + Op8 = 0x58, + Op9 = 0x59, + Op10 = 0x5a, + Op11 = 0x5b, + Op12 = 0x5c, + Op13 = 0x5d, + Op14 = 0x5e, + Op15 = 0x5f, + Op16 = 0x60, + OpNop = 0x61, + /** + * Reserved + */ + OpVer = 0x62, + OpIf = 0x63, + OpNotIf = 0x64, + /** + * Reserved + */ + OpVerIf = 0x65, + /** + * Reserved + */ + OpVerNotIf = 0x66, + OpElse = 0x67, + OpEndIf = 0x68, + OpVerify = 0x69, + OpReturn = 0x6a, + OpToAltStack = 0x6b, + OpFromAltStack = 0x6c, + Op2Drop = 0x6d, + Op2Dup = 0x6e, + Op3Dup = 0x6f, + Op2Over = 0x70, + Op2Rot = 0x71, + Op2Swap = 0x72, + OpIfDup = 0x73, + OpDepth = 0x74, + OpDrop = 0x75, + OpDup = 0x76, + OpNip = 0x77, + OpOver = 0x78, + OpPick = 0x79, + OpRoll = 0x7a, + OpRot = 0x7b, + OpSwap = 0x7c, + OpTuck = 0x7d, + /** + * Disabled + */ + OpCat = 0x7e, + /** + * Disabled + */ + OpSubStr = 0x7f, + /** + * Disabled + */ + OpLeft = 0x80, + /** + * Disabled + */ + OpRight = 0x81, + OpSize = 0x82, + /** + * Disabled + */ + OpInvert = 0x83, + /** + * Disabled + */ + OpAnd = 0x84, + /** + * Disabled + */ + OpOr = 0x85, + /** + * Disabled + */ + OpXor = 0x86, + OpEqual = 0x87, + OpEqualVerify = 0x88, + OpReserved1 = 0x89, + OpReserved2 = 0x8a, + Op1Add = 0x8b, + Op1Sub = 0x8c, + /** + * Disabled + */ + Op2Mul = 0x8d, + /** + * Disabled + */ + Op2Div = 0x8e, + OpNegate = 0x8f, + OpAbs = 0x90, + OpNot = 0x91, + Op0NotEqual = 0x92, + OpAdd = 0x93, + OpSub = 0x94, + /** + * Disabled + */ + OpMul = 0x95, + /** + * Disabled + */ + OpDiv = 0x96, + /** + * Disabled + */ + OpMod = 0x97, + /** + * Disabled + */ + OpLShift = 0x98, + /** + * Disabled + */ + OpRShift = 0x99, + OpBoolAnd = 0x9a, + OpBoolOr = 0x9b, + OpNumEqual = 0x9c, + OpNumEqualVerify = 0x9d, + OpNumNotEqual = 0x9e, + OpLessThan = 0x9f, + OpGreaterThan = 0xa0, + OpLessThanOrEqual = 0xa1, + OpGreaterThanOrEqual = 0xa2, + OpMin = 0xa3, + OpMax = 0xa4, + OpWithin = 0xa5, + OpUnknown166 = 0xa6, + OpUnknown167 = 0xa7, + OpSha256 = 0xa8, + OpCheckMultiSigECDSA = 0xa9, + OpBlake2b = 0xaa, + OpCheckSigECDSA = 0xab, + OpCheckSig = 0xac, + OpCheckSigVerify = 0xad, + OpCheckMultiSig = 0xae, + OpCheckMultiSigVerify = 0xaf, + OpCheckLockTimeVerify = 0xb0, + OpCheckSequenceVerify = 0xb1, + OpUnknown178 = 0xb2, + OpUnknown179 = 0xb3, + OpUnknown180 = 0xb4, + OpUnknown181 = 0xb5, + OpUnknown182 = 0xb6, + OpUnknown183 = 0xb7, + OpUnknown184 = 0xb8, + OpUnknown185 = 0xb9, + OpUnknown186 = 0xba, + OpUnknown187 = 0xbb, + OpUnknown188 = 0xbc, + OpUnknown189 = 0xbd, + OpUnknown190 = 0xbe, + OpUnknown191 = 0xbf, + OpUnknown192 = 0xc0, + OpUnknown193 = 0xc1, + OpUnknown194 = 0xc2, + OpUnknown195 = 0xc3, + OpUnknown196 = 0xc4, + OpUnknown197 = 0xc5, + OpUnknown198 = 0xc6, + OpUnknown199 = 0xc7, + OpUnknown200 = 0xc8, + OpUnknown201 = 0xc9, + OpUnknown202 = 0xca, + OpUnknown203 = 0xcb, + OpUnknown204 = 0xcc, + OpUnknown205 = 0xcd, + OpUnknown206 = 0xce, + OpUnknown207 = 0xcf, + OpUnknown208 = 0xd0, + OpUnknown209 = 0xd1, + OpUnknown210 = 0xd2, + OpUnknown211 = 0xd3, + OpUnknown212 = 0xd4, + OpUnknown213 = 0xd5, + OpUnknown214 = 0xd6, + OpUnknown215 = 0xd7, + OpUnknown216 = 0xd8, + OpUnknown217 = 0xd9, + OpUnknown218 = 0xda, + OpUnknown219 = 0xdb, + OpUnknown220 = 0xdc, + OpUnknown221 = 0xdd, + OpUnknown222 = 0xde, + OpUnknown223 = 0xdf, + OpUnknown224 = 0xe0, + OpUnknown225 = 0xe1, + OpUnknown226 = 0xe2, + OpUnknown227 = 0xe3, + OpUnknown228 = 0xe4, + OpUnknown229 = 0xe5, + OpUnknown230 = 0xe6, + OpUnknown231 = 0xe7, + OpUnknown232 = 0xe8, + OpUnknown233 = 0xe9, + OpUnknown234 = 0xea, + OpUnknown235 = 0xeb, + OpUnknown236 = 0xec, + OpUnknown237 = 0xed, + OpUnknown238 = 0xee, + OpUnknown239 = 0xef, + OpUnknown240 = 0xf0, + OpUnknown241 = 0xf1, + OpUnknown242 = 0xf2, + OpUnknown243 = 0xf3, + OpUnknown244 = 0xf4, + OpUnknown245 = 0xf5, + OpUnknown246 = 0xf6, + OpUnknown247 = 0xf7, + OpUnknown248 = 0xf8, + OpUnknown249 = 0xf9, + OpSmallInteger = 0xfa, + OpPubKeys = 0xfb, + OpUnknown252 = 0xfc, + OpPubKeyHash = 0xfd, + OpPubKey = 0xfe, + OpInvalidOpCode = 0xff, +} + +"#; + +/// +/// ScriptBuilder provides a facility for building custom scripts. It allows +/// you to push opcodes, ints, and data while respecting canonical encoding. In +/// general it does not ensure the script will execute correctly, however any +/// data pushes which would exceed the maximum allowed script engine limits and +/// are therefore guaranteed not to execute will not be pushed and will result in +/// the Script function returning an error. +/// +/// @see {@link Opcode} +/// @category Consensus +#[derive(Clone)] +#[wasm_bindgen(inspectable)] +pub struct ScriptBuilder { + script_builder: Rc>, +} + +impl ScriptBuilder { + #[inline] + pub fn inner(&self) -> Ref<'_, native::ScriptBuilder> { + self.script_builder.borrow() + } + + #[inline] + pub fn inner_mut(&self) -> RefMut<'_, native::ScriptBuilder> { + self.script_builder.borrow_mut() + } +} + +impl Default for ScriptBuilder { + fn default() -> Self { + Self { script_builder: Rc::new(RefCell::new(kaspa_txscript::script_builder::ScriptBuilder::new())) } + } +} + +#[wasm_bindgen] +impl ScriptBuilder { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self::default() + } + + #[wasm_bindgen(getter)] + pub fn data(&self) -> HexString { + self.script() + } + + /// Get script bytes represented by a hex string. + pub fn script(&self) -> HexString { + let inner = self.inner(); + HexString::from(inner.script()) + } + + /// Drains (empties) the script builder, returning the + /// script bytes represented by a hex string. + pub fn drain(&self) -> HexString { + let mut inner = self.inner_mut(); + HexString::from(inner.drain().as_slice()) + } + + #[wasm_bindgen(js_name = canonicalDataSize)] + pub fn canonical_data_size(data: BinaryT) -> Result { + let data = data.try_as_vec_u8()?; + let size = native::ScriptBuilder::canonical_data_size(&data) as u32; + Ok(size) + } + + /// Pushes the passed opcode to the end of the script. The script will not + /// be modified if pushing the opcode would cause the script to exceed the + /// maximum allowed script engine size. + #[wasm_bindgen(js_name = addOp)] + pub fn add_op(&self, op: u8) -> Result { + let mut inner = self.inner_mut(); + inner.add_op(op)?; + Ok(self.clone()) + } + + /// Adds the passed opcodes to the end of the script. + /// Supplied opcodes can be represented as a `Uint8Array` or a `HexString`. + #[wasm_bindgen(js_name = "addOps")] + pub fn add_ops(&self, opcodes: JsValue) -> Result { + let opcodes = opcodes.try_as_vec_u8()?; + self.inner_mut().add_ops(&opcodes)?; + Ok(self.clone()) + } + + /// AddData pushes the passed data to the end of the script. It automatically + /// 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`](kaspa_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`](kaspa_txscript::MAX_SCRIPTS_SIZE). + #[wasm_bindgen(js_name = addData)] + pub fn add_data(&self, data: BinaryT) -> Result { + let data = data.try_as_vec_u8()?; + + let mut inner = self.inner_mut(); + inner.add_data(&data)?; + Ok(self.clone()) + } + + #[wasm_bindgen(js_name = addI64)] + pub fn add_i64(&self, value: i64) -> Result { + let mut inner = self.inner_mut(); + inner.add_i64(value)?; + Ok(self.clone()) + } + + #[wasm_bindgen(js_name = addLockTime)] + pub fn add_lock_time(&self, lock_time: u64) -> Result { + let mut inner = self.inner_mut(); + inner.add_lock_time(lock_time)?; + Ok(self.clone()) + } + + #[wasm_bindgen(js_name = addSequence)] + pub fn add_sequence(&self, sequence: u64) -> Result { + let mut inner = self.inner_mut(); + inner.add_sequence(sequence)?; + Ok(self.clone()) + } +} diff --git a/consensus/client/src/serializable/mod.rs b/consensus/client/src/serializable/mod.rs new file mode 100644 index 000000000..5855e26df --- /dev/null +++ b/consensus/client/src/serializable/mod.rs @@ -0,0 +1,79 @@ +pub mod numeric; +pub mod string; + +use wasm_bindgen::prelude::*; +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &'static str = r#" + +/** + * Interface defines the structure of a serializable UTXO entry. + * + * @see {@link ISerializableTransactionInput}, {@link ISerializableTransaction} + * @category Wallet SDK + */ +export interface ISerializableUtxoEntry { + address?: Address; + amount: bigint; + scriptPublicKey: ScriptPublicKey; + blockDaaScore: bigint; + isCoinbase: boolean; +} + +/** + * Interface defines the structure of a serializable transaction input. + * + * @see {@link ISerializableTransaction} + * @category Wallet SDK + */ +export interface ISerializableTransactionInput { + transactionId : HexString; + index: number; + sequence: bigint; + sigOpCount: number; + signatureScript: HexString; + utxo: ISerializableUtxoEntry; +} + +/** + * Interface defines the structure of a serializable transaction output. + * + * @see {@link ISerializableTransaction} + * @category Wallet SDK + */ +export interface ISerializableTransactionOutput { + value: bigint; + scriptPublicKey: IScriptPublicKey; +} + +/** + * Interface defines the structure of a serializable transaction. + * + * Serializable transactions can be produced using + * {@link Transaction.serializeToJSON}, + * {@link Transaction.serializeToSafeJSON} and + * {@link Transaction.serializeToObject} + * functions for processing (signing) in external systems. + * + * Once the transaction is signed, it can be deserialized + * into {@link Transaction} using {@link Transaction.deserializeFromJSON} + * and {@link Transaction.deserializeFromSafeJSON} functions. + * + * @see {@link Transaction}, + * {@link ISerializableTransactionInput}, + * {@link ISerializableTransactionOutput}, + * {@link ISerializableUtxoEntry} + * + * @category Wallet SDK + */ +export interface ISerializableTransaction { + id? : HexString; + version: number; + inputs: ISerializableTransactionInput[]; + outputs: ISerializableTransactionOutput[]; + lockTime: bigint; + subnetworkId: HexString; + gas: bigint; + payload: HexString; +} + +"#; diff --git a/consensus/client/src/serializable/numeric.rs b/consensus/client/src/serializable/numeric.rs new file mode 100644 index 000000000..0c413fdcf --- /dev/null +++ b/consensus/client/src/serializable/numeric.rs @@ -0,0 +1,349 @@ +//! This module implements the primitives for external transaction signing. + +use crate::error::Error; +use crate::imports::*; +use crate::result::Result; +use crate::{ + Transaction, TransactionInput, TransactionInputInner, TransactionOutpoint, TransactionOutpointInner, TransactionOutput, UtxoEntry, + UtxoEntryId, UtxoEntryReference, +}; +use ahash::AHashMap; +use cctx::VerifiableTransaction; +use kaspa_addresses::Address; +use kaspa_consensus_core::subnets::SubnetworkId; +use workflow_wasm::serde::{from_value, to_value}; + +pub type SignedTransactionIndexType = u32; + +pub struct Options { + pub include_utxo: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableUtxoEntry { + pub address: Option
, + pub amount: u64, + pub script_public_key: ScriptPublicKey, + pub block_daa_score: u64, + pub is_coinbase: bool, +} + +impl AsRef for SerializableUtxoEntry { + fn as_ref(&self) -> &Self { + self + } +} + +impl From<&UtxoEntryReference> for SerializableUtxoEntry { + fn from(utxo: &UtxoEntryReference) -> Self { + let utxo = utxo.utxo.as_ref(); + Self { + address: utxo.address.clone(), + amount: utxo.amount, + script_public_key: utxo.script_public_key.clone(), + block_daa_score: utxo.block_daa_score, + is_coinbase: utxo.is_coinbase, + } + } +} + +impl From<&cctx::UtxoEntry> for SerializableUtxoEntry { + fn from(utxo: &cctx::UtxoEntry) -> Self { + Self { + address: None, + amount: utxo.amount, + script_public_key: utxo.script_public_key.clone(), + block_daa_score: utxo.block_daa_score, + is_coinbase: utxo.is_coinbase, + } + } +} + +impl TryFrom<&SerializableUtxoEntry> for cctx::UtxoEntry { + type Error = crate::error::Error; + fn try_from(utxo: &SerializableUtxoEntry) -> Result { + Ok(Self { + amount: utxo.amount, + script_public_key: utxo.script_public_key.clone(), + block_daa_score: utxo.block_daa_score, + is_coinbase: utxo.is_coinbase, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableTransactionInput { + pub transaction_id: TransactionId, + pub index: SignedTransactionIndexType, + pub sequence: u64, + pub sig_op_count: u8, + #[serde(with = "hex::serde")] + pub signature_script: Vec, + pub utxo: SerializableUtxoEntry, +} + +impl SerializableTransactionInput { + pub fn new(input: &cctx::TransactionInput, utxo: &cctx::UtxoEntry) -> Self { + let utxo = SerializableUtxoEntry::from(utxo); + + Self { + transaction_id: input.previous_outpoint.transaction_id, + index: input.previous_outpoint.index, + signature_script: input.signature_script.clone(), + sequence: input.sequence, + sig_op_count: input.sig_op_count, + utxo: utxo.clone(), + } + } +} + +impl TryFrom<&SerializableTransactionInput> for UtxoEntryReference { + type Error = Error; + fn try_from(input: &SerializableTransactionInput) -> Result { + let outpoint = TransactionOutpoint::new(input.transaction_id, input.index); + + let utxo = UtxoEntry { + outpoint, + address: input.utxo.address.clone(), + amount: input.utxo.amount, + script_public_key: input.utxo.script_public_key.clone(), + block_daa_score: input.utxo.block_daa_score, + is_coinbase: input.utxo.is_coinbase, + }; + + Ok(Self { utxo: Arc::new(utxo) }) + } +} + +impl TryFrom for cctx::TransactionInput { + type Error = Error; + fn try_from(signable_input: SerializableTransactionInput) -> Result { + Ok(Self { + previous_outpoint: cctx::TransactionOutpoint { + transaction_id: signable_input.transaction_id, + index: signable_input.index, + }, + signature_script: signable_input.signature_script, + sequence: signable_input.sequence, + sig_op_count: signable_input.sig_op_count, + }) + } +} + +impl TryFrom<&SerializableTransactionInput> for TransactionInput { + type Error = Error; + fn try_from(signable_input: &SerializableTransactionInput) -> Result { + let utxo = UtxoEntryReference::try_from(signable_input)?; + + let previous_outpoint = TransactionOutpoint::new(signable_input.transaction_id, signable_input.index); + let inner = TransactionInputInner { + previous_outpoint, + signature_script: signable_input.signature_script.clone(), + sequence: signable_input.sequence, + sig_op_count: signable_input.sig_op_count, + utxo: Some(utxo), + }; + + Ok(TransactionInput::new_with_inner(inner)) + } +} + +impl TryFrom<&TransactionInput> for SerializableTransactionInput { + type Error = Error; + fn try_from(input: &TransactionInput) -> Result { + let inner = input.inner(); + let utxo = inner.utxo.as_ref().ok_or(Error::MissingUtxoEntry)?; + let utxo = SerializableUtxoEntry::from(utxo); + Ok(Self { + transaction_id: inner.previous_outpoint.transaction_id(), + index: inner.previous_outpoint.index(), + signature_script: inner.signature_script.clone(), + sequence: inner.sequence, + sig_op_count: inner.sig_op_count, + utxo, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableTransactionOutput { + pub value: u64, + pub script_public_key: ScriptPublicKey, +} + +impl From for SerializableTransactionOutput { + fn from(output: cctx::TransactionOutput) -> Self { + Self { value: output.value, script_public_key: output.script_public_key } + } +} + +impl From<&cctx::TransactionOutput> for SerializableTransactionOutput { + fn from(output: &cctx::TransactionOutput) -> Self { + Self { value: output.value, script_public_key: output.script_public_key.clone() } + } +} + +impl TryFrom for cctx::TransactionOutput { + type Error = Error; + fn try_from(output: SerializableTransactionOutput) -> Result { + Ok(Self { value: output.value, script_public_key: output.script_public_key }) + } +} + +impl TryFrom<&SerializableTransactionOutput> for TransactionOutput { + type Error = Error; + fn try_from(output: &SerializableTransactionOutput) -> Result { + Ok(TransactionOutput::new(output.value, output.script_public_key.clone())) + } +} + +impl TryFrom<&TransactionOutput> for SerializableTransactionOutput { + type Error = Error; + fn try_from(output: &TransactionOutput) -> Result { + let inner = output.inner(); + Ok(Self { value: inner.value, script_public_key: inner.script_public_key.clone() }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableTransaction { + // pub version: u32, + pub id: TransactionId, + pub version: u16, + pub inputs: Vec, + pub outputs: Vec, + pub lock_time: u64, + pub gas: u64, + pub subnetwork_id: SubnetworkId, + #[serde(with = "hex::serde")] + pub payload: Vec, +} + +impl SerializableTransaction { + pub fn serialize_to_object(&self) -> Result { + Ok(to_value(self)?) + } + + pub fn deserialize_from_object(object: JsValue) -> Result { + Ok(from_value(object)?) + } + + pub fn serialize_to_json(&self) -> Result { + Ok(serde_json::to_string(self)?) + } + + pub fn deserialize_from_json(json: &str) -> Result { + Ok(serde_json::from_str(json)?) + } + + pub fn from_signable_transaction(tx: &cctx::SignableTransaction) -> Result { + let verifiable_tx = tx.as_verifiable(); + let mut inputs = vec![]; + let transaction = tx.as_ref(); + for index in 0..transaction.inputs.len() { + let (input, utxo) = verifiable_tx.populated_input(index); + let input = SerializableTransactionInput::new(input, utxo); + inputs.push(input); + } + + let outputs = transaction.outputs.clone(); + + Ok(Self { + // version: 2, + inputs, + outputs: outputs.into_iter().map(Into::into).collect(), + version: transaction.version, + lock_time: transaction.lock_time, + subnetwork_id: transaction.subnetwork_id.clone(), + gas: transaction.gas, + payload: transaction.payload.clone(), + id: transaction.id(), + }) + } + + pub fn from_client_transaction(transaction: &Transaction) -> Result { + let inner = transaction.inner(); + + let inputs = inner.inputs.iter().map(TryFrom::try_from).collect::>>()?; + let outputs = inner.outputs.iter().map(TryFrom::try_from).collect::>>()?; + + Ok(Self { + inputs, + outputs, + version: inner.version, + lock_time: inner.lock_time, + subnetwork_id: inner.subnetwork_id.clone(), + gas: inner.gas, + payload: inner.payload.clone(), + id: inner.id, + }) + } + + pub fn from_cctx_transaction(transaction: &cctx::Transaction, utxos: &AHashMap) -> Result { + let inputs = transaction + .inputs + .iter() + .map(|input| { + let id = TransactionOutpointInner::new(input.previous_outpoint.transaction_id, input.previous_outpoint.index); + let utxo = utxos.get(&id).ok_or(Error::MissingUtxoEntry)?; + let utxo = cctx::UtxoEntry::from(utxo); + let input = SerializableTransactionInput::new(input, &utxo); + Ok(input) + }) + .collect::>>()?; + + let outputs = transaction.outputs.iter().map(Into::into).collect::>(); + + Ok(Self { + id: transaction.id(), + version: transaction.version, + inputs, + outputs, + lock_time: transaction.lock_time, + subnetwork_id: transaction.subnetwork_id.clone(), + gas: transaction.gas, + payload: transaction.payload.clone(), + }) + } +} + +impl TryFrom for cctx::SignableTransaction { + type Error = Error; + fn try_from(serializable: SerializableTransaction) -> Result { + let mut entries = vec![]; + let mut inputs = vec![]; + for input in serializable.inputs { + entries.push(input.utxo.as_ref().try_into()?); + inputs.push(input.try_into()?); + } + + let outputs = serializable.outputs.into_iter().map(TryInto::try_into).collect::>>()?; + + let tx = cctx::Transaction::new( + serializable.version, + inputs, + outputs, + serializable.lock_time, + serializable.subnetwork_id, + serializable.gas, + serializable.payload, + ); + + Ok(Self::with_entries(tx, entries)) + } +} + +impl TryFrom for Transaction { + type Error = Error; + fn try_from(tx: SerializableTransaction) -> Result { + let id = tx.id; + let inputs: Vec = tx.inputs.iter().map(TryInto::try_into).collect::>>()?; + let outputs: Vec = tx.outputs.iter().map(TryInto::try_into).collect::>>()?; + + Transaction::new(Some(id), tx.version, inputs, outputs, tx.lock_time, tx.subnetwork_id, tx.gas, tx.payload) + } +} diff --git a/consensus/client/src/serializable/string.rs b/consensus/client/src/serializable/string.rs new file mode 100644 index 000000000..be3981b0e --- /dev/null +++ b/consensus/client/src/serializable/string.rs @@ -0,0 +1,346 @@ +//! This module implements the primitives for external transaction signing. + +use crate::imports::*; +use crate::result::Result; +use crate::{ + Transaction, TransactionInput, TransactionInputInner, TransactionOutpoint, TransactionOutpointInner, TransactionOutput, UtxoEntry, + UtxoEntryId, UtxoEntryReference, +}; +use ahash::AHashMap; +use cctx::VerifiableTransaction; +use kaspa_addresses::Address; +use kaspa_consensus_core::subnets::SubnetworkId; +use workflow_wasm::serde::{from_value, to_value}; + +pub type SignedTransactionIndexType = u32; + +pub struct Options { + pub include_utxo: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableUtxoEntry { + pub address: Option
, + pub amount: String, + pub script_public_key: ScriptPublicKey, + pub block_daa_score: String, + pub is_coinbase: bool, +} + +impl AsRef for SerializableUtxoEntry { + fn as_ref(&self) -> &Self { + self + } +} + +impl From<&UtxoEntryReference> for SerializableUtxoEntry { + fn from(utxo: &UtxoEntryReference) -> Self { + let utxo = utxo.utxo.as_ref(); + Self { + address: utxo.address.clone(), + amount: utxo.amount.to_string(), + script_public_key: utxo.script_public_key.clone(), + block_daa_score: utxo.block_daa_score.to_string(), + is_coinbase: utxo.is_coinbase, + } + } +} + +impl From<&cctx::UtxoEntry> for SerializableUtxoEntry { + fn from(utxo: &cctx::UtxoEntry) -> Self { + Self { + address: None, + amount: utxo.amount.to_string(), + script_public_key: utxo.script_public_key.clone(), + block_daa_score: utxo.block_daa_score.to_string(), + is_coinbase: utxo.is_coinbase, + } + } +} + +impl TryFrom<&SerializableUtxoEntry> for cctx::UtxoEntry { + type Error = crate::error::Error; + fn try_from(utxo: &SerializableUtxoEntry) -> Result { + Ok(Self { + amount: utxo.amount.parse()?, + script_public_key: utxo.script_public_key.clone(), + block_daa_score: utxo.block_daa_score.parse()?, + is_coinbase: utxo.is_coinbase, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableTransactionInput { + pub transaction_id: TransactionId, + pub index: SignedTransactionIndexType, + pub sequence: String, + pub sig_op_count: u8, + #[serde(with = "hex::serde")] + pub signature_script: Vec, + pub utxo: SerializableUtxoEntry, +} + +impl SerializableTransactionInput { + pub fn new(input: &cctx::TransactionInput, utxo: &cctx::UtxoEntry) -> Self { + let utxo = SerializableUtxoEntry::from(utxo); + + Self { + transaction_id: input.previous_outpoint.transaction_id, + index: input.previous_outpoint.index, + signature_script: input.signature_script.clone(), + sequence: input.sequence.to_string(), + sig_op_count: input.sig_op_count, + utxo: utxo.clone(), + } + } +} + +impl TryFrom<&SerializableTransactionInput> for UtxoEntryReference { + type Error = Error; + fn try_from(input: &SerializableTransactionInput) -> Result { + let outpoint = TransactionOutpoint::new(input.transaction_id, input.index); + + let utxo = UtxoEntry { + outpoint, + address: input.utxo.address.clone(), + amount: input.utxo.amount.parse()?, + script_public_key: input.utxo.script_public_key.clone(), + block_daa_score: input.utxo.block_daa_score.parse()?, + is_coinbase: input.utxo.is_coinbase, + }; + + Ok(Self { utxo: Arc::new(utxo) }) + } +} + +impl TryFrom for cctx::TransactionInput { + type Error = Error; + fn try_from(signable_input: SerializableTransactionInput) -> Result { + Ok(Self { + previous_outpoint: cctx::TransactionOutpoint { + transaction_id: signable_input.transaction_id, + index: signable_input.index, + }, + signature_script: signable_input.signature_script, + sequence: signable_input.sequence.parse()?, + sig_op_count: signable_input.sig_op_count, + }) + } +} + +impl TryFrom<&SerializableTransactionInput> for TransactionInput { + type Error = Error; + fn try_from(serializable_input: &SerializableTransactionInput) -> Result { + let utxo = UtxoEntryReference::try_from(serializable_input)?; + + let previous_outpoint = TransactionOutpoint::new(serializable_input.transaction_id, serializable_input.index); + let inner = TransactionInputInner { + previous_outpoint, + signature_script: serializable_input.signature_script.clone(), + sequence: serializable_input.sequence.parse()?, + sig_op_count: serializable_input.sig_op_count, + utxo: Some(utxo), + }; + + Ok(TransactionInput::new_with_inner(inner)) + } +} + +impl TryFrom<&TransactionInput> for SerializableTransactionInput { + type Error = Error; + fn try_from(input: &TransactionInput) -> Result { + let inner = input.inner(); + let utxo = inner.utxo.as_ref().ok_or(Error::MissingUtxoEntry)?; + let utxo = SerializableUtxoEntry::from(utxo); + Ok(Self { + transaction_id: inner.previous_outpoint.transaction_id(), + index: inner.previous_outpoint.index(), + signature_script: inner.signature_script.clone(), + sequence: inner.sequence.to_string(), + sig_op_count: inner.sig_op_count, + utxo, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableTransactionOutput { + pub value: String, + pub script_public_key: ScriptPublicKey, +} + +impl From for SerializableTransactionOutput { + fn from(output: cctx::TransactionOutput) -> Self { + Self { value: output.value.to_string(), script_public_key: output.script_public_key } + } +} + +impl From<&cctx::TransactionOutput> for SerializableTransactionOutput { + fn from(output: &cctx::TransactionOutput) -> Self { + Self { value: output.value.to_string(), script_public_key: output.script_public_key.clone() } + } +} + +impl TryFrom for cctx::TransactionOutput { + type Error = Error; + fn try_from(output: SerializableTransactionOutput) -> Result { + Ok(Self { value: output.value.parse()?, script_public_key: output.script_public_key }) + } +} + +impl TryFrom<&SerializableTransactionOutput> for TransactionOutput { + type Error = Error; + fn try_from(output: &SerializableTransactionOutput) -> Result { + Ok(TransactionOutput::new(output.value.parse()?, output.script_public_key.clone())) + } +} + +impl TryFrom<&TransactionOutput> for SerializableTransactionOutput { + type Error = Error; + fn try_from(output: &TransactionOutput) -> Result { + let inner = output.inner(); + Ok(Self { value: inner.value.to_string(), script_public_key: inner.script_public_key.clone() }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SerializableTransaction { + pub id: TransactionId, + pub version: u16, + pub inputs: Vec, + pub outputs: Vec, + pub subnetwork_id: SubnetworkId, + pub lock_time: String, + pub gas: String, + #[serde(with = "hex::serde")] + pub payload: Vec, +} + +impl SerializableTransaction { + pub fn serialize_to_object(&self) -> Result { + Ok(to_value(self)?) + } + + pub fn deserialize_from_object(object: JsValue) -> Result { + Ok(from_value(object)?) + } + + pub fn serialize_to_json(&self) -> Result { + Ok(serde_json::to_string(self)?) + } + + pub fn deserialize_from_json(json: &str) -> Result { + Ok(serde_json::from_str(json)?) + } + + pub fn from_signable_transaction(tx: &cctx::SignableTransaction) -> Result { + let verifiable_tx = tx.as_verifiable(); + let mut inputs = vec![]; + let transaction = tx.as_ref(); + for index in 0..transaction.inputs.len() { + let (input, utxo) = verifiable_tx.populated_input(index); + let input = SerializableTransactionInput::new(input, utxo); + inputs.push(input); + } + + let outputs = transaction.outputs.clone(); + + Ok(Self { + id: transaction.id(), + inputs, + version: transaction.version, + outputs: outputs.into_iter().map(Into::into).collect(), + lock_time: transaction.lock_time.to_string(), + subnetwork_id: transaction.subnetwork_id.clone(), + gas: transaction.gas.to_string(), + payload: transaction.payload.clone(), + }) + } + + pub fn from_client_transaction(transaction: &Transaction) -> Result { + let inner = transaction.inner(); + + let inputs = inner.inputs.iter().map(TryFrom::try_from).collect::>>()?; + let outputs = inner.outputs.iter().map(TryFrom::try_from).collect::>>()?; + + Ok(Self { + inputs, + outputs, + version: inner.version, + lock_time: inner.lock_time.to_string(), + subnetwork_id: inner.subnetwork_id.clone(), + gas: inner.gas.to_string(), + payload: inner.payload.clone(), + id: inner.id, + }) + } + + pub fn from_cctx_transaction(transaction: &cctx::Transaction, utxos: &AHashMap) -> Result { + let inputs = transaction + .inputs + .iter() + .map(|input| { + let id = TransactionOutpointInner::new(input.previous_outpoint.transaction_id, input.previous_outpoint.index); + let utxo = utxos.get(&id).ok_or(Error::MissingUtxoEntry)?; + let utxo = cctx::UtxoEntry::from(utxo); + let input = SerializableTransactionInput::new(input, &utxo); + Ok(input) + }) + .collect::>>()?; + + let outputs = transaction.outputs.iter().map(Into::into).collect::>(); + + Ok(Self { + id: transaction.id(), + version: transaction.version, + inputs, + outputs, + lock_time: transaction.lock_time.to_string(), + subnetwork_id: transaction.subnetwork_id.clone(), + gas: transaction.gas.to_string(), + payload: transaction.payload.clone(), + }) + } +} + +impl TryFrom for cctx::SignableTransaction { + type Error = Error; + fn try_from(signable: SerializableTransaction) -> Result { + let mut entries = vec![]; + let mut inputs = vec![]; + for input in signable.inputs { + entries.push(input.utxo.as_ref().try_into()?); + inputs.push(input.try_into()?); + } + + let outputs = signable.outputs.into_iter().map(TryInto::try_into).collect::>>()?; + + let tx = cctx::Transaction::new( + signable.version, + inputs, + outputs, + signable.lock_time.parse()?, + signable.subnetwork_id, + signable.gas.parse()?, + signable.payload, + ); + + Ok(Self::with_entries(tx, entries)) + } +} + +impl TryFrom for crate::Transaction { + type Error = Error; + fn try_from(tx: SerializableTransaction) -> Result { + let id = tx.id; + let inputs: Vec = tx.inputs.iter().map(TryInto::try_into).collect::>>()?; + let outputs: Vec = tx.outputs.iter().map(TryInto::try_into).collect::>>()?; + + Transaction::new(Some(id), tx.version, inputs, outputs, tx.lock_time.parse()?, tx.subnetwork_id, tx.gas.parse()?, tx.payload) + } +} diff --git a/consensus/client/src/sign.rs b/consensus/client/src/sign.rs new file mode 100644 index 000000000..fdab66a60 --- /dev/null +++ b/consensus/client/src/sign.rs @@ -0,0 +1,73 @@ +use crate::transaction::Transaction; +use core::iter::once; +use itertools::Itertools; +use kaspa_consensus_core::{ + hashing::{ + sighash::{calc_schnorr_signature_hash, SigHashReusedValues}, + sighash_type::SIG_HASH_ALL, + }, + tx::PopulatedTransaction, + //sign::Signed, +}; +use std::collections::BTreeMap; + +/// 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(Transaction), + Partially(Transaction), +} + +impl Signed { + /// Returns the transaction regardless of whether it is fully or partially signed + pub fn unwrap(self) -> Transaction { + match self { + Signed::Fully(tx) => tx, + Signed::Partially(tx) => tx, + } + } +} + +/// TODO (aspect) - merge this with `v1` fn above or refactor wallet core to use the script engine. +/// Sign a transaction using schnorr +#[allow(clippy::result_large_err)] +pub fn sign_with_multiple_v3(tx: Transaction, privkeys: &[[u8; 32]]) -> crate::result::Result { + let mut map = BTreeMap::new(); + for privkey in privkeys { + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey).unwrap(); + let schnorr_public_key = schnorr_key.public_key().x_only_public_key().0; + let script_pub_key_script = once(0x20).chain(schnorr_public_key.serialize().into_iter()).chain(once(0xac)).collect_vec(); + map.insert(script_pub_key_script, schnorr_key); + } + + let mut reused_values = SigHashReusedValues::new(); + let mut additional_signatures_required = false; + { + let input_len = tx.inner().inputs.len(); + let (cctx, utxos) = tx.tx_and_utxos(); + let populated_transaction = PopulatedTransaction::new(&cctx, utxos); + for i in 0..input_len { + let script_pub_key = match tx.inner().inputs[i].script_public_key() { + Some(script) => script, + None => { + return Err(crate::imports::Error::Custom("expected to be called only following full UTXO population".to_string())) + } + }; + let script = script_pub_key.script(); + if let Some(schnorr_key) = map.get(script) { + let sig_hash = calc_schnorr_signature_hash(&populated_transaction, i, SIG_HASH_ALL, &mut reused_values); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); + 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) + tx.set_signature_script(i, std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect())?; + } else { + additional_signatures_required = true; + } + } + } + if additional_signatures_required { + Ok(Signed::Partially(tx)) + } else { + Ok(Signed::Fully(tx)) + } +} diff --git a/consensus/client/src/signing.rs b/consensus/client/src/signing.rs new file mode 100644 index 000000000..ef993d011 --- /dev/null +++ b/consensus/client/src/signing.rs @@ -0,0 +1,216 @@ +// +// This module is currently disabled, kept for potential future re-integration. +// + +use crate::imports::*; +use crate::result::Result; +use kaspa_consensus_core::hashing::sighash::SigHashReusedValues; +use kaspa_consensus_core::hashing::*; +use kaspa_consensus_core::hashing::sighash_type::{SigHashType, SIG_HASH_ALL}; +use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; +use kaspa_consensus_core::tx::{TransactionOutpoint, TransactionOutput, VerifiableTransaction}; +// use kaspa_hashes::{Hash, Hasher, HasherBase, TransactionSigningHash}; +use crate::transaction::{Transaction,ITransaction}; +use crate::input::{ITransactionInput, TransactionInput}; +use crate::utxo::{IUtxoEntry,UtxoEntryReference}; +use kaspa_hashes::{Hash, Hasher, HasherBase, TransactionSigningHash, TransactionSigningHashECDSA, ZERO_HASH}; +use kaspa_consensus_core::hashing::HasherExtensions; +use kaspa_consensus_core::hashing::sighash::*; + +#[derive(Default)] +#[wasm_bindgen] +pub struct SigHashCache { + #[wasm_bindgen(js_name = "previousOutputsHash")] + pub previous_outputs_hash: Option, + #[wasm_bindgen(js_name = "sequencesHash")] + pub sequences_hash: Option, + #[wasm_bindgen(js_name = "sigOpCountsHash")] + pub sig_op_counts_hash: Option, + #[wasm_bindgen(js_name = "outputsHash")] + pub outputs_hash: Option, +} + +#[wasm_bindgen] +impl SigHashCache { + #[wasm_bindgen(constructor)] + pub fn ctor() -> SigHashCache { + // let tx = Transaction::try_cast_from(tx)?.as_ref(); + Self::default() + } + + pub fn previous_outputs_hash(&mut self, tx: &Transaction, hash_type: SigHashType) -> Hash { + if hash_type.is_sighash_anyone_can_pay() { + return ZERO_HASH; + } + + if let Some(previous_outputs_hash) = self.previous_outputs_hash { + previous_outputs_hash + } else { + let mut hasher = TransactionSigningHash::new(); + for input in tx.inner().inputs.iter() { + hasher.update(input.previous_outpoint.transaction_id.as_bytes()); + hasher.write_u32(input.previous_outpoint.index); + } + let previous_outputs_hash = hasher.finalize(); + self.previous_outputs_hash = Some(previous_outputs_hash); + previous_outputs_hash + } + } + + pub fn sequences_hash(&mut self, tx: &Transaction, hash_type: SigHashType) -> Hash { + if hash_type.is_sighash_single() || hash_type.is_sighash_anyone_can_pay() || hash_type.is_sighash_none() { + return ZERO_HASH; + } + + if let Some(sequences_hash) = self.sequences_hash { + sequences_hash + } else { + let mut hasher = TransactionSigningHash::new(); + for input in tx.inner().inputs.iter() { + hasher.write_u64(input.sequence); + } + let sequence_hash = hasher.finalize(); + self.sequences_hash = Some(sequence_hash); + sequence_hash + } + } + + pub fn sig_op_counts_hash(&mut self, tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues) -> Hash { + if hash_type.is_sighash_anyone_can_pay() { + return ZERO_HASH; + } + + if let Some(sig_op_counts_hash) = self.sig_op_counts_hash { + sig_op_counts_hash + } else { + let mut hasher = TransactionSigningHash::new(); + for input in tx.inputs.iter() { + hasher.write_u8(input.sig_op_count); + } + let sig_op_counts_hash = hasher.finalize(); + self.sig_op_counts_hash = Some(sig_op_counts_hash); + sig_op_counts_hash + } + } + + // pub fn payload_hash(tx: &Transaction) -> Hash { + // if tx.subnetwork_id == SUBNETWORK_ID_NATIVE { + // return ZERO_HASH; + // } + + // // TODO: Right now this branch will never be executed, since payload is disabled + // // for all non coinbase transactions. Once payload is enabled, the payload hash + // // should be cached to make it cost O(1) instead of O(tx.inputs.len()). + // let mut hasher = TransactionSigningHash::new(); + // hasher.write_var_bytes(&tx.payload); + // hasher.finalize() + // } + + pub fn outputs_hash(&mut self, tx: &Transaction, hash_type: SigHashType, input_index: usize) -> Hash { + if hash_type.is_sighash_none() { + return ZERO_HASH; + } + + if hash_type.is_sighash_single() { + // If the relevant output exists - return its hash, otherwise return zero-hash + if input_index >= tx.outputs.len() { + return ZERO_HASH; + } + + let mut hasher = TransactionSigningHash::new(); + hash_output(&mut hasher, &tx.outputs[input_index]); + return hasher.finalize(); + } + + // Otherwise, return hash of all outputs. Re-use hash if available. + if let Some(outputs_hash) = reused_values.outputs_hash { + outputs_hash + } else { + let mut hasher = TransactionSigningHash::new(); + for output in tx.outputs.iter() { + hash_output(&mut hasher, output); + } + let outputs_hash = hasher.finalize(); + reused_values.outputs_hash = Some(outputs_hash); + outputs_hash + } + } + + pub fn hash_outpoint(hasher: &mut impl Hasher, outpoint: TransactionOutpoint) { + hasher.update(outpoint.transaction_id); + hasher.write_u32(outpoint.index); + } + + pub fn hash_output(hasher: &mut impl Hasher, output: &TransactionOutput) { + hasher.write_u64(output.value); + hash_script_public_key(hasher, &output.script_public_key); + } + + pub fn hash_script_public_key(hasher: &mut impl Hasher, script_public_key: &ScriptPublicKey) { + hasher.write_u16(script_public_key.version()); + hasher.write_var_bytes(script_public_key.script()); + } +} + +pub fn calc_schnorr_signature_hash( + tx : ITransaction, + input : ITransactionInput, + // utxo : IUtxoEntry, +// verifiable_tx: &impl VerifiableTransaction, + input_index: usize, + // hash_type: SigHashType, + // reused_values: &mut SigHashReusedValues, +) -> Result { + // let tx = Transaction::try_cast_from(tx.as_ref())?; + let tx = Transaction::try_cast_from(tx)?; + + let input = TransactionInput::try_cast_from(input)?; + // let input = TransactionInput::try_cast_from(input.as_ref())?; + + // let utxo = input. + + let utxo = input.as_ref().utxo().ok_or(Error::MissingUtxoEntry)?; + + // let utxo = UtxoEntryReference::try_cast_from(utxo.as_ref())?; + + let tx = cctx::Transaction::from(tx.as_ref()); + let input = cctx::TransactionInput::from(input.as_ref()); + let utxo = cctx::UtxoEntry::from(utxo.as_ref()); + + let hash_type = SIG_HASH_ALL; + let mut reused_values = SigHashReusedValues::new(); + + // let input = verifiable_tx.populated_input(input_index); + // let tx = verifiable_tx.tx(); + let mut hasher = TransactionSigningHash::new(); + hasher + .write_u16(tx.version) + .update(previous_outputs_hash(&tx, hash_type, &mut reused_values)) + .update(sequences_hash(&tx, hash_type, &mut reused_values)) + .update(sig_op_counts_hash(&tx, hash_type, &mut reused_values)); + hash_outpoint(&mut hasher, input.previous_outpoint); + hash_script_public_key(&mut hasher, &utxo.script_public_key); + hasher + .write_u64(utxo.amount) + .write_u64(input.sequence) + .write_u8(input.sig_op_count) + .update(outputs_hash(&tx, hash_type, &mut reused_values, input_index)) + .write_u64(tx.lock_time) + .update(&tx.subnetwork_id) + .write_u64(tx.gas) + .update(payload_hash(&tx)) + .write_u8(hash_type.to_u8()); + Ok(hasher.finalize()) +} + +pub fn calc_ecdsa_signature_hash( + tx: &impl VerifiableTransaction, + input_index: usize, + hash_type: SigHashType, + // reused_values: &mut SigHashReusedValues, +) -> Result { + let hash = calc_schnorr_signature_hash(tx, input_index, hash_type, reused_values)?; + let mut hasher = TransactionSigningHashECDSA::new(); + hasher.update(hash); + hasher.finalize() +} diff --git a/consensus/client/src/transaction.rs b/consensus/client/src/transaction.rs new file mode 100644 index 000000000..329349714 --- /dev/null +++ b/consensus/client/src/transaction.rs @@ -0,0 +1,438 @@ +#![allow(non_snake_case)] + +use crate::imports::*; +use crate::input::TransactionInput; +use crate::outpoint::TransactionOutpoint; +use crate::output::TransactionOutput; +use crate::result::Result; +use crate::serializable::{numeric, string}; +use crate::utxo::{UtxoEntryId, UtxoEntryReference}; +use ahash::AHashMap; +use kaspa_consensus_core::network::NetworkType; +use kaspa_consensus_core::network::NetworkTypeT; +use kaspa_consensus_core::subnets::{self, SubnetworkId}; +use kaspa_consensus_core::tx::UtxoEntry; +use kaspa_txscript::extract_script_pub_key_address; +use kaspa_utils::hex::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TRANSACTION: &'static str = r#" +/** + * Interface defining the structure of a transaction. + * + * @category Consensus + */ +export interface ITransaction { + version: number; + inputs: ITransactionInput[]; + outputs: ITransactionOutput[]; + lockTime: bigint; + subnetworkId: HexString; + gas: bigint; + payload: HexString; + + /** Optional verbose data provided by RPC */ + verboseData?: ITransactionVerboseData; +} + +/** + * Optional transaction verbose data. + * + * @category Node RPC + */ +export interface ITransactionVerboseData { + transactionId : HexString; + hash : HexString; + mass : bigint; + blockHash : HexString; + blockTime : bigint; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ITransaction")] + pub type ITransaction; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionInner { + pub version: u16, + pub inputs: Vec, + pub outputs: Vec, + pub lock_time: u64, + pub subnetwork_id: SubnetworkId, + pub gas: u64, + pub payload: Vec, + + // A field that is used to cache the transaction ID. + // Always use the corresponding self.id() instead of accessing this field directly + pub id: TransactionId, +} + +/// Represents a Kaspa transaction. +/// This is an artificial construct that includes additional +/// transaction-related data such as additional data from UTXOs +/// used by transaction inputs. +/// @category Consensus +#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] +#[wasm_bindgen(inspectable)] +pub struct Transaction { + inner: Arc>, +} + +impl Transaction { + pub fn new( + id: Option, + version: u16, + inputs: Vec, + outputs: Vec, + lock_time: u64, + subnetwork_id: SubnetworkId, + gas: u64, + payload: Vec, + ) -> Result { + let finalize = id.is_none(); + let tx = Self { + inner: Arc::new(Mutex::new(TransactionInner { + id: id.unwrap_or_default(), + version, + inputs, + outputs, + lock_time, + subnetwork_id, + gas, + payload, + })), + }; + if finalize { + tx.finalize()?; + } + Ok(tx) + } + + pub fn new_with_inner(inner: TransactionInner) -> Self { + Self { inner: Arc::new(Mutex::new(inner)) } + } + + pub fn inner(&self) -> MutexGuard<'_, TransactionInner> { + self.inner.lock().unwrap() + } + + pub fn id(&self) -> TransactionId { + self.inner().id + } +} + +#[wasm_bindgen] +impl Transaction { + /// Determines whether or not a transaction is a coinbase transaction. A coinbase + /// transaction is a special transaction created by miners that distributes fees and block subsidy + /// to the previous blocks' miners, and specifies the script_pub_key that will be used to pay the current + /// miner in future blocks. + pub fn is_coinbase(&self) -> bool { + self.inner().subnetwork_id == subnets::SUBNETWORK_ID_COINBASE + } + + /// Recompute and finalize the tx id based on updated tx fields + pub fn finalize(&self) -> Result { + let tx: cctx::Transaction = self.into(); + self.inner().id = tx.id(); + Ok(self.inner().id) + } + + /// Returns the transaction ID + #[wasm_bindgen(getter, js_name = id)] + pub fn id_string(&self) -> String { + self.inner().id.to_string() + } + + #[wasm_bindgen(constructor)] + pub fn constructor(js_value: &ITransaction) -> std::result::Result { + Ok(js_value.try_into_owned()?) + } + + #[wasm_bindgen(getter = inputs)] + pub fn get_inputs_as_js_array(&self) -> Array { + let inputs = self.inner.lock().unwrap().inputs.clone().into_iter().map(JsValue::from); + Array::from_iter(inputs) + } + + /// Returns a list of unique addresses used by transaction inputs. + /// This method can be used to determine addresses used by transaction inputs + /// in order to select private keys needed for transaction signing. + pub fn addresses(&self, network_type: &NetworkTypeT) -> Result { + let mut list = std::collections::HashSet::new(); + for input in &self.inner.lock().unwrap().inputs { + if let Some(utxo) = input.get_utxo() { + if let Some(address) = &utxo.utxo.address { + list.insert(address.clone()); + } else if let Ok(address) = + extract_script_pub_key_address(&utxo.utxo.script_public_key, NetworkType::try_from(network_type)?.into()) + { + list.insert(address); + } + } + } + Ok(Array::from_iter(list.into_iter().map(JsValue::from)).unchecked_into()) + } + + #[wasm_bindgen(setter = inputs)] + pub fn set_inputs_from_js_array(&mut self, js_value: &JsValue) { + let inputs = Array::from(js_value) + .iter() + .map(|js_value| { + TransactionInput::try_owned_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction input: {err}")) + }) + .collect::>(); + self.inner().inputs = inputs; + } + + #[wasm_bindgen(getter = outputs)] + pub fn get_outputs_as_js_array(&self) -> Array { + let outputs = self.inner.lock().unwrap().outputs.clone().into_iter().map(JsValue::from); + Array::from_iter(outputs) + } + + #[wasm_bindgen(setter = outputs)] + pub fn set_outputs_from_js_array(&mut self, js_value: &JsValue) { + let outputs = Array::from(js_value) + .iter() + .map(|js_value| TransactionOutput::try_from(&js_value).unwrap_or_else(|err| panic!("invalid transaction output: {err}"))) + .collect::>(); + self.inner().outputs = outputs; + } + + #[wasm_bindgen(getter, js_name = version)] + pub fn get_version(&self) -> u16 { + self.inner().version + } + + #[wasm_bindgen(setter, js_name = version)] + pub fn set_version(&self, v: u16) { + self.inner().version = v; + } + + #[wasm_bindgen(getter, js_name = lock_time)] + pub fn get_lock_time(&self) -> u64 { + self.inner().lock_time + } + + #[wasm_bindgen(setter, js_name = lock_time)] + pub fn set_lock_time(&self, v: u64) { + self.inner().lock_time = v; + } + + #[wasm_bindgen(getter, js_name = gas)] + pub fn get_gas(&self) -> u64 { + self.inner().gas + } + + #[wasm_bindgen(setter, js_name = gas)] + pub fn set_gas(&self, v: u64) { + self.inner().gas = v; + } + + #[wasm_bindgen(getter = subnetworkId)] + pub fn get_subnetwork_id_as_hex(&self) -> String { + self.inner().subnetwork_id.to_hex() + } + + #[wasm_bindgen(setter = subnetworkId)] + pub fn set_subnetwork_id_from_js_value(&mut self, js_value: JsValue) { + let subnetwork_id = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("subnetwork id error: {err}")); + self.inner().subnetwork_id = subnetwork_id.as_slice().try_into().unwrap_or_else(|err| panic!("subnetwork id error: {err}")); + } + + #[wasm_bindgen(getter = payload)] + pub fn get_payload_as_hex_string(&self) -> String { + self.inner().payload.to_hex() + } + + #[wasm_bindgen(setter = payload)] + pub fn set_payload_from_js_value(&mut self, js_value: JsValue) { + self.inner.lock().unwrap().payload = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("payload value error: {err}")); + } +} + +impl TryCastFromJs for Transaction { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> std::result::Result, Self::Error> { + Self::resolve_cast(&value, || { + if let Some(object) = Object::try_from(value.as_ref()) { + if let Some(tx) = object.try_get_value("tx")? { + Transaction::try_cast_from(&tx) + } else { + let id = object.try_get_cast::("id")?.map(|id| id.into_owned()); + let version = object.get_u16("version")?; + let lock_time = object.get_u64("lockTime")?; + let gas = object.get_u64("gas")?; + let payload = object.get_vec_u8("payload")?; + let subnetwork_id = object.get_vec_u8("subnetworkId")?; + if subnetwork_id.len() != subnets::SUBNETWORK_ID_SIZE { + return Err(Error::Custom("subnetworkId must be 20 bytes long".into())); + } + let subnetwork_id: SubnetworkId = subnetwork_id + .as_slice() + .try_into() + .map_err(|err| Error::Custom(format!("`subnetworkId` property error: `{err}`")))?; + let inputs = object + .get_vec("inputs")? + .iter() + .map(TryCastFromJs::try_owned_from) + .collect::, Error>>()?; + let outputs: Vec = object + .get_vec("outputs")? + .iter() + .map(|jsv| jsv.try_into()) + .collect::, Error>>()?; + Transaction::new(id, version, inputs, outputs, lock_time, subnetwork_id, gas, payload).map(Into::into) + } + } else { + Err("Transaction must be an object".into()) + } + }) + // Transaction::try_from(value) + } +} + +impl From for Transaction { + fn from(tx: cctx::Transaction) -> Self { + let id = tx.id(); + let inputs: Vec = tx.inputs.into_iter().map(|input| input.into()).collect::>(); + let outputs: Vec = tx.outputs.into_iter().map(|output| output.into()).collect::>(); + Self::new_with_inner(TransactionInner { + version: tx.version, + inputs, + outputs, + lock_time: tx.lock_time, + gas: tx.gas, + payload: tx.payload, + subnetwork_id: tx.subnetwork_id, + id, + }) + } +} + +impl From<&Transaction> for cctx::Transaction { + fn from(tx: &Transaction) -> Self { + let inner = tx.inner(); + let inputs: Vec = + inner.inputs.clone().into_iter().map(|input| input.as_ref().into()).collect::>(); + let outputs: Vec = + inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::>(); + cctx::Transaction::new( + inner.version, + inputs, + outputs, + inner.lock_time, + inner.subnetwork_id.clone(), + inner.gas, + inner.payload.clone(), + ) + } +} + +impl Transaction { + pub fn from_cctx_transaction(tx: &cctx::Transaction, utxos: &AHashMap) -> Self { + let inputs: Vec = tx + .inputs + .iter() + .map(|input| { + let previous_outpoint: TransactionOutpoint = input.previous_outpoint.into(); + let utxo = utxos.get(previous_outpoint.id()).cloned(); + TransactionInput::new(previous_outpoint, input.signature_script.clone(), input.sequence, input.sig_op_count, utxo) + }) + .collect::>(); + let outputs: Vec = tx.outputs.iter().map(|output| output.into()).collect::>(); + + Self::new_with_inner(TransactionInner { + id: tx.id(), + version: tx.version, + inputs, + outputs, + lock_time: tx.lock_time, + gas: tx.gas, + payload: tx.payload.clone(), + subnetwork_id: tx.subnetwork_id.clone(), + }) + } + + pub fn tx_and_utxos(&self) -> (cctx::Transaction, Vec) { + let mut utxos = vec![]; + let inner = self.inner(); + let inputs: Vec = inner + .inputs + .clone() + .into_iter() + .map(|input| { + utxos.push((&input.get_utxo().unwrap().entry()).into()); + input.as_ref().into() + }) + .collect::>(); + let outputs: Vec = + inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::>(); + let tx = cctx::Transaction::new( + inner.version, + inputs, + outputs, + inner.lock_time, + inner.subnetwork_id.clone(), + inner.gas, + inner.payload.clone(), + ); + + (tx, utxos) + } + + pub fn set_signature_script(&self, input_index: usize, signature_script: Vec) -> Result<()> { + if self.inner().inputs.len() <= input_index { + return Err(Error::Custom("Input index is invalid".to_string())); + } + self.inner().inputs[input_index].set_signature_script(signature_script); + Ok(()) + } +} + +#[wasm_bindgen] +impl Transaction { + /// Serializes the transaction to a pure JavaScript Object. + /// The schema of the JavaScript object is defined by {@link ISerializableTransaction}. + /// @see {@link ISerializableTransaction} + #[wasm_bindgen(js_name = "serializeToObject")] + pub fn serialize_to_object(&self) -> Result { + Ok(numeric::SerializableTransaction::from_client_transaction(self)?.serialize_to_object()?.into()) + } + + /// Serializes the transaction to a JSON string. + /// The schema of the JSON is defined by {@link ISerializableTransaction}. + #[wasm_bindgen(js_name = "serializeToJSON")] + pub fn serialize_to_json(&self) -> Result { + numeric::SerializableTransaction::from_client_transaction(self)?.serialize_to_json() + } + + /// Serializes the transaction to a "Safe" JSON schema where it converts all `bigint` values to `string` to avoid potential client-side precision loss. + #[wasm_bindgen(js_name = "serializeToSafeJSON")] + pub fn serialize_to_json_safe(&self) -> Result { + string::SerializableTransaction::from_client_transaction(self)?.serialize_to_json() + } + + /// Deserialize the {@link Transaction} Object from a pure JavaScript Object. + #[wasm_bindgen(js_name = "deserializeFromObject")] + pub fn deserialize_from_object(js_value: &JsValue) -> Result { + numeric::SerializableTransaction::deserialize_from_object(js_value.clone())?.try_into() + } + + /// Deserialize the {@link Transaction} Object from a JSON string. + #[wasm_bindgen(js_name = "deserializeFromJSON")] + pub fn deserialize_from_json(json: &str) -> Result { + numeric::SerializableTransaction::deserialize_from_json(json)?.try_into() + } + + /// Deserialize the {@link Transaction} Object from a "Safe" JSON schema where all `bigint` values are represented as `string`. + #[wasm_bindgen(js_name = "deserializeFromSafeJSON")] + pub fn deserialize_from_safe_json(json: &str) -> Result { + string::SerializableTransaction::deserialize_from_json(json)?.try_into() + } +} diff --git a/consensus/wasm/src/utxo.rs b/consensus/client/src/utxo.rs similarity index 57% rename from consensus/wasm/src/utxo.rs rename to consensus/client/src/utxo.rs index 07c4e5110..ffa7f7a49 100644 --- a/consensus/wasm/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -1,12 +1,46 @@ use crate::imports::*; +use crate::outpoint::{TransactionOutpoint, TransactionOutpointInner}; use crate::result::Result; -use crate::{TransactionOutpoint, TransactionOutpointInner}; use kaspa_addresses::Address; -use workflow_wasm::abi::ref_from_abi; + +#[wasm_bindgen(typescript_custom_section)] +const TS_UTXO_ENTRY: &'static str = r#" +/** + * Interface defines the structure of a UTXO entry. + * + * @category Consensus + */ +export interface IUtxoEntry { + /** @readonly */ + address?: Address; + /** @readonly */ + outpoint: ITransactionOutpoint; + /** @readonly */ + amount : bigint; + /** @readonly */ + scriptPublicKey : IScriptPublicKey; + /** @readonly */ + blockDaaScore: bigint; + /** @readonly */ + isCoinbase: boolean; +} + +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = Array, typescript_type = "UtxoEntryReference[]")] + pub type UtxoEntryReferenceArrayT; + #[wasm_bindgen(typescript_type = "IUtxoEntry")] + pub type IUtxoEntry; + #[wasm_bindgen(typescript_type = "IUtxoEntry[]")] + pub type IUtxoEntryArray; +} pub type UtxoEntryId = TransactionOutpointInner; -#[derive(Clone, Debug, Serialize, Deserialize)] +/// @category Wallet SDK +#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] #[serde(rename_all = "camelCase")] #[wasm_bindgen(inspectable)] pub struct UtxoEntry { @@ -14,27 +48,73 @@ pub struct UtxoEntry { pub address: Option
, #[wasm_bindgen(getter_with_clone)] pub outpoint: TransactionOutpoint, - #[wasm_bindgen(js_name=entry, getter_with_clone)] - pub entry: cctx::UtxoEntry, + pub amount: u64, + #[wasm_bindgen(js_name = scriptPublicKey, getter_with_clone)] + pub script_public_key: ScriptPublicKey, + #[wasm_bindgen(js_name = blockDaaScore)] + pub block_daa_score: u64, + #[wasm_bindgen(js_name = isCoinbase)] + pub is_coinbase: bool, +} + +#[wasm_bindgen] +impl UtxoEntry { + #[wasm_bindgen(js_name = toString)] + pub fn js_to_string(&self) -> Result { + //SerializableUtxoEntry::from(self).serialize_to_json() + Ok(js_sys::JSON::stringify(&self.to_js_object()?.into())?) + } } impl UtxoEntry { #[inline(always)] pub fn amount(&self) -> u64 { - self.entry.amount + self.amount } #[inline(always)] pub fn block_daa_score(&self) -> u64 { - self.entry.block_daa_score + self.block_daa_score } #[inline(always)] pub fn is_coinbase(&self) -> bool { - self.entry.is_coinbase + self.is_coinbase + } + + fn to_js_object(&self) -> Result { + let obj = js_sys::Object::new(); + if let Some(address) = &self.address { + obj.set("address", &address.to_string().into())?; + } + + let outpoint = js_sys::Object::new(); + outpoint.set("transactionId", &self.outpoint.transaction_id().to_string().into())?; + outpoint.set("index", &self.outpoint.index().into())?; + + obj.set("amount", &self.amount.to_string().into())?; + obj.set("outpoint", &outpoint.into())?; + obj.set("scriptPublicKey", &workflow_wasm::serde::to_value(&self.script_public_key)?)?; + obj.set("blockDaaScore", &self.block_daa_score.to_string().into())?; + obj.set("isCoinbase", &self.is_coinbase.into())?; + + Ok(obj) + } +} + +impl From<&UtxoEntry> for cctx::UtxoEntry { + fn from(utxo: &UtxoEntry) -> Self { + cctx::UtxoEntry { + amount: utxo.amount, + script_public_key: utxo.script_public_key.clone(), + block_daa_score: utxo.block_daa_score, + is_coinbase: utxo.is_coinbase, + } + // value.entry.clone() } } -#[derive(Clone, Debug, Serialize, Deserialize)] +/// @category Wallet SDK +#[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] #[wasm_bindgen(inspectable)] pub struct UtxoEntryReference { #[wasm_bindgen(skip)] @@ -43,6 +123,14 @@ pub struct UtxoEntryReference { #[wasm_bindgen] impl UtxoEntryReference { + #[wasm_bindgen(js_name = toString)] + pub fn js_to_string(&self) -> Result { + //let entry = workflow_wasm::serde::to_value(&SerializableUtxoEntry::from(self))?; + let object = js_sys::Object::new(); + object.set("entry", &self.utxo.to_js_object()?.into())?; + Ok(js_sys::JSON::stringify(&object)?) + } + #[wasm_bindgen(getter)] pub fn entry(&self) -> UtxoEntry { self.as_ref().clone() @@ -65,12 +153,12 @@ impl UtxoEntryReference { #[wasm_bindgen(getter, js_name = "isCoinbase")] pub fn is_coinbase(&self) -> bool { - self.utxo.entry.is_coinbase + self.utxo.is_coinbase } #[wasm_bindgen(getter, js_name = "blockDaaScore")] pub fn block_daa_score(&self) -> u64 { - self.utxo.entry.block_daa_score + self.utxo.block_daa_score } } @@ -87,7 +175,7 @@ impl UtxoEntryReference { #[inline(always)] pub fn amount_as_ref(&self) -> &u64 { - &self.utxo.entry.amount + &self.utxo.amount } #[inline(always)] @@ -119,6 +207,13 @@ impl From for UtxoEntry { } } +impl From<&UtxoEntryReference> for cctx::UtxoEntry { + fn from(value: &UtxoEntryReference) -> Self { + value.utxo.as_ref().into() + // (*value.utxo).clone() + } +} + impl From for UtxoEntryReference { fn from(entry: UtxoEntry) -> Self { Self { utxo: Arc::new(entry) } @@ -151,7 +246,14 @@ pub trait TryIntoUtxoEntryReferences { impl TryIntoUtxoEntryReferences for JsValue { fn try_into_utxo_entry_references(&self) -> Result> { - Array::from(self).iter().map(UtxoEntryReference::try_from).collect() + Array::from(self).iter().map(UtxoEntryReference::try_owned_from).collect() + } +} + +impl TryCastFromJs for UtxoEntry { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Ok(Self::try_ref_from_js_value_as_cast(value)?) } } @@ -160,7 +262,8 @@ impl TryIntoUtxoEntryReferences for JsValue { /// processing. This struct keeps a list of entries represented /// by `UtxoEntryReference` struct. This data structure is used /// internally by the framework, but is exposed for convenience. -/// Please consider using `UtxoContect` instead. +/// Please consider using `UtxoContext` instead. +/// @category Wallet SDK #[derive(Default, Clone, Debug, Serialize, Deserialize)] #[wasm_bindgen(inspectable)] pub struct UtxoEntries(Arc>); @@ -194,7 +297,7 @@ impl UtxoEntries { let items = Array::from(js_value) .iter() .map(|js_value| { - ref_from_abi!(UtxoEntryReference, &js_value).unwrap_or_else(|err| panic!("invalid UtxoEntryReference: {err}")) + UtxoEntryReference::try_owned_from(&js_value).unwrap_or_else(|err| panic!("invalid UtxoEntryReference: {err}")) }) .collect::>(); self.0 = Arc::new(items); @@ -234,7 +337,7 @@ impl From> for UtxoEntries { impl From for Vec> { fn from(value: UtxoEntries) -> Self { - value.0.as_ref().iter().map(|entry| Some(entry.utxo.entry.clone())).collect::>() + value.0.as_ref().iter().map(|entry| Some(entry.utxo.as_ref().into())).collect::>() } } @@ -267,39 +370,29 @@ impl TryFrom for UtxoEntries { } } -impl TryFrom for UtxoEntryReference { +impl TryCastFromJs for UtxoEntryReference { type Error = Error; - fn try_from(js_value: JsValue) -> std::result::Result { - Self::try_from(&js_value) - } -} - -impl TryFrom<&JsValue> for UtxoEntryReference { - type Error = Error; - fn try_from(js_value: &JsValue) -> std::result::Result { - if let Ok(utxo_entry) = ref_from_abi!(UtxoEntry, js_value) { - Ok(Self::from(utxo_entry)) - } else if let Ok(utxo_entry_reference) = ref_from_abi!(UtxoEntryReference, js_value) { - Ok(utxo_entry_reference) - } else if let Some(object) = Object::try_from(js_value) { - let address = Address::try_from(object.get_value("address")?)?; - let outpoint = TransactionOutpoint::try_from(object.get_value("outpoint")?)?; - let utxo_entry = Object::from(object.get_value("utxoEntry")?); - let amount = utxo_entry.get_u64("amount")?; - let script_public_key = ScriptPublicKey::try_from(utxo_entry.get_value("scriptPublicKey")?)?; - let block_daa_score = utxo_entry.get_u64("blockDaaScore")?; - let is_coinbase = utxo_entry.get_bool("isCoinbase")?; - - let utxo_entry = UtxoEntry { - address: Some(address), - outpoint, - entry: cctx::UtxoEntry { amount, script_public_key, block_daa_score, is_coinbase }, - }; - - Ok(UtxoEntryReference::from(utxo_entry)) - } else { - Err("Data type supplied to UtxoEntryReference must be an object".into()) - } + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Ok(utxo_entry) = UtxoEntry::try_ref_from_js_value(&value) { + Ok(Self::from(utxo_entry.clone())) + } else if let Some(object) = Object::try_from(value.as_ref()) { + let address = object.get_cast::
("address")?.into_owned(); + let outpoint = TransactionOutpoint::try_from(object.get_value("outpoint")?.as_ref())?; + let utxo_entry = Object::from(object.get_value("utxoEntry")?); + let amount = utxo_entry.get_u64("amount")?; + let script_public_key = ScriptPublicKey::try_owned_from(utxo_entry.get_value("scriptPublicKey")?)?; + let block_daa_score = utxo_entry.get_u64("blockDaaScore")?; + let is_coinbase = utxo_entry.get_bool("isCoinbase")?; + + let utxo_entry = + UtxoEntry { address: Some(address), outpoint, amount, script_public_key, block_daa_score, is_coinbase }; + + Ok(UtxoEntryReference::from(utxo_entry)) + } else { + Err("Data type supplied to UtxoEntryReference must be an object".into()) + } + }) } } @@ -316,11 +409,8 @@ impl UtxoEntryReference { let block_daa_score = 0; let is_coinbase = true; - let utxo_entry = UtxoEntry { - address: Some(address.clone()), - outpoint, - entry: cctx::UtxoEntry { amount, script_public_key, block_daa_score, is_coinbase }, - }; + let utxo_entry = + UtxoEntry { address: Some(address.clone()), outpoint, amount, script_public_key, block_daa_score, is_coinbase }; UtxoEntryReference::from(utxo_entry) } diff --git a/consensus/client/src/vtx.rs b/consensus/client/src/vtx.rs new file mode 100644 index 000000000..e5fdd9236 --- /dev/null +++ b/consensus/client/src/vtx.rs @@ -0,0 +1,35 @@ +use crate::imports::*; +// use crate::serializable::{numeric,string}; +use crate::result::Result; +use kaspa_addresses::Address; +use serde::de::DeserializeOwned; +// use serde::de::DeserializeOwned; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VirtualTransactionT +where + T: Clone + serde::Serialize, +{ + //} + Deserialize { + pub version: u32, + pub generator: Option, + pub transactions: Vec, + pub addresses: Option>, +} + +impl VirtualTransactionT +where + T: Clone + Serialize, +{ + pub fn deserialize(json: &str) -> Result + where + T: DeserializeOwned, + { + Ok(serde_json::from_str(json)?) + } + + pub fn serialize(&self) -> String { + serde_json::to_string(self).unwrap() + } +} diff --git a/consensus/core/Cargo.toml b/consensus/core/Cargo.toml index 43669547b..4e4bd9ea0 100644 --- a/consensus/core/Cargo.toml +++ b/consensus/core/Cargo.toml @@ -9,6 +9,11 @@ include.workspace = true license.workspace = true repository.workspace = true +[features] +devnet-prealloc = [] +wasm32-sdk = [] +default = [] + [dependencies] async-trait.workspace = true borsh.workspace = true @@ -48,8 +53,5 @@ web-sys.workspace = true name = "serde_benchmark" harness = false -[features] -devnet-prealloc = [] - [lints.clippy] empty_docs = "allow" diff --git a/consensus/core/src/block.rs b/consensus/core/src/block.rs index 3917f1709..dde6fd5e7 100644 --- a/consensus/core/src/block.rs +++ b/consensus/core/src/block.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use crate::{ coinbase::MinerData, header::Header, @@ -7,6 +5,7 @@ use crate::{ BlueWorkType, }; use kaspa_hashes::Hash; +use std::sync::Arc; /// A mutable block structure where header and transactions within can still be mutated. #[derive(Debug, Clone)] diff --git a/consensus/core/src/hashing/mod.rs b/consensus/core/src/hashing/mod.rs index aa40a9e96..aa0ac2d9b 100644 --- a/consensus/core/src/hashing/mod.rs +++ b/consensus/core/src/hashing/mod.rs @@ -6,7 +6,7 @@ pub mod sighash; pub mod sighash_type; pub mod tx; -pub(crate) trait HasherExtensions { +pub trait HasherExtensions { /// Writes the len as u64 little endian bytes fn write_len(&mut self, len: usize) -> &mut Self; diff --git a/consensus/core/src/hashing/sighash.rs b/consensus/core/src/hashing/sighash.rs index 8cf6390e3..c1b6133e8 100644 --- a/consensus/core/src/hashing/sighash.rs +++ b/consensus/core/src/hashing/sighash.rs @@ -24,7 +24,7 @@ impl SigHashReusedValues { } } -fn previous_outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues) -> Hash { +pub fn previous_outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues) -> Hash { if hash_type.is_sighash_anyone_can_pay() { return ZERO_HASH; } @@ -43,7 +43,7 @@ fn previous_outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values } } -fn sequences_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues) -> Hash { +pub fn sequences_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues) -> Hash { if hash_type.is_sighash_single() || hash_type.is_sighash_anyone_can_pay() || hash_type.is_sighash_none() { return ZERO_HASH; } @@ -61,7 +61,7 @@ fn sequences_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut } } -fn sig_op_counts_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues) -> Hash { +pub fn sig_op_counts_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues) -> Hash { if hash_type.is_sighash_anyone_can_pay() { return ZERO_HASH; } @@ -79,7 +79,7 @@ fn sig_op_counts_hash(tx: &Transaction, hash_type: SigHashType, reused_values: & } } -fn payload_hash(tx: &Transaction) -> Hash { +pub fn payload_hash(tx: &Transaction) -> Hash { if tx.subnetwork_id == SUBNETWORK_ID_NATIVE { return ZERO_HASH; } @@ -92,7 +92,7 @@ fn payload_hash(tx: &Transaction) -> Hash { hasher.finalize() } -fn outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues, input_index: usize) -> Hash { +pub fn outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut SigHashReusedValues, input_index: usize) -> Hash { if hash_type.is_sighash_none() { return ZERO_HASH; } @@ -122,17 +122,17 @@ fn outputs_hash(tx: &Transaction, hash_type: SigHashType, reused_values: &mut Si } } -fn hash_outpoint(hasher: &mut impl Hasher, outpoint: TransactionOutpoint) { +pub fn hash_outpoint(hasher: &mut impl Hasher, outpoint: TransactionOutpoint) { hasher.update(outpoint.transaction_id); hasher.write_u32(outpoint.index); } -fn hash_output(hasher: &mut impl Hasher, output: &TransactionOutput) { +pub fn hash_output(hasher: &mut impl Hasher, output: &TransactionOutput) { hasher.write_u64(output.value); hash_script_public_key(hasher, &output.script_public_key); } -fn hash_script_public_key(hasher: &mut impl Hasher, script_public_key: &ScriptPublicKey) { +pub fn hash_script_public_key(hasher: &mut impl Hasher, script_public_key: &ScriptPublicKey) { hasher.write_u16(script_public_key.version()); hasher.write_var_bytes(script_public_key.script()); } diff --git a/consensus/core/src/hashing/sighash_type.rs b/consensus/core/src/hashing/sighash_type.rs index 7696cb5f5..76d772f0d 100644 --- a/consensus/core/src/hashing/sighash_type.rs +++ b/consensus/core/src/hashing/sighash_type.rs @@ -1,3 +1,5 @@ +use wasm_bindgen::prelude::*; + pub const SIG_HASH_ALL: SigHashType = SigHashType(0b00000001); pub const SIG_HASH_NONE: SigHashType = SigHashType(0b00000010); pub const SIG_HASH_SINGLE: SigHashType = SigHashType(0b00000100); @@ -17,6 +19,7 @@ const ALLOWED_SIG_HASH_TYPES_VALUES: [u8; 6] = [ ]; #[derive(Copy, Clone)] +#[wasm_bindgen] pub struct SigHashType(pub(crate) u8); impl SigHashType { diff --git a/consensus/core/src/header.rs b/consensus/core/src/header.rs index 709b5b2c1..b6c2b9bc7 100644 --- a/consensus/core/src/header.rs +++ b/consensus/core/src/header.rs @@ -1,38 +1,26 @@ use crate::{hashing, BlueWorkType}; use borsh::{BorshDeserialize, BorshSerialize}; -use js_sys::{Array, Object}; use kaspa_hashes::Hash; -use kaspa_utils::hex::ToHex; use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::*; -use wasm_bindgen::prelude::*; -use workflow_wasm::prelude::*; +/// @category Consensus #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] -#[wasm_bindgen(inspectable)] pub struct Header { - #[wasm_bindgen(skip)] - pub hash: Hash, // Cached hash + /// Cached hash + pub hash: Hash, pub version: u16, - #[wasm_bindgen(skip)] pub parents_by_level: Vec>, - #[wasm_bindgen(skip)] pub hash_merkle_root: Hash, - #[wasm_bindgen(skip)] pub accepted_id_merkle_root: Hash, - #[wasm_bindgen(skip)] pub utxo_commitment: Hash, - pub timestamp: u64, // Timestamp is in milliseconds + /// Timestamp is in milliseconds + pub timestamp: u64, pub bits: u32, pub nonce: u64, - #[wasm_bindgen(js_name = "daaScore")] pub daa_score: u64, - #[wasm_bindgen(skip)] pub blue_work: BlueWorkType, - #[wasm_bindgen(js_name = "blueScore")] pub blue_score: u64, - #[wasm_bindgen(skip)] pub pruning_point: Hash, } @@ -104,183 +92,6 @@ impl Header { } } -#[wasm_bindgen] -impl Header { - /// Finalizes the header and recomputes (updates) the header hash - /// @return { String } header hash - #[wasm_bindgen(js_name = finalize)] - pub fn finalize_js(&mut self) -> String { - self.hash = hashing::header::hash(self); - self.hash.to_hex() - } - - #[wasm_bindgen(constructor)] - pub fn constructor(js_value: JsValue) -> std::result::Result { - Ok(js_value.try_into()?) - } - - /// Obtain `JSON` representation of the header. JSON representation - /// should be obtained using WASM, to ensure proper serialization of - /// big integers. - #[wasm_bindgen(js_name = asJSON)] - pub fn as_json(&self) -> String { - serde_json::to_string(self).unwrap() - } - - #[wasm_bindgen(getter = hash)] - pub fn get_hash_as_hex(&self) -> String { - self.hash.to_hex() - } - - #[wasm_bindgen(getter = hashMerkleRoot)] - pub fn get_hash_merkle_root_as_hex(&self) -> String { - self.hash_merkle_root.to_hex() - } - - #[wasm_bindgen(setter = hashMerkleRoot)] - pub fn set_hash_merkle_root_from_js_value(&mut self, js_value: JsValue) { - self.hash_merkle_root = Hash::from_slice(&js_value.try_as_vec_u8().expect("hash merkle root")); - } - - #[wasm_bindgen(getter = acceptedIdMerkleRoot)] - pub fn get_accepted_id_merkle_root_as_hex(&self) -> String { - self.accepted_id_merkle_root.to_hex() - } - - #[wasm_bindgen(setter = acceptedIdMerkleRoot)] - pub fn set_accepted_id_merkle_root_from_js_value(&mut self, js_value: JsValue) { - self.accepted_id_merkle_root = Hash::from_slice(&js_value.try_as_vec_u8().expect("accepted id merkle root")); - } - - #[wasm_bindgen(getter = utxoCommitment)] - pub fn get_utxo_commitment_as_hex(&self) -> String { - self.utxo_commitment.to_hex() - } - - #[wasm_bindgen(setter = utxoCommitment)] - pub fn set_utxo_commitment_from_js_value(&mut self, js_value: JsValue) { - self.utxo_commitment = Hash::from_slice(&js_value.try_as_vec_u8().expect("utxo commitment")); - } - - #[wasm_bindgen(getter = pruningPoint)] - pub fn get_pruning_point_as_hex(&self) -> String { - self.pruning_point.to_hex() - } - - #[wasm_bindgen(setter = pruningPoint)] - pub fn set_pruning_point_from_js_value(&mut self, js_value: JsValue) { - self.pruning_point = Hash::from_slice(&js_value.try_as_vec_u8().expect("pruning point")); - } - - #[wasm_bindgen(getter = parentsByLevel)] - pub fn get_parents_by_level_as_js_value(&self) -> JsValue { - to_value(&self.parents_by_level).expect("invalid parents_by_level") - } - - #[wasm_bindgen(setter = parentsByLevel)] - pub fn set_parents_by_level_from_js_value(&mut self, js_value: JsValue) { - let array = Array::from(&js_value); - self.parents_by_level = array - .iter() - .map(|jsv| { - Array::from(&jsv) - .to_vec() - .into_iter() - .map(|hash| Ok(hash.try_into()?)) - .collect::, Error>>() - }) - .collect::>, Error>>() - .unwrap_or_else(|err| { - panic!("{}", err); - }); - } - - #[wasm_bindgen(getter = blueWork)] - pub fn blue_work(&self) -> js_sys::BigInt { - self.blue_work.try_into().unwrap_or_else(|err| panic!("invalid blue work: {err}")) - } - - #[wasm_bindgen(js_name = getBlueWorkAsHex)] - pub fn get_blue_work_as_hex(&self) -> String { - self.blue_work.to_hex() - } - - #[wasm_bindgen(setter = blueWork)] - pub fn set_blue_work_from_js_value(&mut self, js_value: JsValue) { - self.blue_work = js_value.try_into().unwrap_or_else(|err| panic!("invalid blue work: {err}")); - } -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("{0}")] - Custom(String), - #[error("{0}")] - SerdeWasmBindgen(#[from] serde_wasm_bindgen::Error), - #[error(transparent)] - WorkflowWasm(#[from] workflow_wasm::error::Error), - #[error("`TryFrom for Header` - error converting property `{0}`: {1}")] - Conversion(&'static str, String), -} - -impl Error { - pub fn custom>(msg: S) -> Self { - Self::Custom(msg.into()) - } - - pub fn convert(prop: &'static str, msg: S) -> Self { - Self::Conversion(prop, msg.to_string()) - } -} - -impl TryFrom for Header { - type Error = Error; - fn try_from(js_value: JsValue) -> std::result::Result { - if let Some(object) = Object::try_from(&js_value) { - let parents_by_level = object - .get_vec("parentsByLevel")? - .iter() - .map(|jsv| { - Array::from(jsv) - .to_vec() - .into_iter() - .map(|hash| Ok(hash.try_into()?)) - .collect::, Error>>() - }) - .collect::>, Error>>()?; - - let header = Self { - hash: object.get_value("hash")?.try_into().unwrap_or_default(), - version: object.get_u16("version")?, - parents_by_level, - hash_merkle_root: object - .get_value("hashMerkleRoot")? - .try_into() - .map_err(|err| Error::convert("hashMerkleRoot", err))?, - accepted_id_merkle_root: object - .get_value("acceptedIdMerkleRoot")? - .try_into() - .map_err(|err| Error::convert("acceptedIdMerkleRoot", err))?, - utxo_commitment: object - .get_value("utxoCommitment")? - .try_into() - .map_err(|err| Error::convert("utxoCommitment", err))?, - nonce: object.get_u64("nonce")?, - timestamp: object.get_u64("timestamp")?, - daa_score: object.get_u64("daaScore")?, - bits: object.get_u32("bits")?, - blue_work: object.get_value("blueWork")?.try_into().map_err(|err| Error::convert("blueWork", err))?, - blue_score: object.get_u64("blueScore")?, - pruning_point: object.get_value("pruningPoint")?.try_into().map_err(|err| Error::convert("pruningPoint", err))?, - }; - - Ok(header) - } else { - Err(Error::Custom("supplied argument must be an object".to_string())) - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/consensus/core/src/network.rs b/consensus/core/src/network.rs index 4f30ee94e..ad59adfc3 100644 --- a/consensus/core/src/network.rs +++ b/consensus/core/src/network.rs @@ -4,9 +4,9 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{Debug, Display, Formatter}; use std::ops::Deref; use std::str::FromStr; +use wasm_bindgen::convert::TryFromJsValue; use wasm_bindgen::prelude::*; -use workflow_core::enums::u8_try_from; -use workflow_wasm::abi::ref_from_abi; +use workflow_wasm::prelude::*; #[derive(thiserror::Error, PartialEq, Eq, Debug, Clone)] pub enum NetworkTypeError { @@ -14,16 +14,15 @@ pub enum NetworkTypeError { InvalidNetworkType(String), } -u8_try_from! { - #[derive(Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq)] - #[serde(rename_all = "lowercase")] - #[wasm_bindgen] - pub enum NetworkType { - Mainnet, - Testnet, - Devnet, - Simnet, - } +/// @category Consensus +#[derive(Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[serde(rename_all = "lowercase")] +#[wasm_bindgen] +pub enum NetworkType { + Mainnet, + Testnet, + Devnet, + Simnet, } impl NetworkType { @@ -113,27 +112,36 @@ impl Display for NetworkType { } } -impl TryFrom for NetworkType { +impl TryFrom<&NetworkTypeT> for NetworkType { type Error = NetworkTypeError; - fn try_from(js_value: JsValue) -> Result { - NetworkType::try_from(&js_value) - } -} - -impl TryFrom<&JsValue> for NetworkType { - type Error = NetworkTypeError; - fn try_from(js_value: &JsValue) -> Result { - if let Some(network_type) = js_value.as_string() { + fn try_from(value: &NetworkTypeT) -> Result { + if let Ok(network_id) = NetworkId::try_cast_from(value) { + Ok(network_id.network_type()) + } else if let Some(network_type) = value.as_string() { Self::from_str(&network_type) - } else if let Some(v) = js_value.as_f64() { - Self::try_from(v as u8).map_err(|_| NetworkTypeError::InvalidNetworkType(format!("{js_value:?}"))) + } else if let Ok(network_type) = NetworkType::try_from_js_value(JsValue::from(value)) { + Ok(network_type) } else { - Err(NetworkTypeError::InvalidNetworkType(format!("{js_value:?}"))) + Err(NetworkTypeError::InvalidNetworkType(format!("{value:?}"))) } } } -#[derive(thiserror::Error, PartialEq, Eq, Debug, Clone)] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "Network", typescript_type = "NetworkType | NetworkId | string")] + #[derive(Debug)] + pub type NetworkTypeT; +} + +impl TryFrom<&NetworkTypeT> for Prefix { + type Error = NetworkIdError; + fn try_from(value: &NetworkTypeT) -> Result { + Ok(NetworkType::try_from(value)?.into()) + } +} + +#[derive(thiserror::Error, Debug, Clone)] pub enum NetworkIdError { #[error("Invalid network name prefix: {0}. The expected prefix is 'kaspa'.")] InvalidPrefix(String), @@ -155,9 +163,24 @@ pub enum NetworkIdError { #[error("Invalid network id: '{0}'")] InvalidNetworkId(String), + + #[error(transparent)] + Wasm(#[from] workflow_wasm::error::Error), +} + +impl From for JsValue { + fn from(err: NetworkIdError) -> Self { + JsValue::from_str(&err.to_string()) + } } -#[derive(Clone, Copy, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +/// +/// NetworkId is a unique identifier for a kaspa network instance. +/// It is composed of a network type and an optional suffix. +/// +/// @category Consensus +/// +#[derive(Clone, Copy, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq, Hash, Ord, PartialOrd, CastFromJs)] #[wasm_bindgen(inspectable)] pub struct NetworkId { #[wasm_bindgen(js_name = "type")] @@ -335,8 +358,8 @@ impl<'de> Deserialize<'de> for NetworkId { #[wasm_bindgen] impl NetworkId { #[wasm_bindgen(constructor)] - pub fn ctor(value: JsValue) -> Result { - value.try_into().map_err(|err: NetworkIdError| err.to_string()) + pub fn ctor(value: &JsValue) -> Result { + Ok(NetworkId::try_cast_from(value)?.into_owned()) } #[wasm_bindgen(getter, js_name = "id")] @@ -350,61 +373,41 @@ impl NetworkId { } #[wasm_bindgen(js_name = "addressPrefix")] - pub fn js_address_prefix(&self) -> Result { - Ok(Prefix::from(self.network_type).to_string()) + pub fn js_address_prefix(&self) -> String { + Prefix::from(self.network_type).to_string() } } -impl TryFrom for NetworkId { - type Error = NetworkIdError; - fn try_from(js_value: JsValue) -> Result { - NetworkId::try_from(&js_value) - } +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "NetworkId | string")] + pub type NetworkIdT; } impl TryFrom<&JsValue> for NetworkId { type Error = NetworkIdError; - fn try_from(js_value: &JsValue) -> Result { - if let Some(network_id) = js_value.as_string() { - NetworkId::from_str(&network_id) - } else if let Ok(network_id) = ref_from_abi!(NetworkId, js_value) { - Ok(network_id) - } else { - Err(NetworkIdError::InvalidNetworkId(format!("{:?}", js_value))) - } + fn try_from(value: &JsValue) -> Result { + Self::try_owned_from(value) } } -pub mod wasm { - use super::*; +impl TryFrom for NetworkId { + type Error = NetworkIdError; + fn try_from(value: JsValue) -> Result { + Self::try_owned_from(value) + } +} - #[wasm_bindgen] - extern "C" { - #[wasm_bindgen(js_name = "Network", typescript_type = "NetworkType | NetworkId | string")] - pub type Network; - } - - impl TryFrom for NetworkType { - type Error = String; - fn try_from(network: Network) -> std::result::Result { - let js_value = JsValue::from(network); - if let Ok(network_id) = ref_from_abi!(NetworkId, &js_value) { - Ok(network_id.network_type()) - } else if let Ok(network_id) = NetworkId::try_from(&js_value) { - Ok(network_id.network_type()) - } else if let Ok(network_type) = NetworkType::try_from(&js_value) { - Ok(network_type) +impl TryCastFromJs for NetworkId { + type Error = NetworkIdError; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(network_id) = value.as_ref().as_string() { + Ok(NetworkId::from_str(&network_id)?) } else { - Err(format!("Invalid network value: {:?}", js_value)) + Err(NetworkIdError::InvalidNetworkId(format!("{:?}", value.as_ref()))) } - } - } - - impl TryFrom for Prefix { - type Error = String; - fn try_from(value: Network) -> Result { - NetworkType::try_from(value).map(Into::into) - } + }) } } @@ -454,7 +457,11 @@ mod tests { ]; for test in tests { - assert_eq!(NetworkId::from_str(test.expr), test.expected, "{}: unexpected result", test.name); + let Test { name, expr, expected } = test; + match NetworkId::from_str(expr) { + Ok(nid) => assert_eq!(nid, expected.unwrap(), "{}: unexpected result", name), + Err(err) => assert_eq!(err.to_string(), expected.unwrap_err().to_string(), "{}: unexpected error", name), + } } } } diff --git a/consensus/core/src/sign.rs b/consensus/core/src/sign.rs index e9171afe6..dee0d3844 100644 --- a/consensus/core/src/sign.rs +++ b/consensus/core/src/sign.rs @@ -79,7 +79,7 @@ impl Signed { } /// Sign a transaction using schnorr -pub fn sign(mut signable_tx: SignableTransaction, schnorr_key: secp256k1::KeyPair) -> SignableTransaction { +pub fn sign(mut signable_tx: SignableTransaction, schnorr_key: secp256k1::Keypair) -> SignableTransaction { for i in 0..signable_tx.tx.inputs.len() { signable_tx.tx.inputs[i].sig_op_count = 1; } @@ -87,7 +87,7 @@ pub fn sign(mut signable_tx: SignableTransaction, schnorr_key: secp256k1::KeyPai let mut reused_values = SigHashReusedValues::new(); for i in 0..signable_tx.tx.inputs.len() { let sig_hash = calc_schnorr_signature_hash(&signable_tx.as_verifiable(), i, SIG_HASH_ALL, &mut reused_values); - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice()).unwrap(); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); 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) signable_tx.tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect(); @@ -99,7 +99,7 @@ pub fn sign(mut signable_tx: SignableTransaction, schnorr_key: secp256k1::KeyPai pub fn sign_with_multiple(mut mutable_tx: SignableTransaction, privkeys: Vec<[u8; 32]>) -> SignableTransaction { let mut map = BTreeMap::new(); for privkey in privkeys { - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, &privkey).unwrap(); + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &privkey).unwrap(); map.insert(schnorr_key.public_key().serialize(), schnorr_key); } for i in 0..mutable_tx.tx.inputs.len() { @@ -111,7 +111,7 @@ pub fn sign_with_multiple(mut mutable_tx: SignableTransaction, privkeys: Vec<[u8 let script = mutable_tx.entries[i].as_ref().unwrap().script_public_key.script(); if let Some(schnorr_key) = map.get(script) { let sig_hash = calc_schnorr_signature_hash(&mutable_tx.as_verifiable(), i, SIG_HASH_ALL, &mut reused_values); - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice()).unwrap(); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); 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(); @@ -123,10 +123,10 @@ 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 #[allow(clippy::result_large_err)] -pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: Vec<[u8; 32]>) -> Signed { +pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: &[[u8; 32]]) -> Signed { let mut map = BTreeMap::new(); for privkey in privkeys { - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, &privkey).unwrap(); + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey).unwrap(); let schnorr_public_key = schnorr_key.public_key().x_only_public_key().0; let script_pub_key_script = once(0x20).chain(schnorr_public_key.serialize().into_iter()).chain(once(0xac)).collect_vec(); map.insert(script_pub_key_script, schnorr_key); @@ -138,7 +138,7 @@ pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: Vec< let script = mutable_tx.entries[i].as_ref().unwrap().script_public_key.script(); if let Some(schnorr_key) = map.get(script) { let sig_hash = calc_schnorr_signature_hash(&mutable_tx.as_verifiable(), i, SIG_HASH_ALL, &mut reused_values); - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice()).unwrap(); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); 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(); @@ -163,7 +163,7 @@ pub fn verify(tx: &impl crate::tx::VerifiableTransaction) -> Result<(), Error> { let pk = secp256k1::XOnlyPublicKey::from_slice(pk)?; let sig = secp256k1::schnorr::Signature::from_slice(&input.signature_script[1..65])?; let sig_hash = calc_schnorr_signature_hash(tx, i, SIG_HASH_ALL, &mut reused_values); - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice())?; + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice())?; sig.verify(&msg, &pk)?; } diff --git a/consensus/core/src/subnets.rs b/consensus/core/src/subnets.rs index 94c08fb62..2456f8444 100644 --- a/consensus/core/src/subnets.rs +++ b/consensus/core/src/subnets.rs @@ -4,6 +4,7 @@ use std::str::{self, FromStr}; use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_utils::hex::{FromHex, ToHex}; use kaspa_utils::{serde_impl_deser_fixed_bytes_ref, serde_impl_ser_fixed_bytes_ref}; +use thiserror::Error; /// The size of the array used to store subnetwork IDs. pub const SUBNETWORK_ID_SIZE: usize = 20; @@ -65,12 +66,28 @@ impl SubnetworkId { } } +#[derive(Error, Debug, Clone)] +pub enum SubnetworkConversionError { + #[error("Invalid bytes")] + InvalidBytes, + + #[error(transparent)] + SliceError(#[from] std::array::TryFromSliceError), + + #[error(transparent)] + HexError(#[from] faster_hex::Error), +} + impl TryFrom<&[u8]> for SubnetworkId { - type Error = std::array::TryFromSliceError; + type Error = SubnetworkConversionError; fn try_from(value: &[u8]) -> Result { let bytes = <[u8; SUBNETWORK_ID_SIZE]>::try_from(value)?; - Ok(Self(bytes)) + if bytes != Self::from_byte(0).0 && bytes != Self::from_byte(1).0 { + Err(Self::Error::InvalidBytes) + } else { + Ok(Self(bytes)) + } } } @@ -92,22 +109,30 @@ impl ToHex for SubnetworkId { } impl FromStr for SubnetworkId { - type Err = faster_hex::Error; + type Err = SubnetworkConversionError; #[inline] fn from_str(hex_str: &str) -> Result { let mut bytes = [0u8; SUBNETWORK_ID_SIZE]; faster_hex::hex_decode(hex_str.as_bytes(), &mut bytes)?; - Ok(SubnetworkId(bytes)) + if bytes != Self::from_byte(0).0 && bytes != Self::from_byte(1).0 { + Err(Self::Err::InvalidBytes) + } else { + Ok(Self(bytes)) + } } } impl FromHex for SubnetworkId { - type Error = faster_hex::Error; + type Error = SubnetworkConversionError; fn from_hex(hex_str: &str) -> Result { let mut bytes = [0u8; SUBNETWORK_ID_SIZE]; faster_hex::hex_decode(hex_str.as_bytes(), &mut bytes)?; - Ok(SubnetworkId(bytes)) + if bytes != Self::from_byte(0).0 && bytes != Self::from_byte(1).0 { + Err(Self::Error::InvalidBytes) + } else { + Ok(Self(bytes)) + } } } diff --git a/consensus/core/src/tx.rs b/consensus/core/src/tx.rs index 302cbac5d..137633701 100644 --- a/consensus/core/src/tx.rs +++ b/consensus/core/src/tx.rs @@ -28,9 +28,10 @@ pub type TransactionId = kaspa_hashes::Hash; /// set such as whether or not it was contained in a coinbase tx, the daa /// score of the block that accepts the tx, its public key script, and how /// much it pays. +/// @category Consensus #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] -#[wasm_bindgen(inspectable, js_name = TxUtxoEntry)] +#[wasm_bindgen(inspectable, js_name = TransactionUtxoEntry)] pub struct UtxoEntry { pub amount: u64, #[wasm_bindgen(js_name = scriptPublicKey, getter_with_clone)] diff --git a/consensus/core/src/tx/script_public_key.rs b/consensus/core/src/tx/script_public_key.rs index 30f6ea739..7f3ade694 100644 --- a/consensus/core/src/tx/script_public_key.rs +++ b/consensus/core/src/tx/script_public_key.rs @@ -33,8 +33,21 @@ use wasm_bindgen::prelude::wasm_bindgen; //Represents a Set of [`ScriptPublicKey`]s pub type ScriptPublicKeys = HashSet; +#[wasm_bindgen(typescript_custom_section)] +const TS_SCRIPT_PUBLIC_KEY: &'static str = r#" +/** + * Interface defines the structure of a Script Public Key. + * + * @category Consensus + */ +export interface IScriptPublicKey { + script: HexString; +} +"#; + /// Represents a Kaspad ScriptPublicKey -#[derive(Default, PartialEq, Eq, Clone, Hash)] +/// @category Consensus +#[derive(Default, PartialEq, Eq, Clone, Hash, CastFromJs)] #[wasm_bindgen(inspectable)] pub struct ScriptPublicKey { pub version: ScriptPublicKeyVersion, @@ -350,17 +363,17 @@ impl BorshDeserialize for ScriptPublicKey { } } -impl TryFrom for ScriptPublicKey { - type Error = JsValue; - - fn try_from(js_value: JsValue) -> Result { - if let Ok(script_public_key) = ref_from_abi!(ScriptPublicKey, &js_value) { - Ok(script_public_key) - } else if let Some(hex_str) = js_value.as_string() { - Self::from_str(&hex_str).map_err(|e| JsValue::from_str(&format!("{}", e))) - } else { - Err(JsValue::from_str(&format!("Unable to convert ScriptPublicKey from: {js_value:?}"))) - } +type CastError = workflow_wasm::error::Error; +impl TryCastFromJs for ScriptPublicKey { + type Error = workflow_wasm::error::Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(hex_str) = value.as_ref().as_string() { + Ok(Self::from_str(&hex_str).map_err(CastError::custom)?) + } else { + Err(CastError::custom(format!("Unable to convert ScriptPublicKey from: {:?}", value.as_ref()))) + } + }) } } @@ -400,8 +413,10 @@ mod tests { assert_eq!(spk, spk2); } + use wasm_bindgen::convert::IntoWasmAbi; use wasm_bindgen_test::wasm_bindgen_test; use workflow_wasm::serde::{from_value, to_value}; + #[wasm_bindgen_test] pub fn test_wasm_serde_constructor() { let version = 0xc0de; @@ -418,6 +433,7 @@ mod tests { assert_eq!(spk, from_value(spk_js.clone()).map_err(|_| ()).unwrap()); assert_eq!(JsValue::from_str("string"), spk_js.js_typeof()); } + #[wasm_bindgen_test] pub fn test_wasm_serde_js_spk_object() { let version = 0xc0de; @@ -439,8 +455,6 @@ mod tests { #[wasm_bindgen_test] pub fn test_wasm_serde_spk_object() { - use wasm_bindgen::convert::IntoWasmAbi; - let version = 0xc0de; let vec: Vec = (0..SCRIPT_VECTOR_SIZE as u8).collect(); let spk = ScriptPublicKey::from_vec(version, vec.clone()); diff --git a/consensus/pow/Cargo.toml b/consensus/pow/Cargo.toml index 086c21474..13dc80f37 100644 --- a/consensus/pow/Cargo.toml +++ b/consensus/pow/Cargo.toml @@ -9,19 +9,23 @@ include.workspace = true license.workspace = true repository.workspace = true +[features] +wasm32-sdk = [] + [dependencies] js-sys.workspace = true kaspa-consensus-core.workspace = true +kaspa-consensus-client.workspace = true kaspa-hashes.workspace = true kaspa-math.workspace = true kaspa-utils.workspace = true wasm-bindgen.workspace = true workflow-wasm.workspace = true +num.workspace = true [dev-dependencies] criterion.workspace = true - [[bench]] name = "bench" harness = false \ No newline at end of file diff --git a/consensus/pow/src/lib.rs b/consensus/pow/src/lib.rs index c0ff7e366..c3fbdd867 100644 --- a/consensus/pow/src/lib.rs +++ b/consensus/pow/src/lib.rs @@ -1,10 +1,10 @@ // public for benchmarks #[doc(hidden)] pub mod matrix; +#[cfg(feature = "wasm32-sdk")] +pub mod wasm; #[doc(hidden)] pub mod xoshiro; -//#[cfg(target_arch="wasm32")] -pub mod wasm; use std::cmp::max; diff --git a/consensus/pow/src/wasm.rs b/consensus/pow/src/wasm.rs index da61f0579..f5179e44a 100644 --- a/consensus/pow/src/wasm.rs +++ b/consensus/pow/src/wasm.rs @@ -1,15 +1,18 @@ use crate::matrix::Matrix; use js_sys::BigInt; -use kaspa_consensus_core::{hashing, header::Header}; +use kaspa_consensus_client::Header; +use kaspa_consensus_core::hashing; use kaspa_hashes::Hash; use kaspa_hashes::PowHash; use kaspa_math::Uint256; use kaspa_utils::hex::ToHex; +use num::Float; use wasm_bindgen::prelude::*; use workflow_wasm::error::Error; use workflow_wasm::prelude::*; use workflow_wasm::result::Result; +/// @category PoW #[wasm_bindgen(inspectable)] pub struct State { inner: crate::State, @@ -24,6 +27,9 @@ impl State { // the pre_pow_hash value internally, making it available // via the `pre_pow_hash` property getter. + // obtain locked inner + let header = header.inner(); + let target = Uint256::from_compact_target_bits(header.bits); // Zero out the time and nonce. let pre_pow_hash = hashing::header::hash_override_nonce_time(header, 0, 0); @@ -55,3 +61,29 @@ impl State { self.pre_pow_hash.to_hex() } } + +// https://github.com/tmrlvi/kaspa-miner/blob/bf361d02a46c580f55f46b5dfa773477634a5753/src/client/stratum.rs#L36 +const DIFFICULTY_1_TARGET: (u64, i16) = (0xffffu64, 208); // 0xffff 2^208 + +/// `calculate_difficulty` is based on set_difficulty function: +/// @category PoW +#[wasm_bindgen(js_name = calculateDifficulty)] +pub fn calculate_difficulty(difficulty: f32) -> std::result::Result { + let mut buf = [0u64, 0u64, 0u64, 0u64]; + let (mantissa, exponent, _) = difficulty.recip().integer_decode(); + let new_mantissa = mantissa * DIFFICULTY_1_TARGET.0; + let new_exponent = (DIFFICULTY_1_TARGET.1 + exponent) as u64; + let start = (new_exponent / 64) as usize; + let remainder = new_exponent % 64; + + buf[start] = new_mantissa << remainder; // bottom + if start < 3 { + buf[start + 1] = new_mantissa >> (64 - remainder); // top + } else if new_mantissa.leading_zeros() < remainder as u32 { + return Err(JsError::new("Target is too big")); + } + + // let target_pool = Uint256(buf); + // workflow_log::log_info!("Difficulty: {:?}, Target: 0x{}", difficulty, target_pool.to_hex()); + Ok(Uint256(buf).try_into()?) +} diff --git a/consensus/src/processes/transaction_validator/transaction_validator_populated.rs b/consensus/src/processes/transaction_validator/transaction_validator_populated.rs index ca3186368..696b9a9d4 100644 --- a/consensus/src/processes/transaction_validator/transaction_validator_populated.rs +++ b/consensus/src/processes/transaction_validator/transaction_validator_populated.rs @@ -675,7 +675,7 @@ mod tests { is_coinbase: false, }, ]; - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, &secret_key.secret_bytes()).unwrap(); + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &secret_key.secret_bytes()).unwrap(); let signed_tx = sign(MutableTransaction::with_entries(unsigned_tx, entries), schnorr_key); let populated_tx = signed_tx.as_verifiable(); assert_eq!(tv.check_scripts(&populated_tx), Ok(())); diff --git a/consensus/wasm/Cargo.toml b/consensus/wasm/Cargo.toml index 1da0b26a8..ea211f3f9 100644 --- a/consensus/wasm/Cargo.toml +++ b/consensus/wasm/Cargo.toml @@ -9,22 +9,28 @@ include.workspace = true license.workspace = true repository.workspace = true +[features] +wasm32-sdk = [] +wasm32-types = [] + [dependencies] kaspa-consensus-core.workspace = true +kaspa-consensus-client.workspace = true kaspa-addresses.workspace = true kaspa-txscript.workspace = true kaspa-hashes.workspace = true kaspa-utils.workspace = true -wasm-bindgen.workspace = true -serde.workspace = true +cfg-if.workspace = true +faster-hex.workspace = true +js-sys.workspace = true +rand.workspace = true +secp256k1.workspace = true serde_json.workspace = true serde-wasm-bindgen.workspace = true +serde.workspace = true thiserror.workspace = true -js-sys.workspace = true -secp256k1.workspace = true -faster-hex.workspace = true -rand.workspace = true +wasm-bindgen.workspace = true workflow-wasm.workspace = true workflow-log.workspace = true diff --git a/consensus/wasm/src/error.rs b/consensus/wasm/src/error.rs index cb76a6764..470c5cc4b 100644 --- a/consensus/wasm/src/error.rs +++ b/consensus/wasm/src/error.rs @@ -39,6 +39,9 @@ pub enum Error { #[error(transparent)] NetworkTypeError(#[from] kaspa_consensus_core::network::NetworkTypeError), + + #[error(transparent)] + ConsensusClient(#[from] kaspa_consensus_client::error::Error), } // unsafe impl Send for Error {} diff --git a/consensus/wasm/src/input.rs b/consensus/wasm/src/input.rs deleted file mode 100644 index 1cfe08175..000000000 --- a/consensus/wasm/src/input.rs +++ /dev/null @@ -1,129 +0,0 @@ -use super::TransactionOutpoint; -use crate::imports::*; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TransactionInputInner { - pub previous_outpoint: TransactionOutpoint, - pub signature_script: Vec, - pub sequence: u64, - pub sig_op_count: u8, -} - -/// Represents a Kaspa transaction input -#[derive(Clone, Debug, Serialize, Deserialize)] -#[wasm_bindgen(inspectable)] -pub struct TransactionInput { - inner: Arc>, -} - -impl TransactionInput { - pub fn new(previous_outpoint: TransactionOutpoint, signature_script: Vec, sequence: u64, sig_op_count: u8) -> Self { - Self { inner: Arc::new(Mutex::new(TransactionInputInner { previous_outpoint, signature_script, sequence, sig_op_count })) } - } - - pub fn new_with_inner(inner: TransactionInputInner) -> Self { - Self { inner: Arc::new(Mutex::new(inner)) } - } - - pub fn inner(&self) -> MutexGuard<'_, TransactionInputInner> { - self.inner.lock().unwrap() - } - - pub fn sig_op_count(&self) -> u8 { - self.inner().sig_op_count - } -} - -#[wasm_bindgen] -impl TransactionInput { - #[wasm_bindgen(constructor)] - pub fn constructor(js_value: JsValue) -> Result { - Ok(js_value.try_into()?) - } - - #[wasm_bindgen(getter = previousOutpoint)] - pub fn get_previous_outpoint(&self) -> TransactionOutpoint { - self.inner().previous_outpoint.clone() - } - - #[wasm_bindgen(setter = previousOutpoint)] - pub fn set_previous_outpoint(&mut self, js_value: JsValue) { - self.inner().previous_outpoint = js_value.try_into().expect("invalid signature script"); - } - - #[wasm_bindgen(getter = signatureScript)] - pub fn get_signature_script_as_hex(&self) -> String { - self.inner().signature_script.to_hex() - } - - #[wasm_bindgen(setter = signatureScript)] - pub fn set_signature_script_from_js_value(&mut self, js_value: JsValue) { - self.inner().signature_script = js_value.try_as_vec_u8().expect("invalid signature script"); - } - - #[wasm_bindgen(getter = sequence)] - pub fn get_sequence(&self) -> u64 { - self.inner().sequence - } - - #[wasm_bindgen(setter = sequence)] - pub fn set_sequence(&mut self, sequence: u64) { - self.inner().sequence = sequence; - } - - #[wasm_bindgen(getter = sigOpCount)] - pub fn get_sig_op_count(&self) -> u8 { - self.inner().sig_op_count - } - - #[wasm_bindgen(setter = sigOpCount)] - pub fn set_sig_op_count(&mut self, sig_op_count: u8) { - self.inner().sig_op_count = sig_op_count; - } -} - -impl AsRef for TransactionInput { - fn as_ref(&self) -> &TransactionInput { - self - } -} - -impl TryFrom for TransactionInput { - type Error = Error; - fn try_from(js_value: JsValue) -> Result { - if let Some(object) = Object::try_from(&js_value) { - let previous_outpoint: TransactionOutpoint = object.get_value("previousOutpoint")?.try_into()?; - let signature_script = object.get_vec_u8("signatureScript")?; - let sequence = object.get_u64("sequence")?; - let sig_op_count = object.get_u8("sigOpCount")?; - - Ok(TransactionInput::new(previous_outpoint, signature_script, sequence, sig_op_count)) - } else { - Err("TransactionInput must be an object".into()) - } - } -} - -impl From for TransactionInput { - fn from(tx_input: cctx::TransactionInput) -> Self { - TransactionInput::new_with_inner(TransactionInputInner { - previous_outpoint: tx_input.previous_outpoint.into(), - signature_script: tx_input.signature_script, - sequence: tx_input.sequence, - sig_op_count: tx_input.sig_op_count, - }) - } -} - -impl From<&TransactionInput> for cctx::TransactionInput { - fn from(tx_input: &TransactionInput) -> Self { - let inner = tx_input.inner(); - cctx::TransactionInput::new( - inner.previous_outpoint.clone().into(), - inner.signature_script.clone(), - inner.sequence, - inner.sig_op_count, - ) - } -} diff --git a/consensus/wasm/src/keypair.rs b/consensus/wasm/src/keypair.rs deleted file mode 100644 index 3781d4d95..000000000 --- a/consensus/wasm/src/keypair.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! -//! [`keypair`](mod@self) module encapsulates [`Keypair`] and [`PrivateKey`]. -//! The [`Keypair`] provides access to the secret and public keys. -//! -//! ```javascript -//! -//! let keypair = Keypair.random(); -//! let privateKey = keypair.privateKey; -//! let publicKey = keypair.publicKey; -//! -//! // to obtain an address from a keypair -//! let address = keypair.toAddress(NetworkType.Mainnnet); -//! -//! // to obtain a keypair from a private key -//! let keypair = privateKey.toKeypair(); -//! -//! ``` -//! - -use crate::error::Error; -use crate::result::Result; -use js_sys::{Array, Uint8Array}; -use kaspa_addresses::{Address, Version as AddressVersion}; -use kaspa_consensus_core::network::wasm::Network; -#[allow(unused_imports)] // needed for rust doc! -use kaspa_consensus_core::network::NetworkType; -use secp256k1::{Secp256k1, XOnlyPublicKey}; -use serde_wasm_bindgen::to_value; -use std::str::FromStr; -use wasm_bindgen::prelude::*; -use workflow_wasm::abi::*; - -/// Data structure that contains a secret and public keys. -#[derive(Debug, Clone)] -#[wasm_bindgen(inspectable)] -pub struct Keypair { - secret_key: secp256k1::SecretKey, - public_key: secp256k1::PublicKey, - xonly_public_key: XOnlyPublicKey, -} - -#[wasm_bindgen] -impl Keypair { - fn new(secret_key: secp256k1::SecretKey, public_key: secp256k1::PublicKey, xonly_public_key: XOnlyPublicKey) -> Self { - Self { secret_key, public_key, xonly_public_key } - } - - /// Get the [`PublicKey`] of this [`Keypair`]. - #[wasm_bindgen(getter = publicKey)] - pub fn get_public_key(&self) -> JsValue { - to_value(&self.public_key).unwrap() - } - - /// Get the [`PrivateKey`] of this [`Keypair`]. - #[wasm_bindgen(getter = privateKey)] - pub fn get_private_key(&self) -> PrivateKey { - (&self.secret_key).into() - } - - /// Get the `XOnlyPublicKey` of this [`Keypair`]. - #[wasm_bindgen(getter = xOnlyPublicKey)] - pub fn get_xonly_public_key(&self) -> JsValue { - to_value(&self.xonly_public_key).unwrap() - } - - /// Get the [`Address`] of this Keypair's [`PublicKey`]. - /// Receives a [`NetworkType`] to determine the prefix of the address. - /// JavaScript: `let address = keypair.toAddress(NetworkType.MAINNET);`. - #[wasm_bindgen(js_name = toAddress)] - // pub fn to_address(&self, network_type: NetworkType) -> Result
{ - pub fn to_address(&self, network: Network) -> Result
{ - let pk = PublicKey { xonly_public_key: self.xonly_public_key, source: self.public_key.to_string() }; - let address = pk.to_address(network)?; - Ok(address) - } - - /// Get `ECDSA` [`Address`] of this Keypair's [`PublicKey`]. - /// Receives a [`NetworkType`] to determine the prefix of the address. - /// JavaScript: `let address = keypair.toAddress(NetworkType.MAINNET);`. - #[wasm_bindgen(js_name = toAddressECDSA)] - pub fn to_address_ecdsa(&self, network: Network) -> Result
{ - let pk = PublicKey { xonly_public_key: self.xonly_public_key, source: self.public_key.to_string() }; - let address = pk.to_address_ecdsa(network)?; - Ok(address) - } - - /// Create a new random [`Keypair`]. - /// JavaScript: `let keypair = Keypair::random();`. - #[wasm_bindgen] - pub fn random() -> Result { - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng()); - let (xonly_public_key, _) = public_key.x_only_public_key(); - Ok(Keypair::new(secret_key, public_key, xonly_public_key)) - } - - /// Create a new [`Keypair`] from a [`PrivateKey`]. - /// JavaScript: `let privkey = new PrivateKey(hexString); let keypair = privkey.toKeypair();`. - #[wasm_bindgen(js_name = "fromPrivateKey")] - pub fn from_private_key(secret_key: &PrivateKey) -> Result { - let secp = Secp256k1::new(); - let secret_key = secp256k1::SecretKey::from_slice(&secret_key.secret_bytes())?; - let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); - let (xonly_public_key, _) = public_key.x_only_public_key(); - Ok(Keypair::new(secret_key, public_key, xonly_public_key)) - } -} - -impl TryFrom for Keypair { - type Error = Error; - fn try_from(value: JsValue) -> std::result::Result { - Ok(ref_from_abi!(Keypair, &value)?) - } -} - -/// Data structure that envelops a Private Key -#[derive(Clone, Debug)] -#[wasm_bindgen] -pub struct PrivateKey { - inner: secp256k1::SecretKey, -} - -impl PrivateKey { - pub fn secret_bytes(&self) -> [u8; 32] { - self.inner.secret_bytes() - } -} - -impl From<&secp256k1::SecretKey> for PrivateKey { - fn from(value: &secp256k1::SecretKey) -> Self { - Self { inner: *value } - } -} - -impl From<&PrivateKey> for [u8; 32] { - fn from(key: &PrivateKey) -> Self { - key.secret_bytes() - } -} - -#[wasm_bindgen] -impl PrivateKey { - /// Create a new [`PrivateKey`] from a hex-encoded string. - #[wasm_bindgen(constructor)] - pub fn try_new(key: &str) -> Result { - Ok(Self { inner: secp256k1::SecretKey::from_str(key)? }) - } -} - -impl PrivateKey { - pub fn try_from_slice(data: &[u8]) -> Result { - Ok(Self { inner: secp256k1::SecretKey::from_slice(data)? }) - } -} - -#[wasm_bindgen] -impl PrivateKey { - /// Returns the [`PrivateKey`] key encoded as a hex string. - #[wasm_bindgen(js_name = toString)] - pub fn to_hex(&self) -> String { - use kaspa_utils::hex::ToHex; - self.secret_bytes().to_vec().to_hex() - } - - /// Generate a [`Keypair`] from this [`PrivateKey`]. - #[wasm_bindgen(js_name = toKeypair)] - pub fn to_keypair(&self) -> Result { - Keypair::from_private_key(self) - } -} - -impl TryFrom for PrivateKey { - type Error = Error; - fn try_from(js_value: JsValue) -> std::result::Result { - if let Some(hex_str) = js_value.as_string() { - Self::try_new(hex_str.as_str()) - } else if Array::is_array(&js_value) { - let array = Uint8Array::new(&js_value); - Self::try_from_slice(array.to_vec().as_slice()) - } else { - Ok(ref_from_abi!(PrivateKey, &js_value)?) - } - } -} - -// Data structure that envelopes a PublicKey -// Only supports Schnorr-based addresses -#[derive(Clone, Debug)] -#[wasm_bindgen(js_name = PublicKey)] -pub struct PublicKey { - xonly_public_key: XOnlyPublicKey, - source: String, -} - -#[wasm_bindgen(js_class = PublicKey)] -impl PublicKey { - /// Create a new [`PublicKey`] from a hex-encoded string. - #[wasm_bindgen(constructor)] - pub fn try_new(key: &str) -> Result { - match secp256k1::PublicKey::from_str(key) { - Ok(public_key) => { - let (xonly_public_key, _) = public_key.x_only_public_key(); - Ok(Self { xonly_public_key, source: (*key).to_string() }) - } - Err(_e) => Ok(Self { xonly_public_key: XOnlyPublicKey::from_str(key)?, source: (*key).to_string() }), - } - } - - #[wasm_bindgen(js_name = "toString")] - pub fn js_to_string(&self) -> String { - self.source.clone() - } - - /// Get the [`Address`] of this PublicKey. - /// Receives a [`NetworkType`] to determine the prefix of the address. - /// JavaScript: `let address = keypair.toAddress(NetworkType.MAINNET);`. - #[wasm_bindgen(js_name = toAddress)] - pub fn to_address(&self, network: Network) -> Result
{ - let payload = &self.xonly_public_key.serialize(); - let address = Address::new(network.try_into()?, AddressVersion::PubKey, payload); - Ok(address) - } - - /// Get `ECDSA` [`Address`] of this PublicKey. - /// Receives a [`NetworkType`] to determine the prefix of the address. - /// JavaScript: `let address = keypair.toAddress(NetworkType.MAINNET);`. - #[wasm_bindgen(js_name = toAddressECDSA)] - pub fn to_address_ecdsa(&self, network: Network) -> Result
{ - let payload = &self.xonly_public_key.serialize(); - let address = Address::new(network.try_into()?, AddressVersion::PubKeyECDSA, payload); - Ok(address) - } -} - -impl std::fmt::Display for PublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.source) - } -} - -impl TryFrom for PublicKey { - type Error = Error; - fn try_from(js_value: JsValue) -> std::result::Result { - if let Some(hex_str) = js_value.as_string() { - Self::try_new(hex_str.as_str()) - } else { - Ok(ref_from_abi!(PublicKey, &js_value)?) - } - } -} - -impl From for XOnlyPublicKey { - fn from(value: PublicKey) -> Self { - value.xonly_public_key - } -} diff --git a/consensus/wasm/src/lib.rs b/consensus/wasm/src/lib.rs index efdfdf5d0..75f53f380 100644 --- a/consensus/wasm/src/lib.rs +++ b/consensus/wasm/src/lib.rs @@ -1,24 +1,11 @@ +use cfg_if::cfg_if; + pub mod error; -mod imports; -pub mod input; -pub mod keypair; -pub mod outpoint; -pub mod output; -pub mod result; -pub mod signable; -pub mod signer; -pub mod transaction; -pub mod txscript; -pub mod utils; -pub mod utxo; -pub use input::*; -pub use keypair::*; -pub use outpoint::*; -pub use output::*; -pub use signable::*; -pub use signer::*; -pub use transaction::*; -pub use txscript::*; -pub use utils::*; -pub use utxo::*; +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + pub mod result; + mod utils; + pub use utils::*; + } +} diff --git a/consensus/wasm/src/signable.rs b/consensus/wasm/src/signable.rs deleted file mode 100644 index c71d0c8af..000000000 --- a/consensus/wasm/src/signable.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::imports::*; -use crate::utils::script_hashes; -use crate::utxo::UtxoEntries; -use crate::{Transaction, TransactionInput, TransactionOutput}; -use kaspa_consensus_core::tx; -use serde_wasm_bindgen::to_value; -use std::str::FromStr; - -/// Represents a generic mutable transaction -#[derive(Clone, Debug, Serialize, Deserialize)] -#[wasm_bindgen(inspectable)] -pub struct SignableTransaction { - tx: Arc>, - /// UTXO entry data - #[wasm_bindgen(getter_with_clone)] - pub entries: UtxoEntries, -} - -#[wasm_bindgen] -impl SignableTransaction { - #[wasm_bindgen(constructor)] - pub fn new_from_refs(tx: &Transaction, entries: &UtxoEntries) -> Self { - Self { tx: Arc::new(Mutex::new(tx.clone())), entries: entries.clone() } - } - - #[wasm_bindgen(getter=tx)] - pub fn tx_getter(&self) -> Transaction { - self.tx.lock().unwrap().clone() - } - - #[wasm_bindgen(js_name=toJSON)] - pub fn to_json(&self) -> Result { - Ok(self.serialize(serde_json::value::Serializer)?.to_string()) - } - - #[wasm_bindgen(js_name=fromJSON)] - pub fn from_json(json: &str) -> Result { - let mtx: Self = serde_json::from_value(serde_json::Value::from_str(json)?)?; - Ok(mtx) - } - - #[wasm_bindgen(js_name=getScriptHashes)] - pub fn script_hashes(&self) -> Result { - let hashes = script_hashes(self.clone().into())?; - Ok(to_value(&hashes)?) - } - - #[wasm_bindgen(js_name=setSignatures)] - pub fn set_signatures(&self, signatures: js_sys::Array) -> Result { - let signatures = - signatures.iter().map(|s| s.try_as_vec_u8()).collect::>, workflow_wasm::error::Error>>()?; - - { - let mut locked = self.tx.lock(); - let tx = locked.as_mut().unwrap(); - - if signatures.len() != tx.inner().inputs.len() { - return Err(Error::Custom("Signature counts don't match input counts".to_string()).into()); - } - let len = tx.inner().inputs.len(); - for (i, signature) in signatures.into_iter().enumerate().take(len) { - tx.inner().inputs[i].inner().sig_op_count = 1; - tx.inner().inputs[i].inner().signature_script = signature; - } - } - - Ok(to_value(self)?) - } - - #[wasm_bindgen(getter=inputs)] - pub fn get_inputs(&self) -> Result { - let inputs = self.tx.lock()?.get_inputs_as_js_array(); - Ok(inputs) - } - - #[wasm_bindgen(getter=outputs)] - pub fn get_outputs(&self) -> Result { - let outputs = self.tx.lock()?.get_outputs_as_js_array(); - Ok(outputs) - } - - // TODO (aspect) - discuss - either remove this or make it utilize wasm MassCalculator (address this as a part of MassCalculator refactoring). - // pub fn mass(&self, network_type: NetworkType, estimate_signature_mass: bool, minimum_signatures: u16) -> Result { - // let params = get_consensus_params_by_network(&network_type); - // let calc = MassCalculator::new(params); - // calc.calc_mass_for_tx(tx) - // Ok(calculate_mass(&self.tx(), ¶ms, estimate_signature_mass, minimum_signatures)) - // } -} - -impl SignableTransaction { - pub fn new(tx: Transaction, entries: UtxoEntries) -> Self { - Self { tx: Arc::new(Mutex::new(tx)), entries } - } - - pub fn id(&self) -> TransactionId { - self.tx.lock().unwrap().id() - } - - pub fn tx(&self) -> MutexGuard<'_, Transaction> { - self.tx.lock().unwrap() - } - pub fn inputs(&self) -> Result, crate::error::Error> { - Ok(self.tx.lock().unwrap().inner().inputs.clone()) - } - - pub fn outputs(&self) -> Result, crate::error::Error> { - Ok(self.tx.lock().unwrap().inner().outputs.clone()) - } - - pub fn total_input_amount(&self) -> Result { - let amount = self.entries.items().iter().map(|entry| entry.amount()).sum(); - Ok(amount) - } - - pub fn total_output_amount(&self) -> Result { - let amount = self.outputs()?.iter().map(|output| output.get_value()).sum(); - Ok(amount) - } -} - -impl From for tx::SignableTransaction { - fn from(mtx: SignableTransaction) -> Self { - let tx = &mtx.tx.lock().unwrap().clone(); - Self { tx: tx.into(), entries: mtx.entries.into(), calculated_fee: None, calculated_compute_mass: None } - } -} - -impl TryFrom<(tx::SignableTransaction, UtxoEntries)> for SignableTransaction { - type Error = Error; - fn try_from(value: (tx::SignableTransaction, UtxoEntries)) -> Result { - Ok(Self { tx: Arc::new(Mutex::new(value.0.tx.into())), entries: value.1 }) - } -} - -impl From for Transaction { - fn from(mtx: SignableTransaction) -> Self { - mtx.tx.lock().unwrap().clone() - } -} - -impl TryFrom for SignableTransaction { - type Error = Error; - fn try_from(js_value: JsValue) -> Result { - SignableTransaction::try_from(&js_value) - } -} - -impl TryFrom<&JsValue> for SignableTransaction { - type Error = Error; - fn try_from(js_value: &JsValue) -> Result { - Ok(ref_from_abi!(SignableTransaction, js_value)?) - } -} diff --git a/consensus/wasm/src/signer.rs b/consensus/wasm/src/signer.rs deleted file mode 100644 index 326b56df7..000000000 --- a/consensus/wasm/src/signer.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::imports::*; -use crate::keypair::PrivateKey; -use crate::result::Result; -use crate::signable::*; -use js_sys::Array; -use kaspa_consensus_core::{ - hashing::sighash_type::SIG_HASH_ALL, - sign::{sign_with_multiple_v2, verify}, - tx, -}; -use kaspa_hashes::Hash; -use serde_wasm_bindgen::from_value; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(extends = js_sys::Array, is_type_of = Array::is_array, typescript_type = "PrivateKey[]")] - #[derive(Clone, Debug, PartialEq, Eq)] - pub type PrivateKeyArray; -} - -impl TryFrom for Vec { - type Error = crate::error::Error; - fn try_from(keys: PrivateKeyArray) -> std::result::Result { - let mut private_keys: Vec = vec![]; - for key in keys.iter() { - private_keys.push(PrivateKey::try_from(key).map_err(|_| Self::Error::Custom("Unable to cast PrivateKey".to_string()))?); - } - - Ok(private_keys) - } -} - -/// `signTransaction()` is a helper function to sign a transaction using a private key array or a signer array. -#[wasm_bindgen(js_name = "signTransaction")] -pub fn js_sign_transaction(mtx: SignableTransaction, signer: PrivateKeyArray, verify_sig: bool) -> Result { - if signer.is_array() { - let mut private_keys: Vec<[u8; 32]> = vec![]; - for key in Array::from(&signer).iter() { - let key = PrivateKey::try_from(key).map_err(|_| Error::Custom("Unable to cast PrivateKey".to_string()))?; - private_keys.push(key.secret_bytes()); - } - - let mtx = sign_transaction(mtx, private_keys, verify_sig).map_err(|err| Error::Custom(format!("Unable to sign: {err:?}")))?; - Ok(mtx) - } else { - Err(Error::custom("signTransaction() requires an array of signatures")) - } -} - -pub fn sign_transaction(mtx: SignableTransaction, private_keys: Vec<[u8; 32]>, verify_sig: bool) -> Result { - let entries = mtx.entries.clone(); - let mtx = sign_transaction_impl(mtx.into(), private_keys, verify_sig)?; - let mtx = SignableTransaction::try_from((mtx, entries))?; - Ok(mtx) -} - -fn sign_transaction_impl( - mtx: tx::SignableTransaction, - private_keys: Vec<[u8; 32]>, - verify_sig: bool, -) -> Result { - let mtx = sign(mtx, private_keys)?; - if verify_sig { - let tx_verifiable = mtx.as_verifiable(); - verify(&tx_verifiable)?; - } - Ok(mtx) -} - -/// 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).unwrap()) -} - -#[wasm_bindgen(js_name=signScriptHash)] -pub fn sign_script_hash(script_hash: JsValue, privkey: &PrivateKey) -> Result { - let script_hash = from_value(script_hash)?; - let result = sign_hash(script_hash, &privkey.into())?; - Ok(result.to_hex()) -} - -pub fn sign_hash(sig_hash: Hash, privkey: &[u8; 32]) -> Result> { - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice())?; - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; - let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref(); - let signature = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect(); - Ok(signature) -} diff --git a/consensus/wasm/src/transaction.rs b/consensus/wasm/src/transaction.rs deleted file mode 100644 index 434217ee7..000000000 --- a/consensus/wasm/src/transaction.rs +++ /dev/null @@ -1,265 +0,0 @@ -use crate::imports::*; -use crate::input::TransactionInput; -use crate::output::TransactionOutput; -use crate::result::Result; -use kaspa_consensus_core::subnets::{self, SubnetworkId}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TransactionInner { - pub version: u16, - pub inputs: Vec, - pub outputs: Vec, - pub lock_time: u64, - pub subnetwork_id: SubnetworkId, - pub gas: u64, - pub payload: Vec, - - // A field that is used to cache the transaction ID. - // Always use the corresponding self.id() instead of accessing this field directly - pub id: TransactionId, -} - -/// Represents a Kaspa transaction -#[derive(Clone, Debug, Serialize, Deserialize)] -#[wasm_bindgen(inspectable)] -pub struct Transaction { - inner: Arc>, -} - -impl Transaction { - pub fn new( - version: u16, - inputs: Vec, - outputs: Vec, - lock_time: u64, - subnetwork_id: SubnetworkId, - gas: u64, - payload: Vec, - ) -> Result { - let tx = Self { - inner: Arc::new(Mutex::new(TransactionInner { - version, - inputs, - outputs, - lock_time, - subnetwork_id, - gas, - payload, - id: Default::default(), // Temp init before the finalize below - })), - }; - tx.finalize()?; - Ok(tx) - } - - pub fn new_with_inner(inner: TransactionInner) -> Self { - Self { inner: Arc::new(Mutex::new(inner)) } - } - - pub fn inner(&self) -> MutexGuard<'_, TransactionInner> { - self.inner.lock().unwrap() - } - - pub fn id(&self) -> TransactionId { - self.inner().id - } -} - -#[wasm_bindgen] -impl Transaction { - /// Determines whether or not a transaction is a coinbase transaction. A coinbase - /// transaction is a special transaction created by miners that distributes fees and block subsidy - /// to the previous blocks' miners, and specifies the script_pub_key that will be used to pay the current - /// miner in future blocks. - pub fn is_coinbase(&self) -> bool { - self.inner().subnetwork_id == subnets::SUBNETWORK_ID_COINBASE - } - - /// Recompute and finalize the tx id based on updated tx fields - pub fn finalize(&self) -> Result { - let tx: cctx::Transaction = self.into(); - self.inner().id = tx.id(); - Ok(self.inner().id) - } - - /// Returns the transaction ID - #[wasm_bindgen(getter, js_name = id)] - pub fn id_string(&self) -> String { - self.inner().id.to_string() - } - - #[wasm_bindgen(constructor)] - pub fn constructor(js_value: JsValue) -> std::result::Result { - Ok(js_value.try_into()?) - } - - #[wasm_bindgen(getter = inputs)] - pub fn get_inputs_as_js_array(&self) -> Array { - let inputs = self.inner.lock().unwrap().inputs.clone().into_iter().map(JsValue::from); - Array::from_iter(inputs) - } - - #[wasm_bindgen(setter = inputs)] - pub fn set_inputs_from_js_array(&mut self, js_value: &JsValue) { - let inputs = Array::from(js_value) - .iter() - .map(|js_value| { - ref_from_abi!(TransactionInput, &js_value).unwrap_or_else(|err| panic!("invalid transaction input: {err}")) - }) - .collect::>(); - self.inner().inputs = inputs; - } - - #[wasm_bindgen(getter = outputs)] - pub fn get_outputs_as_js_array(&self) -> Array { - let outputs = self.inner.lock().unwrap().outputs.clone().into_iter().map(JsValue::from); - Array::from_iter(outputs) - } - - #[wasm_bindgen(setter = outputs)] - pub fn set_outputs_from_js_array(&mut self, js_value: &JsValue) { - let outputs = Array::from(js_value) - .iter() - .map(|js_value| { - ref_from_abi!(TransactionOutput, &js_value).unwrap_or_else(|err| panic!("invalid transaction output: {err}")) - }) - .collect::>(); - self.inner().outputs = outputs; - } - - #[wasm_bindgen(getter, js_name = version)] - pub fn get_version(&self) -> u16 { - self.inner().version - } - - #[wasm_bindgen(setter, js_name = version)] - pub fn set_version(&self, v: u16) { - self.inner().version = v; - } - - #[wasm_bindgen(getter, js_name = lock_time)] - pub fn get_lock_time(&self) -> u64 { - self.inner().lock_time - } - - #[wasm_bindgen(setter, js_name = lock_time)] - pub fn set_lock_time(&self, v: u64) { - self.inner().lock_time = v; - } - - #[wasm_bindgen(getter, js_name = gas)] - pub fn get_gas(&self) -> u64 { - self.inner().gas - } - - #[wasm_bindgen(setter, js_name = gas)] - pub fn set_gas(&self, v: u64) { - self.inner().gas = v; - } - - #[wasm_bindgen(getter = subnetworkId)] - pub fn get_subnetwork_id_as_hex(&self) -> String { - self.inner().subnetwork_id.to_hex() - } - - #[wasm_bindgen(setter = subnetworkId)] - pub fn set_subnetwork_id_from_js_value(&mut self, js_value: JsValue) { - let subnetwork_id = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("subnetwork id error: {err}")); - self.inner().subnetwork_id = subnetwork_id.as_slice().try_into().unwrap_or_else(|err| panic!("subnetwork id error: {err}")); - } - - #[wasm_bindgen(getter = payload)] - pub fn get_payload_as_hex_string(&self) -> String { - self.inner().payload.to_hex() - } - - #[wasm_bindgen(setter = payload)] - pub fn set_payload_from_js_value(&mut self, js_value: JsValue) { - self.inner.lock().unwrap().payload = js_value.try_as_vec_u8().unwrap_or_else(|err| panic!("payload value error: {err}")); - } -} - -impl TryFrom for Transaction { - type Error = Error; - fn try_from(js_value: JsValue) -> std::result::Result { - Transaction::try_from(&js_value) - } -} - -impl TryFrom<&JsValue> for Transaction { - type Error = Error; - fn try_from(js_value: &JsValue) -> std::result::Result { - if let Ok(tx) = ref_from_abi!(Transaction, js_value) { - Ok(tx) - } else if let Some(object) = Object::try_from(js_value) { - if let Some(tx) = object.try_get_value("tx")? { - Transaction::try_from(&tx) - } else { - let version = object.get_u16("version")?; - let lock_time = object.get_u64("lockTime")?; - let gas = object.get_u64("gas")?; - let payload = object.get_vec_u8("payload")?; - let subnetwork_id = object.get_vec_u8("subnetworkId")?; - if subnetwork_id.len() != subnets::SUBNETWORK_ID_SIZE { - return Err(Error::Custom("subnetworkId must be 20 bytes long".into())); - } - let subnetwork_id: SubnetworkId = subnetwork_id - .as_slice() - .try_into() - .map_err(|err| Error::Custom(format!("`subnetworkId` property error: `{err}`")))?; - let inputs = object - .get_vec("inputs")? - .into_iter() - .map(|jsv| jsv.try_into()) - .collect::, Error>>()?; - let outputs: Vec = object - .get_vec("outputs")? - .into_iter() - .map(|jsv| jsv.try_into()) - .collect::, Error>>()?; - Transaction::new(version, inputs, outputs, lock_time, subnetwork_id, gas, payload) - } - } else { - Err("Transaction must be an object".into()) - } - } -} - -impl From for Transaction { - fn from(tx: cctx::Transaction) -> Self { - let id = tx.id(); - let inputs: Vec = tx.inputs.into_iter().map(|input| input.into()).collect::>(); - let outputs: Vec = tx.outputs.into_iter().map(|output| output.into()).collect::>(); - Self::new_with_inner(TransactionInner { - version: tx.version, - inputs, - outputs, - lock_time: tx.lock_time, - gas: tx.gas, - payload: tx.payload, - subnetwork_id: tx.subnetwork_id, - id, - }) - } -} - -// impl TryFrom<&Transaction> for cctx::Transaction { -impl From<&Transaction> for cctx::Transaction { - fn from(tx: &Transaction) -> Self { - let inner = tx.inner(); - let inputs: Vec = - inner.inputs.clone().into_iter().map(|input| input.as_ref().into()).collect::>(); - let outputs: Vec = - inner.outputs.clone().into_iter().map(|output| output.as_ref().into()).collect::>(); - cctx::Transaction::new( - inner.version, - inputs, - outputs, - inner.lock_time, - inner.subnetwork_id.clone(), - inner.gas, - inner.payload.clone(), - ) - } -} diff --git a/consensus/wasm/src/txscript.rs b/consensus/wasm/src/txscript.rs deleted file mode 100644 index cc3a9f0cf..000000000 --- a/consensus/wasm/src/txscript.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::imports::*; -use crate::result::Result; -use kaspa_txscript::script_builder::ScriptBuilder as Inner; - -/// -/// ScriptBuilder provides a facility for building custom scripts. It allows -/// you to push opcodes, ints, and data while respecting canonical encoding. In -/// general it does not ensure the script will execute correctly, however any -/// data pushes which would exceed the maximum allowed script engine limits and -/// are therefore guaranteed not to execute will not be pushed and will result in -/// the Script function returning an error. -/// -#[derive(Clone)] -#[wasm_bindgen] -pub struct ScriptBuilder { - inner: Arc>, -} - -impl ScriptBuilder { - pub fn inner(&self) -> MutexGuard<'_, Inner> { - self.inner.lock().unwrap() - } -} - -#[wasm_bindgen] -impl ScriptBuilder { - /// Get script bytes represented by a hex string. - pub fn script(&self) -> String { - let inner = self.inner(); - let script = inner.script(); - script.to_hex() - } - - /// Drains (empties) the script builder, returning the - /// script bytes represented by a hex string. - pub fn drain(&self) -> String { - let mut inner = self.inner(); - let script = inner.drain(); - script.to_hex() - } - - /// Pushes the passed opcode to the end of the script. The script will not - /// be modified if pushing the opcode would cause the script to exceed the - /// maximum allowed script engine size. - #[wasm_bindgen(js_name = "addOp")] - pub fn add_op(&self, opcode: u8) -> Result { - self.inner().add_op(opcode)?; - Ok(self.clone()) - } - - #[wasm_bindgen(js_name = "addOps")] - pub fn add_ops(&self, opcodes: JsValue) -> Result { - let opcodes = opcodes.try_as_vec_u8()?; - self.inner().add_ops(&opcodes)?; - Ok(self.clone()) - } - - /// AddData pushes the passed data to the end of the script. It automatically - /// 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`](kaspa_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`](kaspa_txscript::MAX_SCRIPTS_SIZE). - #[wasm_bindgen(js_name = "addData")] - pub fn add_data(&self, data: JsValue) -> Result { - let data = data.try_as_vec_u8()?; - self.inner().add_data(&data)?; - Ok(self.clone()) - } - - #[wasm_bindgen(js_name = "addI64")] - pub fn add_i64(&self, val: i64) -> Result { - self.inner().add_i64(val)?; - Ok(self.clone()) - } - - #[wasm_bindgen(js_name = "addLockTime")] - pub fn add_lock_time(&self, lock_time: u64) -> Result { - self.inner().add_lock_time(lock_time)?; - Ok(self.clone()) - } - - #[wasm_bindgen(js_name = "addSequence")] - pub fn add_sequence(&self, sequence: u64) -> Result { - self.inner().add_sequence(sequence)?; - Ok(self.clone()) - } -} diff --git a/core/src/log/mod.rs b/core/src/log/mod.rs index 5b0a3cd59..4207cb74f 100644 --- a/core/src/log/mod.rs +++ b/core/src/log/mod.rs @@ -5,36 +5,20 @@ #[allow(unused_imports)] pub use log::{Level, LevelFilter}; +pub use workflow_log; cfg_if::cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { -use consts::*; + use consts::*; -mod appender; -mod consts; -mod logger; + mod appender; + mod consts; + mod logger; } } -cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - static mut LEVEL_FILTER : LevelFilter = LevelFilter::Trace; - #[inline(always)] - pub fn log_level_enabled(level: Level) -> bool { - unsafe { LEVEL_FILTER >= level } - } - pub fn set_log_level(level: LevelFilter) { - unsafe { LEVEL_FILTER = level }; - workflow_log::set_log_level(level); - } - } else { - - /// WARNING: This function is internal and - /// only has effect on the workflow_log logger. - pub fn set_log_level(level: LevelFilter) { - workflow_log::set_log_level(level); - } - } +pub fn set_log_level(level: LevelFilter) { + workflow_log::set_log_level(level); } #[cfg(not(target_arch = "wasm32"))] @@ -94,8 +78,8 @@ pub fn try_init_logger(filters: &str) { #[macro_export] macro_rules! trace { ($($t:tt)*) => { - if kaspa_core::log::log_level_enabled(log::Level::Trace) { - kaspa_core::console::log(&format_args!($($t)*).to_string()); + if kaspa_core::log::workflow_log::log_level_enabled(log::Level::Trace) { + kaspa_core::log::workflow_log::impls::trace_impl(None, &format_args!($($t)*)); } }; } @@ -112,8 +96,8 @@ macro_rules! trace { #[macro_export] macro_rules! debug { ($($t:tt)*) => ( - if kaspa_core::log::log_level_enabled(log::Level::Debug) { - kaspa_core::console::log(&format_args!($($t)*).to_string()); + if kaspa_core::log::workflow_log::log_level_enabled(log::Level::Debug) { + kaspa_core::log::workflow_log::impls::debug_impl(None, &format_args!($($t)*)); } ) } @@ -130,8 +114,8 @@ macro_rules! debug { #[macro_export] macro_rules! info { ($($t:tt)*) => ( - if kaspa_core::log::log_level_enabled(log::Level::Info) { - kaspa_core::console::log(&format_args!($($t)*).to_string()); + if kaspa_core::log::workflow_log::log_level_enabled(log::Level::Info) { + kaspa_core::log::workflow_log::impls::info_impl(None, &format_args!($($t)*)); } ) } @@ -148,8 +132,8 @@ macro_rules! info { #[macro_export] macro_rules! warn { ($($t:tt)*) => ( - if kaspa_core::log::log_level_enabled(log::Level::Warn) { - kaspa_core::console::warn(&format_args!($($t)*).to_string()); + if kaspa_core::log::workflow_log::log_level_enabled(log::Level::Warn) { + kaspa_core::log::workflow_log::impls::warn_impl(None, &format_args!($($t)*)); } ) } @@ -166,8 +150,8 @@ macro_rules! warn { #[macro_export] macro_rules! error { ($($t:tt)*) => ( - if kaspa_core::log::log_level_enabled(log::Level::Error) { - kaspa_core::console::error(&format_args!($($t)*).to_string()); + if kaspa_core::log::workflow_log::log_level_enabled(log::Level::Error) { + kaspa_core::log::workflow_log::impls::error_impl(None, &format_args!($($t)*)); } ) } diff --git a/crypto/addresses/Cargo.toml b/crypto/addresses/Cargo.toml index 414955a2b..dd9f8e3ac 100644 --- a/crypto/addresses/Cargo.toml +++ b/crypto/addresses/Cargo.toml @@ -17,6 +17,7 @@ smallvec.workspace = true thiserror.workspace = true wasm-bindgen.workspace = true workflow-wasm.workspace = true +workflow-log.workspace = true [dev-dependencies] criterion.workspace = true @@ -26,3 +27,6 @@ web-sys.workspace = true [[bench]] name = "bench" harness = false + +[lints.clippy] +empty_docs = "allow" diff --git a/crypto/addresses/src/lib.rs b/crypto/addresses/src/lib.rs index 538bb804b..fdba63ef7 100644 --- a/crypto/addresses/src/lib.rs +++ b/crypto/addresses/src/lib.rs @@ -1,11 +1,13 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use js_sys::Array; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use smallvec::SmallVec; use std::fmt::{Display, Formatter}; use thiserror::Error; use wasm_bindgen::prelude::*; -use workflow_wasm::extensions::object::*; +use workflow_wasm::{ + convert::{Cast, CastFromJs, TryCastFromJs}, + extensions::object::*, +}; mod bech32; @@ -32,6 +34,9 @@ pub enum AddressError { #[error("The address is invalid")] InvalidAddress, + #[error("The address array is invalid")] + InvalidAddressArray, + #[error("{0}")] WASM(String), } @@ -109,6 +114,7 @@ impl TryFrom<&str> for Prefix { /// /// Kaspa `Address` version (`PubKey`, `PubKey ECDSA`, `ScriptHash`) /// +/// @category Address #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[repr(u8)] #[wasm_bindgen(js_name = "AddressVersion")] @@ -177,7 +183,8 @@ pub const PAYLOAD_VECTOR_SIZE: usize = 36; pub type PayloadVec = SmallVec<[u8; PAYLOAD_VECTOR_SIZE]>; /// Kaspa `Address` struct that serializes to and from an address format string: `kaspa:qz0s...t8cv`. -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +/// @category Address +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, CastFromJs)] #[wasm_bindgen(inspectable)] pub struct Address { #[wasm_bindgen(skip)] @@ -214,6 +221,11 @@ impl Address { address.try_into().unwrap_or_else(|err| panic!("Address::constructor() - address error `{}`: {err}", address)) } + #[wasm_bindgen(js_name=validate)] + pub fn validate(address: &str) -> bool { + Self::try_from(address).is_ok() + } + /// Convert an address to a string. #[wasm_bindgen(js_name = toString)] pub fn address_to_string(&self) -> String { @@ -475,44 +487,40 @@ impl<'de> Deserialize<'de> for Address { } } -impl TryFrom for Address { +impl TryCastFromJs for Address { type Error = AddressError; - fn try_from(js_value: JsValue) -> Result { - if let Some(string) = js_value.as_string() { - Address::try_from(string) - } else if let Some(object) = js_sys::Object::try_from(&js_value) { - let prefix: Prefix = object.get_string("prefix")?.as_str().try_into()?; - let payload = object.get_string("payload")?; //.as_str(); - Self::decode_payload(prefix, &payload) - } else { - Err(AddressError::InvalidAddress) - } + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(string) = value.as_ref().as_string() { + Address::try_from(string) + } else if let Some(object) = js_sys::Object::try_from(value.as_ref()) { + let prefix: Prefix = object.get_string("prefix")?.as_str().try_into()?; + let payload = object.get_string("payload")?; //.as_str(); + Address::decode_payload(prefix, &payload) + } else { + Err(AddressError::InvalidAddress) + } + }) } } #[wasm_bindgen] -pub struct AddressList(Vec
); - -impl From for Vec
{ - fn from(address_list: AddressList) -> Self { - address_list.0 - } -} - -impl TryFrom for AddressList { - type Error = AddressError; - fn try_from(js_value: JsValue) -> Result { - js_value.as_ref().try_into() - } +extern "C" { + #[wasm_bindgen(extends = js_sys::Array, typescript_type = "Address | string")] + pub type AddressT; + #[wasm_bindgen(extends = js_sys::Array, typescript_type = "(Address | string)[]")] + pub type AddressOrStringArrayT; + #[wasm_bindgen(extends = js_sys::Array, typescript_type = "Address[]")] + pub type AddressArrayT; } -impl TryFrom<&JsValue> for AddressList { +impl TryFrom for Vec
{ type Error = AddressError; - fn try_from(js_value: &JsValue) -> Result { - if let Ok(array) = js_value.clone().dyn_into::() { - Ok(Self(array.iter().map(|v| v.try_into()).collect::, AddressError>>()?)) + fn try_from(js_value: AddressOrStringArrayT) -> Result { + if js_value.is_array() { + js_value.iter().map(Address::try_owned_from).collect::, AddressError>>() } else { - Err(AddressError::InvalidAddress) + Err(AddressError::InvalidAddressArray) } } } diff --git a/crypto/hashes/src/lib.rs b/crypto/hashes/src/lib.rs index 929a6cd8e..6384a96c5 100644 --- a/crypto/hashes/src/lib.rs +++ b/crypto/hashes/src/lib.rs @@ -21,7 +21,8 @@ pub const HASH_SIZE: usize = 32; pub use hashers::*; // TODO: Check if we use hash more as an array of u64 or of bytes and change the default accordingly -#[derive(Eq, Clone, Copy, Default, PartialOrd, Ord, BorshSerialize, BorshDeserialize)] +/// @category General +#[derive(Eq, Clone, Copy, Default, PartialOrd, Ord, BorshSerialize, BorshDeserialize, CastFromJs)] #[wasm_bindgen] pub struct Hash([u8; HASH_SIZE]); @@ -184,32 +185,16 @@ impl Hash { } type TryFromError = workflow_wasm::error::Error; -impl TryFrom for Hash { - type Error = workflow_wasm::error::Error; - fn try_from(js_value: JsValue) -> Result { - let hash = if js_value.is_string() || js_value.is_array() { - let bytes = js_value.try_as_vec_u8()?; - Hash( +impl TryCastFromJs for Hash { + type Error = TryFromError; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + let bytes = value.as_ref().try_as_vec_u8()?; + Ok(Hash( <[u8; HASH_SIZE]>::try_from(bytes) .map_err(|_| TryFromError::WrongSize("Slice must have the length of Hash".into()))?, - ) - } else if js_value.is_object() { - ref_from_abi!(Hash, &js_value).map_err(|_| TryFromError::WrongType("supplied object must be a `Hash`".to_string()))? - } else { - return Err(TryFromError::WrongType("supplied object must be a `Hash`".to_string())); - }; - Ok(hash) - } -} - -impl Hash { - pub fn try_vec_from_array(array: js_sys::Array) -> Result, workflow_wasm::error::Error> { - let mut list = vec![]; - for item in array.iter() { - list.push(item.try_into()?); - } - - Ok(list) + )) + }) } } diff --git a/crypto/txscript/src/lib.rs b/crypto/txscript/src/lib.rs index 75559cbde..77cef45bc 100644 --- a/crypto/txscript/src/lib.rs +++ b/crypto/txscript/src/lib.rs @@ -441,7 +441,7 @@ impl<'a, T: VerifiableTransaction> TxScriptEngine<'a, T> { let pk = secp256k1::XOnlyPublicKey::from_slice(key).map_err(TxScriptError::InvalidSignature)?; let sig = secp256k1::schnorr::Signature::from_slice(sig).map_err(TxScriptError::InvalidSignature)?; let sig_hash = calc_schnorr_signature_hash(tx, id, hash_type, self.reused_values); - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice()).unwrap(); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); let sig_cache_key = SigCacheKey { signature: Signature::Secp256k1(sig), pub_key: PublicKey::Schnorr(pk), message: msg }; @@ -476,7 +476,7 @@ impl<'a, T: VerifiableTransaction> TxScriptEngine<'a, T> { let pk = secp256k1::PublicKey::from_slice(key).map_err(TxScriptError::InvalidSignature)?; let sig = secp256k1::ecdsa::Signature::from_compact(sig).map_err(TxScriptError::InvalidSignature)?; let sig_hash = calc_ecdsa_signature_hash(tx, id, hash_type, self.reused_values); - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice()).unwrap(); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); let sig_cache_key = SigCacheKey { signature: Signature::Ecdsa(sig), pub_key: PublicKey::Ecdsa(pk), message: msg }; match self.sig_cache.get(&sig_cache_key) { diff --git a/crypto/txscript/src/standard/multisig.rs b/crypto/txscript/src/standard/multisig.rs index 28d9173d5..79c74c7b3 100644 --- a/crypto/txscript/src/standard/multisig.rs +++ b/crypto/txscript/src/standard/multisig.rs @@ -81,27 +81,27 @@ mod tests { tx::*, }; use rand::thread_rng; - use secp256k1::KeyPair; + use secp256k1::Keypair; use std::{iter, iter::empty}; struct Input { - kp: KeyPair, + kp: Keypair, required: bool, sign: bool, } - fn kp() -> [KeyPair; 3] { - let kp1 = KeyPair::from_seckey_slice( + fn kp() -> [Keypair; 3] { + let kp1 = Keypair::from_seckey_slice( secp256k1::SECP256K1, hex::decode("1d99c236b1f37b3b845336e6c568ba37e9ced4769d83b7a096eec446b940d160").unwrap().as_slice(), ) .unwrap(); - let kp2 = KeyPair::from_seckey_slice( + let kp2 = Keypair::from_seckey_slice( secp256k1::SECP256K1, hex::decode("349ca0c824948fed8c2c568ce205e9d9be4468ef099cad76e3e5ec918954aca4").unwrap().as_slice(), ) .unwrap(); - let kp3 = KeyPair::new(secp256k1::SECP256K1, &mut thread_rng()); + let kp3 = Keypair::new(secp256k1::SECP256K1, &mut thread_rng()); [kp1, kp2, kp3] } @@ -160,7 +160,7 @@ mod tests { } else { calc_ecdsa_signature_hash(&tx.as_verifiable(), 0, SIG_HASH_ALL, &mut reused_values) }; - let msg = secp256k1::Message::from_slice(sig_hash.as_bytes().as_slice()).unwrap(); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); let signatures: Vec<_> = inputs .iter() .filter(|input| input.sign) diff --git a/kos/.gitignore b/kos/.gitignore deleted file mode 100644 index b02305c0a..000000000 --- a/kos/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.DS_Store -target -Cargo.lock -analyzer-target diff --git a/kos/Cargo.toml b/kos/Cargo.toml deleted file mode 100644 index a808a7603..000000000 --- a/kos/Cargo.toml +++ /dev/null @@ -1,68 +0,0 @@ -[package] -name = "kaspa-os" -description = "Kaspa Node & Wallet Manager" -# please keep this version detached from the workspace -rust-version.workspace = true -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -include = [ - "src/**/*.rs", - "src/**/*.txt", - "src/**/*.css", - "Cargo.toml", - "Cargo.lock", -] - -[features] -metrics = [] -default = ["metrics"] - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -async-trait.workspace = true -borsh.workspace = true -cfg-if.workspace = true -downcast.workspace = true -futures.workspace = true -js-sys.workspace = true -kaspa-cli.workspace = true -kaspa-consensus-core.workspace = true -kaspa-core.workspace = true -kaspa-daemon.workspace = true -kaspa-metrics-core.workspace = true -kaspa-rpc-core.workspace = true -kaspa-wallet-core.workspace = true -nw-sys.workspace = true -regex.workspace = true -serde_json.workspace = true -serde.workspace = true -thiserror.workspace = true -wasm-bindgen-futures.workspace = true -wasm-bindgen.workspace = true -workflow-core.workspace = true -workflow-d3.workspace = true -workflow-dom.workspace = true -workflow-log.workspace = true -workflow-nw.workspace = true -workflow-terminal.workspace = true -workflow-wasm.workspace = true - -[dependencies.web-sys] -workspace = true -features = [ - 'console', - 'Document', - 'Window', - 'HtmlElement', - 'CustomEvent', - # 'DomRect', - # 'MouseEvent', - # 'HtmlCanvasElement', - # 'CanvasRenderingContext2d' -] - diff --git a/kos/README.md b/kos/README.md deleted file mode 100644 index deb7feb0e..000000000 --- a/kos/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# `KOS` - -An integrated desktop application that provides a local Kaspa node instance, a CPU miner (for testnet) -and the Wallet subsystem based on the Rusty Kaspa project. This application is written in Rust and integrates -the Rusty Kaspa framework. - -This application is compatible with Windows, MacOS and Linux desktop environments. - -Please note that this project is comprised of 3 top-level components: -- `/kos` - desktop application environment (can run as a dekstop application) -- `/cli` - native terminal environment (can run from a command line) -- `/wallet/wasm` - web application (can run in the browser) - -All three components listed above are the same application written in Rust, the only -difference is that the `cli` and `wallet` can not run `kaspad` (but can connect remotely to it). -Also, `cli` and `kos` store all wallet data on the filesystem and are interchangeable, -while the `web wallet` stores the wallet data in your browser (associated and locked by your browser -to the domain name where the wallet is running). - -## Dependencies - -- cargo nw: `cargo install cargo-nw` (`cargo nw --help` for more info) - -Cargo NW is a tool for running and deploying NWJS applications. - -Please make sure that you have the latest Rust version by running `rustup update` and the latest -`wasm-pack` by running `cargo install wasm-pack`. - -If you have not previously setup Rusty Kaspa development environment, please follow the README -in the root of this project. - -## Repositories - -To use KOS on the testnet, you need to clone rusty-kaspa and cpu-miner repositories as follows. - -``` -mkdir kaspa -cd kaspa -git clone -b kos https://github.com/aspectron/rusty-kaspa -git clone https://github.com/aspectron/kaspa-cpu-miner -``` -(please note that you are cloning `kos` branch from `aspectron/rusty-kaspa` repository until KOS is merged into `kaspanet/master`) - -## Building - -Please note that to run this application in the development environment -you need to build Rusty Kaspa and CPU Miner. You can do that as follows: - -``` -cd rusty-kaspa -cargo build --bin kaspad --release -cd ../kaspa-cpu-miner -cargo build --release -cd ../rusty-kaspa/kos -``` - - -### Release -``` -cd rusty-kaspa/kos -./build -``` -### Debug -``` -cd rusty-kaspa/kos -./build --dev -``` - -## Running - -- regular: `cargo nw run` -- with NWJS SDK: `cargo nw run --sdk` -- using local cargo-nw: `cargo run -- nw ../rusty-kaspa/kos run --sdk` - -The `--sdk` option allows you to bring up the development console which -gives you access to application log outputs (using `log_info!` and related macros). - - -## KOS and CLI - -NOTE: `/kos` and `/cli` are practically identical. KOS runs in a desktop environment -powered by NWJS and automated via `cargo-nw`. CLI runs in a command-line environment. -KOS is capable of running and managing Rusty Kaspa node (`kaspad`) as a child process as -well as (during initial alpha stages) a CPU miner. - -KOS will auto-detect Kaspa node and CPU miner applications by looking at `../target/release/kaspad` and `../../kaspa-cpu-miner/target/release/kaspa-cpu-miner` - -You can also use `node select` and `miner select` commands to select release or debug versions or supply a custom path as a 3rd argument to these commands. - -## Basic Guide - -Once you power up KOS you can do the following: - -Before you do anything else, enter: `network testnet-10` - specify that you want to interface with `testnet-10`. - -`node start` - start the kaspa node; you will need to wait for the node to sync - -`node mute` - toggles node log output - -`wallet create` - create a local wallet; You can create multiple wallets by specifying a name after the create command. For example: `wallet create alpha` will create a wallet `alpha` which can later be opened with `open alpha`. The default name for the wallet is `kaspa`. Each named wallet is stored in a separate wallet file. (you can not currently rename the wallet once created) - -`account create bip32` - creates a new account in the currently opened wallet. You can give a name to the account by supplying it during creation as follows: `account create bip32 personal` or `account create bip32 business`. This will create an account named `personal` or `business`. Account names can be later used as shorthand when transferring funds between accounts or selecting them. - -Once you are synced and have the miner operational, make sure you have a wallet open with a selected account and use `miner start`. This will launch the miner and start mining to your selected account. If your hashrate is in Kh/s, it may take a while to find a block. - -Once you have received some TKAS, you can test sending it by doing `transfer p 10`. The letter `p` is for `personal` - when using account names you can use first set of letters of the account name or its id. If more than one account matches your supplied prefix, you will be asked to be more specific. - -Use `list` to see your accounts and their balances. - -You can click on any address to copy it to the clipboard - -Use `mute` to toggle visibility of internal framework events. Applications integrating with this framework will receive these events and will be able to update UI accordingly. - -Use `send
10` to send funds to someone else. - -Use `guide` for an internal guide that provides additional information about supported commands. - diff --git a/kos/app/index.html b/kos/app/index.html deleted file mode 100644 index 4701d70cd..000000000 --- a/kos/app/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Kaspa OS - - - - - - diff --git a/kos/app/metrics.html b/kos/app/metrics.html deleted file mode 100644 index 55c6e1320..000000000 --- a/kos/app/metrics.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - Kaspa OS - Metrics - - - - - - diff --git a/kos/build b/kos/build deleted file mode 100755 index 1995e0101..000000000 --- a/kos/build +++ /dev/null @@ -1,3 +0,0 @@ -cargo fmt --all - -wasm-pack build --target web --out-name kaspa --out-dir app/wasm $@ diff --git a/kos/build.ps1 b/kos/build.ps1 deleted file mode 100644 index 604ff73d1..000000000 --- a/kos/build.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -cargo fmt --all - -if ($args.Contains("--dev")) { - & "wasm-pack" build --dev --target web --out-name kaspa --out-dir app/wasm -} else { - & "wasm-pack" build --target web --out-name kaspa --out-dir app/wasm -} diff --git a/kos/index.js b/kos/index.js deleted file mode 100644 index 08ce684e0..000000000 --- a/kos/index.js +++ /dev/null @@ -1,11 +0,0 @@ -false && process.versions["nw-flavor"] === "sdk" && chrome.developerPrivate.openDevTools({ - renderViewId: -1, - renderProcessId: -1, - extensionId: chrome.runtime.id, -}); - -(async()=>{ - window.kaspa = await import('/app/wasm/kaspa.js'); - const wasm = await window.kaspa.default('/app/wasm/kaspa_bg.wasm'); - await window.kaspa.init_core(); -})(); diff --git a/kos/linux-deps.sh b/kos/linux-deps.sh deleted file mode 100644 index 5df71d0bd..000000000 --- a/kos/linux-deps.sh +++ /dev/null @@ -1,2 +0,0 @@ -# ubuntu dependencies needed for NWJS binaries -sudo apt install libnss3-dev libasound2 diff --git a/kos/nw.toml b/kos/nw.toml deleted file mode 100644 index 5b5895043..000000000 --- a/kos/nw.toml +++ /dev/null @@ -1,65 +0,0 @@ -[application] -name = "kaspa-os" -version = "Cargo.toml::package.version" -title = "Kaspa OS" -organization = "Kaspa Contributors" - -[description] -short = "Kaspa Desktop OS" -long = """ -Kaspa Desktop OS -"""" - -[package] -root = ".." -# resources = "resources/setup" -# exclude = ["resources/setup"] -exclude = [{ glob = ["{src/*,target/*,test/*,resources/setup/*,*.lock,*.toml,.git*,build*}"] }] -output = "../setup" -use-app-nw = true -update-package-json = true -build = [{ WASM = { name = "kaspa", outdir = "app/wasm" }}] - -[nwjs] -version = "0.77.0" -windows = "0.72.0" -ffmpeg = false - -[macos-disk-image] -# window_caption_height = 60 -# icon_size = 72 -# window_position = [200,200] -# window_size = [485,330] -application_icon_position = [100,178] -system_applications_folder_position = [385,178] - -[windows] -uuid = "9464f462-db23-4f78-a027-c864db698121" -group = "Kaspa" -# run_on_startup = "everyone" -run_after_setup = true - -# [languages] -# languages = ["english"] - -[firewall] -application = { direction = "in+out" } -rules = [ - { name = "Kaspad", program = "bin\\kaspad.exe", direction="in+out" } -] - -[[action]] -stage = "build" -name = "building rusty kaspa daemon (bin/kaspad)" -items = [ - { run = { cmd = "cargo build --release --bin kaspad", cwd = ".." } }, - { copy = { file = "../target/release/kaspad$EXE", to = "$TARGET/bin/" } }, -] - -[[action]] -stage = "build" -name = "building kaspa CPU miner (bin/kaspa-cpu-miner)" -items = [ - { run = { cmd = "cargo build --release", cwd = "../../kaspa-cpu-miner" } }, - { copy = { file = "../../kaspa-cpu-miner/target/release/kaspa-miner$EXE", to = "$TARGET/bin/kaspa-cpu-miner$EXE" } }, -] diff --git a/kos/package b/kos/package deleted file mode 100755 index 17ec81cc9..000000000 --- a/kos/package +++ /dev/null @@ -1,24 +0,0 @@ - -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - $INSTALLER = "snap" -elif [[ "$OSTYPE" == "darwin"* ]]; then - $INSTALLER = "dmg" -elif [[ "$OSTYPE" == "cygwin" ]]; then - $INSTALLER = "innosetup" -elif [[ "$OSTYPE" == "msys" ]]; then - $INSTALLER = "innosetup" -elif [[ "$OSTYPE" == "win32" ]]; then - $INSTALLER = "innosetup" -elif [[ "$OS" == "Windows"* ]]; then - $INSTALLER = "innosetup" -elif [[ "$OSTYPE" == "freebsd"* ]]; then - echo "FreeBSD is not supported" -else - echo "Unknown operating system" -fi - -if [ "$1" = "--dev" ]; then - cargo nw build --sdk $INSTALLER -else - cargo nw build $INSTALLER -fi diff --git a/kos/package.json b/kos/package.json deleted file mode 100644 index 0a6affa60..000000000 --- a/kos/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "kaspa-os", - "main": "index.js", - "description": "Kaspa OS", - "version": "0.1.0", - "chromium-args": "--disable-background-timer-throttling" -} \ No newline at end of file diff --git a/kos/package.ps1 b/kos/package.ps1 deleted file mode 100644 index edf4918f0..000000000 --- a/kos/package.ps1 +++ /dev/null @@ -1,6 +0,0 @@ - -if ($args.Contains("--dev")) { - & "cargo nw build --sdk innosetup" -} else { - & "cargo nw build innosetup" -} diff --git a/kos/resources/icons/tray-icon@2x.png b/kos/resources/icons/tray-icon@2x.png deleted file mode 100644 index efc89ec407aba9db2950d8a30ee8c8797310e3c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3046 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
r@edVpcs%YhNmP5thrNFsEZD#PCU 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 diff --git a/kos/resources/setup/document.png b/kos/resources/setup/document.png deleted file mode 100644 index 6a8d7dfbfb1cf663ffd49250a6d2cb6c25248b88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76932 zcmbTd3p~^B8$WEu6tjqGGfI<)WHyHlg$Siksf0o{a~Q@POPQ8ajnVXRkOE(&Bv!$|KXGchOz_WYn*DLQgoK=u_y;0!lY^9ykiOvW;u`LHV85wP zNYD;%QphRtj_4p7SS=x8ZV^rM_6a11tDhoM{DXI5rfTXi>i(pim}5ogLg=V1poXxVL(A5H*-?8od*Em#EV1*t% zCOF(XS}&Nc@t+-R$aJ4De_FVINU*wiNAFW15#c*AfY{$$1kwJxZ7}_hq=3xyqrGYR z20O4~u2vS3eEz$R77<2WIh^F9Po|QC$id-su-4$eYiWKV;URRtkpG*l|NGzn(*Z!b z2M+xAjQ=GqK|%jLf*!sn60q?{ApgtNbe9+!S^p539ug7eL*5e!$kY&XLo>AwBYTI3 zgt>%-Q2%pR&i^4<9f#k6SJyt^?c*OTW@6j_@Cez)JDj`|1MUs4hc(f|8oS^OOpT08 z@mO6f-V}@dz4Sl`$=^5Te_m=}=wg5aD@=`u|I<>SHzekw*47&sVEhhL%MzyZ^}!SrzNU?1{co1GZI^$vf3k|~);G9erI z;Pi~Kc(R@$7LU~lynPkxhIIO$gq2qPf1<9Kb{BzrX$7NdJF-;);@*+WU(si1~BwVRGnyDNtY0OH*$j zvAylY_=pvaOv3#6v;Y6^8~MF$lph&v`oB@X-^=JBzTr{cVPq=`xV`@u3a`J}xW+heYn^P@MCMbXM1V3@AH;~(HppdRYowB2i8zPKU* zkwonnl?v-qy-M4i1zjCVQ|CpqQiG^1onCu;uAY8S{P9fDzh|eu)N2H{=-sagoO-Z4 zXA@;-^ZQd#k^JXFjZysb5zS)$d`gG+{8>k8x%6ipKVt*&QfANGNfQ0~q>T*a3@*>e zPY|u%ly}X03ftk}EK|E1hlJgehhx=IRApNj9GhpHznW7K)o}bl&cCag@{Ezv_OK!= zH5SPkjpQxKQ;I_!ZICb|%uoKN*P{)IM33sp{aT}b7g8l1m9g7d|9VZR5YHHMyf2hX zcFH292W`Av5I;?DFx9k@lgPmKG0hJq0s<{=wU(?8S!ynoZuoL$ZJKSqu}>=>^;B3d zi^z<>FLfq~T`q|@YOBV2VO^NbVXjlvgZ>)Y{$AMsHfvvr&yeaRS_w4YYht5m5Mwam z31y7bH;GW)2E`ZQI|$#yS!}7YBKon{uJ>}->nA5v=&2n}G)gP@_2hZ8GrUt@!f~<1 zf!k9#Z~Kdj4c-$F-R}&J2E6XB%nuY83;)sXXq~P~>*pSlzx4$b!FZQmmY+DNxa(Mb z7&I_prYgF!aX4BXQA;=Qbuo1UWXL9!mlw4|=i*?}i=UQ)WS~|Y#cCWOEULdMA2YVc%0Q(tGX0pFQG=(Dtl8GEKInwN~q8*Fsk3VS_| zLp={0a4DcL<}?$mar5&oeYC%B?)?OX(}Y3@{`;Nm52B_=1QItUT6J1-z0T;>EK!g% zamx;MW^@1i>AqH#!CZ+G8?%Uik*F+Lvky`Tv~nz)x2jC<-%qz6A456u{9#XRKVttO zZP}%dJ}L7ty*=zIG>B^=>rfQ>aYpi}9qm;$y&21GSlC zQq}8omPSl$IuA+OM_B7^2X&BFO{3kr@Uow~DuCH~g!a@h@-nB2S5=4uHlx)b-lxg` zj4c*Ui;}k6=hK?l@rD*9Gx<>RaMN)V5!i=LeQ`QHMEV8XERf*A9jz`oh^$F+RPrjq zexnldC0;vxFu5SrEsD3!;l>UM%7tG}_|%4uxK%#%5g1{6Ol5Rkshr@%)(BPt?=ywq zs`5Qtvfg`Cn;t^mf{#qYCFwN0`C!nj`VX>j`(+L@$C5_5A36D1N;WDZV06+kFP!ou za&l6tl-N>UcE}73nbc1mR!OrP^t-UrdUR>+W^w?I)dp80?I}8k_8U@pnN^<`h zpRf7#+MwmD*XDF3-swB^HD<-a?wSdi6ib*JlbWazwzT*<$Ei|Y4V)Kt`I!C}TBs&3qQT(C1?BZnhDs@pR@y)o$w1GWlf zTsp%h#ooN8YK{sl3ib5ibV}JL0*ArT0zM^uP8($itLMV+9E6P#pGpgSDHKLA6tS*k z(+5Z+Twp}orhKHpa23OBNTq&IGXvdZXrMzq?i5Vg?JGLeH5BP~cRq9=(nfTSP+h88b&zQBlHUbS_IjtkzgY zmhxcK9K{cQiqJ?}Pvg=DBN9~^xNFz?xr#}>biz$t@FY+Wx+!cJz@OObzDE2rH- zfzSXM)vbz$!U#Y*b`?sa-6&TEM2ihY@G_BJ3}}-&6c)}|mlP3|c%(%h6_6BZ8d?&3 zuZB&%QW{#MmzuYSL|qpye-&tzrVEG{LgRv1&HJ0Nl8M~5+oAS|CJ2b3 zNU=a^NCU(xa5pCDN@^8#yRVN^VvAbt1kY#fetpA}f(j`pz7VS|FROmD!)OfnoTho5 zG(IU*NOQztg~c>oxlx^@meag{i3S5(OhV?8r6$`&ev+Fa<}XoW4bjhh2WdD&xvda} zKmsKl)DoL^D}(ARlZ*mKZYLAkRL5h1=1XLw&wZ|v=MkV)MCgkpH?-%mts7`Jc%a zACOU$N-gxQy*xD#s;Q&Ex!GIAhc`fgg=jN&fIy<4xlxo+nw|9zWxW~O!C};`@1N!> zQg2%(^a=#n=oY992VZJsYa`?l8;DJWhi^Y;(2hJF))|CDCDWkRgEngO(HESKu)qJy zx~$SY#hVHC9dkB=#~UTdCk^$bB`5O})&Z@5k`7!l^}5KFPfKm+=;B^l@0KAG7!31u z^mc}gWx2~}U$Yu;P*to_;NZLGG0?RXW!J~qRn^c;Cvt9%dw|K+!77@qs8X*X3HMwV zy@}7e-RWw=1qM|`10035YchH2s3XQlugzqQnFmA+m0hwey!Lqy<$+*AC#Qu0HL#Wj zdInU7o~VJzezhRG*-~cA)%T+W6wbF)s=PDkp|;j(f*AFb_i^{Q7Tma5&{H(?8UAeR zpmU&VxcH!$dGSH@Vr?C6i^_r)oJkR~4Bk6Da$&c?bJ2bNx*Dr2VX8E!jH*3swIW+e zhzgFq(dW!9PAh9!i&S91;n3H%yq~(G(2S~}EAuXl8cCLFCqo06uYA&Vu__LA0|Om8 zAIhYL_f$6a8;GI}gk_QMHb(v}#eA6ucYM$25s*Dw0_2ltwA*+`EAQV)k204tMttjHJ`#Al{{K!F$qPoNhdNo0c8&6g6_SY$UJlJi)@5c9r#rb^tdabt| zxoJO7%YCqd$B$sFSaR%MK(~+$MbWso2eIQN9~{2=rn2uh?z<5a%h+J%LF{EfP%DB- zOy(b3u^WEu-f+{qa|Ak;%jgqgX{U<%>LT$|QS-V?GgIN|8^rX!vzxl8BaC?heq`Qm${$=BeWrrZ7P?FLf?gbamooHI+F{U1+l~CcWA{r zYH%7?UX^BJvt#?{Q-!=h$M>q@6eu_TiCcL$Hiz)E?MHj(J+meY51CKbSgnS<{!RtO z^dKNC36{rab9jr|E1&-5`&mHc7X(v8l)c z#%S!rfTqSnnkuV}atXaZ^3SlAfkK; zlYp*DvBOgw@e-;|b4bs<+h!l#&yZXNd!?<8!K^tvvW3}*GU!kvoiz80QM|$^-#T5x zeT<5gs)Q;kqT~RIuI^OX;#1!rb0%rY5dSSwsYZA7AmqnL-wI1%5Lbqu&|fqbf$OdO zKzp@+05If-+m)WW{81(DGffh(0@z7k-EWEG!$hu-Z;sEl`wq_Bn0xs-qtG^~Qyoom z6tg2q=celGmE(Y3|e zt<%Mw^F^k#IAEVcl0c*^luW6W&-EMu+04$vz}`HddKfL!bSuD8vRfSuh=c&dxMV5} zcuY1T7{atzuMcQ_i+Q7UQSgha)>PTU83YuGD<>#CY(3{b$-^io`qw}FUZlEdn+k}U zK+=fz;*bE+ep7ls`5*^2d0c;9FR6EXUk^7F*h_knCRo)3;Mf2uy^(lg?!Zrz#UEqa z!||DWv`n>7K;25$p}Z_<$t7i(F`xInDIe*EcIWh+D4MB>==&r_P(BklNzoR>7A7xi zoOo=9k?I&F_fyMrkTR2jB*mx|iXdD`?bt`5JR=uEgBWjP1RgEqyK^(a1CyQeB%rtO zQg9)oK$A*|0(F~0IrF;A=$6>=R)hj>g^g5Vn@S!wXAUr+A_*w-#GKyaz2Fz-QHaoL}|Zuwp-=pR}q5#y)mbBX?uJeWKIW!DHyb6A=9}j z1mur8ZO7-lth#e8rYG%V@7kPToS(c{x)`X4P(-6{ota%|Y;liD%UFD?@-3uA@EYD3 zui~emD1w29*Q7%bhA=c_Bqwmwlm4j*@{UWw07fxz1f>f&1%}nYx_51looEEYrd{jj zzZcEyu(KhJgW{gKO=`SV)ZK5CuS&<>Y{@n6o&9I5O^VL|Mu5p~hKz1uz7@3xiTb(x z=$GNALRw=Eg(E_wc4e{ZsPe7AEoCTyFlQP*CV51E?rD$4@=s=CM3L2s{_$O#0mkWi zoxVT3j_{bz+g5j!e6BWoAzbi?NR`k7E(%C?NELNOUZnv&xUNjk#dp>i z>WFh3r<^QtqrVYu4Kd`^ATk#(9qfC2Eq*fww*aW~v;*m{DfXR{c~y4wX$!iw$(+1q zn+geRy7ls$X8Z9m(t6n>@4mOB?GxzPCpvRac+nw&-8bYF(^G)y#7H1)#v<@7^)Uk; z4N>w}E$|>zfB|_I)yH?{8DU-Bf}?-!`|?O;SD0s0-I(vEb@*EmNJ$NK4GhjoEbu%8 zwqthS?#GaAIXjQ|8li#BV%)PqT4S22Z~WY6#{ybuaoDZT;wdk#m8S@ZvIpFT*uzo{ zWXyxM&fE(g(WJ{ewZKGRM@;Fz#6C;LgKW_z+}}J6{6ts#^z)MgK)byH7cax*K>!XE zbT&rl-{a_85Ei_h^Gv_f{E5U(1`t)n5U_WKl~H!IFt&y6#>`Leog^*AgzxGrK|CTp z1z>j@tlC>tuZ>Lee0F9muI5(EDO#l06@Xvq`ZRTv7K+)3uVL#l>7%FXr?dupD|lbh zlKJx~VDAk0)q3H_kSs^@pz2|RGCh4gwLnRNI5*{QAb1`!?Z^>?<90Z(d*tLZk4)b3 zPhCTaey>eKDyAEN-GiDSNjh8)3;L_c^yM~>7Q8!?>MT~R*$9A;TBsKS^5K-0NJF7* zAM?BIx!POvcoT%AooWEk&N*@7X`l`Tc}R5Qp$7L6x04>^NF$&eKxq*qCR|x~!$B`D zWXD2EYF`}9a8@qgYm+W82XkOgM{Oae**ne9aj&Q?hflLdUD#?Xa>lGlqT4bY1rw9d z9>H#qEB@+8o%uvs%uE+Ok+icJI&~4KG>D8o?hhZyP+9KWsj+m9_JAZtLtx|};alrf zV%R%f)RIcaOn0I#%%d#7f7AAgn6oeXEYx<)Czwt*>1coqVUG=|WgjfHJ`G4ZBu1n@GkcIp3PkO%Xq91;o@6s&2iGbN7Curfzv1{mF15 z4~PwbFD9ECPrAa8Fx%i(ORkFOl>J24#+l{Jd+r&bBfk6kse&R6NG;tjLYqKF__5_V zIg;PvK(Py2x!+2l8l*gdxi6)`;$-j~ZRhmVl>WkkF)dI4LT3{oWn2J$AH|zIX3|=* z^c4Vij$nrgU#uF0-!w}q!#Sw6WQk3tZD`L=Uda^!@C>S?lGb zcGOdG2mn-O6lk(#|Dzp-vEyNbkECUa$)QAb$p!Y%=i7GphEy8@lDoebkmY z)gMLTP#1HJ2iS~nj#=FIW;yCf)3Z%F3(Gml-yY-s2D%~z!c&4cbT{OY6Xs?YG725Z zxyOi1-pa-79kFCMri%{NBbZ@3XDXYk&c2y%Et(kx`d@Vkz|dYx#%Fe8ZtzINjk(18 z8Jchv;I(Wjk_Dh{U~4gxb-&T)CgYfJ_;899ZP&GaIs(Khv1;NYi>$QfuIH35Tbj&W zkoZLvi)DQVt3!~QkP&h-DCVeqT(`=y4tmPgW=I_0*x19$tpd#hOlRIc=ubGzE|k{V zvU1cKUCz1wuWzD1r}4fEp2cqQ{bV8gtt&P37Ko?$$AG@N($!I4*Iw!SDc=k0oX8U$S^EJ_*s%O%zP9>q^)^g0;28&tA{7j%Z6PnCn)85LcF$>qq zFLKy-Qh+&9HYlt)>h`DqOk7$nhK3xMTRauGh-NVXXD4lJct?E{`rZOJpRtfJ8je3^ z)+aVe{;^dGaunw+Fha-&ldt1W%8SQ+7LWSGmWF}-fFNB%L9FPr!}M}iV9aoH^3-KS zgxIlyB0+p7p=jKLc&{pqv?TbsFLKZ5Wwl-Ge&Zw+MNtO0@U&`FZcW>CMe*EqYIG11 zTsH_=pyl!P1BV)-$n);6HI`S;6Z;wfSSf=rlEQ%IOZr~(EW^#3z}LQy;*6i~5PK95 z2KXw#O%uQTLq1vIMwb|kd$exRQ7t+IcTZJO1Zo_mpVxY|<@i?x*nJ84NSuAfNT7mL z1ZZVaBV?2n>^tf*koYlrpAwX{*mzGXLlXaB3#JS>O}-Zpo)3g`$NQ z-`kKm@P61VH^0i5+L@3OYZhPEn0<2+#H{H92(unM@(!unk#cO!+Q(y>xyl@zxYUB# za8WwBKIMf#-Yn3Q?Y}V8{&x9`_OxdK@Jc%1e^21-ZF~Ew9Y=`21miL(zf#4y1jsn= z!AVTXkrB7tCw)t-=Hp1%dWQS8nI}|^rA@{4D)H51qgoobox2ObUg1R$!x1$gNF8Iwn8^P5eO(&A%yY0!M`Hsg-jAbL?;S35t> z+;w(PG%w){O-tU-0lYXjNer^h(6tfO!_qLZ&&3S3O3;y8a*q&NCKsDh&U-$FZW6*q zlKE1AnV3;aV?HnbWem?{U>L9WI2o~mi#SD4a7nSQ&6dR7G#RWi#6PUh>v{ zSUbpfKt3`j|L7h1Wh51+&wt;qOd_9E>}~Q2_EqT zkLZI`5!l4+p)V#=kABG~MyzD-m{KE5C8?8(^49CFewnM^s~>oC2cDAW*z!6gFG z)iyR)#kTU}qAP}9HLd7rkq)GZ(6l}>HTreR%Ukr4p-qi~nx4uvehTSN)&eySQqIea zo=HpGG)5^76SGsXM&^MaKLfq=DR#EuyX~@xoyM{$3W>pqV~s0SR*5f|jfZ{RKACsT z4pFZFfL0Xkx@yG;(3u(gU}}KZ&Uq02Dei9&qJT8KR0N)J-jno8`uVBa{+cr>FMw(Q zqtXLD^Qx5v{X_^~|B>A}GeTWk)8cmtwB>FaV^E#x>r}Rx(A-{G;9a+rx?mL`wA(?s=xSq0)d4lWoR10#!aQTsuf-ylAs5XL+ zW2jJ@BXabu$gFysV=R46zy7DnU`k;@+uT2enQBS~90b1cLJJr)P#NPI9}&sXd4<+bgeVGHospc6J4vx<%siRU|G0+K#9&$2^{OPnW(*d>?_=Ef-%9W;BIvdqX5j$hlFrmUF-SeDC8_i%B#l zEO!rxR{yz4f1^HA@hUOIg!xgd0O@k-s4c9qi7vCRb@aHV2s^p8(*{XDRaI<;o&#}i zsItNpXZtVYer}xW5&Kv0z?*nuuFJ>|ojE7;;F@f%eg&^6Et#_h1c$ll*p z%2kq72?}}tXnsR?bI`sIC#Guap7uV5eviaqX4Qd7p=2Z*gHJ_7j6Lo-IJcFxQ}kO3 zp;zNo^TH$b-#?G|w0Qh3{*33V5J?#|H=7|2c!+Jv%_EP--(LQ@zoo-qgwV7ixzHQL zCUx%Jntb0eYOT^3yfyAzW+*FXWS-%`!1VmRfYMqC%Q#l*HSV8v?K3a}$uW%Szq zZ24+*d@nWC;z1u0s3a(lF`%%XbAYVIu44fwyO83v7!a4etenYlVWNDG7O zSCuSz9DZ__{c~@fIesU1<$6Ut(C8?+-&p>uvAllu_sqr81}#-Ln~L;g{sp=Zk?=Sx zaG@BK-d9|eP3`VL&VX6qr_Rvg>cXKjWixw!uQX6c8$#R$74htBhtVT<=_!zvyX7B0 z&iE8QAR;W2cFx9C@IsCVO)*`THl&wNVMlxe3jPf|&CdK6_xL{U%vCF6F{atn7Ify( ze#q(RZ}X~3@K0LK=W?p~ko!&IzTYH;PMG|Wd&|DCv+;3FEP3%;h zKbv$*Vo4=kZ#$^9!SBgSmf&VbzieE{E?nCx1zQR7Fl*cxNUx~(F-@Otzn960i!q#C zl5j~AR~*<9MOL4`j-u-#r`mdP+0j*KgBAFjZ6k3R&7vpyGuaOJWG9vh8)HHOsp_S# zfa|uD%m}Fac2$14K5g!As`7898wNT3JP-v%yqI_&_ka>|LO0q3ydZ{r{oq z2b#QnAS5>!!MAL+>xITiiXg#g60va_sw^dEuw&sh-a$V9?kuzU?>34%vv zcN*1wt-2W18NaM{57-e{e#$b?Hc(7ZG2+*ov!nO9Pk(PeV?!zNnCsEP{7j1XG?+(X zlz>ESRhVx-x4t{~Wl}LDH!vmvOgdORnI1YrQ#zP;L*F}sbPu=6_ zLid}AK^v3Z5-)kWOs|*Yy$p&ThP-7qt0LTHtE~;`ni1@78TS3``5cW;yLx8tZ56j? zKxnYQ!JD#=n4COnW|pbql=iFtUY~Oxkmkf58)HvSU`)Lxda3n%nXAl-w(&sY)JtdX z0!Td0*7twQ{bhOuVn-5ywec$iX~cExdlR)@cxPpET&*fL`3C*nzAqt?FLO@(ym63$ z+l=c3Wab%Y=Tt2uZYcWuc~Dzegqj$56xpkST|f=wnBAw)e0t^3ceg=dpI0)gFquDc zZ$u^dIjJG8n6Y5G;+Sy;s(jdq(WXr5kX?A$fw169pjXC9*Co%YCBlVU+oa#f;dArZZXI^SyRdJig=BF25U5|<-- zVZD5?;pG>#B?~U_Z7dM^N|P)DuQ1+ytUDXQ??sec5woc%I$a8C486_-W>@t$>j*Cw zvJG!HsE9w1*P-}U^=pcQmVF)ve@ndLFL?OcnoQ1=U+SQY?m4o?^!+ifWNIPJxNmvlg_#}Hgi zxliK(Ad)XyJAx!vQVmR3Hr@8MsQhml-MDk|{`~O2+@c#AVeO!*u@#WwTieeA1&t=1 zvi#py$7%wiz;OU{XN8!KBCtH4fctR+86)FdrE=+QVwAY5AUeqhc6lwBtt z{mPK^4IcmaU9&;XZs`0aafR0Eb9fYMJ|oBI*;gf*n-VM7l^m;w2_s@Rh+iibSNP7MZK{SWAFe$}Aq2!DErUk~t@AhPHzk{6ddL++cI^vvB;w-}Nhy+Pg78 z<>#+4Rdcsl2z^)M-XbeW0hh7WQUad-SRYI&k(h6fb3&pgCpX7E%lyzKwStL5m{4^~ zZC2rRAv$327%o;D6=BSWfq|Q3PBy6TgytEz7R5Bx-d@(AXMFt4q6eudPatR|_J(db zH6DLUW>*^DS{dY9jE{M=4?AZ~9junza^xZVb)mL z4E+UM-TF~Xk<}wkTzx8cS0KD>1k83PE5o^ z@vn>ie#!Th()%)d1f9J(P&oaw($>m zlFpNau71%4R>jP?5qtkl_frps+X!_qUHc?6(81kx{mV}o3BMf*M(F{f&0(qWto4$W z+McIHhn(H2q9w)U(@Ne|MDpS|dw=v2bRfz&aLa;RI zF-6hr+LCVCV7&Y?TSD$P=-O1Q8inHvWSTrnLYPi9Gid9L!metDe(7Jzpp#KM5;7p|+rRNk0Ez{nR6w*%Zk{ zX+vKU_e#-rjM8gxH}NL`&=lW%wodqbB`U(qzEj0&|4h*`?S6W4dRw0oOs_P^hBP4d zbtb*jFHe9l#ILIQx4@Il^CSyugsCqC?WtU+a^4qyg<|?%mp4Y;(OwP9$1lA4Bfr@Q zO{Pv6s5`4^f&PNzlA5zd*KV7w=OF;Xfbz=M*v3Zv@%x~r-U7;QWu^mmYr`kUW?T3b z3h61%Xcyr`ScA#rqbjmaDEW^(Vw^enA1S&~f5%eP2+<+0rY^ITi4Cf7Q2+*~DESpc zWh?68{p3H?lJX_+DO#$?A5L&)PY!0&MaMy&e;USD_Guj!-Iu`iqzp#kRU=6c zQ~0h1>X4%PfG;MG5esIJ5&Vj`#eiY)F|Fbzp}SLfbX=gSZ3QQpb>kx|_ovtWkzrkp zPo_62AN^s|1B+00tjmn~=Tt*)u%tyqWuH$PRZ|g#M%YlWy|Z_q*2`K3WgdT&WJhHu zmz#n9$iAzJjDXyXjtzj zNg^HELpn@!CcIck90&C_Ggyk&A9zkbaP7@Kj>9p#rRsmVPs-0nJ+=$OX`qm}6ilf< zu1)HL(O0c6YCpb5;+&0&j&Pv@1;6b@MLh0AZhy_B~wAcIsAjsEah?9 z;(@XhbXmK2j2DE8<`-VFbN0s4)d;m47w&i71HImKIpIP;LdUJZ%E;yFI-e^s=ZsB% zTPZ&pw791GcJI&CGQ+pZ6WQWr%_j! zHR9`v{{wR>2&D3_#&stVs4{%+WqG9Ik62XC^s)`J~!%C@3cvmIOMyhKY7qlG@oN8^f{M-h`V(0wBU*) z?=+^(jU{9(zsJt5xh(xDRlL#}0h`#ClMPET`By3P+3%`QGN050gT12;B#g^HQ`4XS zYFmE-fy7OM)+8CJJl!qIcowl%#_hLrAe{pB;Pq~Cd+`Ig?mxfz1m)@BaU)tQk{O6dRV+`yALy%0`t z%qg|s1rkul+3XXJzuXPtm`#@+scnmSmJO_#cM!y)Brxmy;!LAH7;`ZG9V(>jX`+!| zf3i0e6XQfLb|*zpe82+=aOkoD-?vWFu-PqSa6R`Mp)CPbwff}|Ry<(7wS0F6jz zL7vI`*=M%#%&0v}M*{$V*})*OfFj7_Qh2pmy4*@j%n)JBTr>Z&%u~0bP5zjgd$Kq=Z)g?qcLw3Kb+uGin&{nt}VY`&~?Ju9hMkfmEA$T(q~s6}`M zW{Hv>aGK5iBgT58+%Lz)Z23shN$TToy~KygTGF%#h0;kcZyWyr>9MQYLKpm-cvcsb zcIg)fh5iVFa04kyEX@Sekz|)W5M(>+tz~K*Ii+<6J#F-FoLuu70F@T6KYQ9ha$GuQsZxic_q0fUyK+68o z38`c|_MVD?6` znm1U@c0bL|mVLdF1p5ex4LzZbp`hkkAL@92x7BM|LEcl#b+V^cA#MHRA84d|#f&zar6?7`+M$v$xQg58xi-SgK| z2K(S~uHeHzvT|FZ;MSm_UHVp3^Y{E*yc0|E)<&2g(>u+>2AD@v-uQO?r_TfK_kTG- zS()w$izoCE2m}ft&8(2jVJI>cSG#O>PLzZ`aBf>Iy$35R=du~Arh7-adJm~~0=rIE z{P%dPMl?RHp% zP#IvaxrY#yBRG;HaA3r(=}z$oR|Mb+X1GC9NDV};?vb5KoqfjR;xo)`#QL`{lBOE< zI-WH+i3B*sbZwVUKdJcnm3>|E{&ku!?sz#zY8R=)^3|OaEaB3S6^p;1fE}urg|$o8 zgTYUr^`2|7H(~}R6_@8b;+KQ;W}6%n`}lj-ibmkts&FmRDodIpDjati)W1ePXK0zM zoNZr3xoTDzE(Q=O7Ea3aHo!^%An`iNjb2}OH>n>Dx3+qm9Da%Kvi~48uhhBOve{R*NADRV6hTYy%VIU*vTz%NLu-Tayj_-8v z=xSPYHkoVe6!&jpruXS;Goir2khdPS!TUBN=;X{`#m^Vci%L%wPbgT&Q*#})0e)St zNIN4BW>I2%z`G0|+UHZ-7sfTxEzErAT}ueq*BK5W(atzSj#Yt}G1I+fO2= zD@Eju#GRM34<=apL-6@efdi1mpGV!5K94G`x+8u1$(<5L4H$8%B~svPozzrrRX-bA zqgLgL3A+i#?5yH+=KcvD8HN;yYv(Z~sxqvco?V&nktTvM+EU8ytPw z-k6{Io*eglebrB?#q@X$P+W6FOCt@q(fuip2bxi(fa&5`Y2z>H8pTm=50h7>7}i|#M$#Sy z5k9857OX+ymabCv;dSJILD&mXOaVSACG}^Hg^;J1G{H|$5c#Tt7Zh$v(95UX)PA4~ z*E)rZFCrEzw9oY~8$|f>r9gc>qnJ=wZGg8`_(uYb$ND8vCL1(YmH|?%Diof z9H;c4t)|L|1T&2VVi9#R?EPXC%@vuuO(Rxu**I(fHk4Q`*<0 zJ0|xT6|y&D!fs1biQEqPU6QfuiBp&^7G$+ws_5iM!MTyuC?G^)9UQ9S#0tBlaqT6; z@u2fM&|kUqim12U__?nO*3@GrACioM6Fo6q`+eGyUdYM!b~$w`a8}w_Ly{ltQ@kCe z@FW}(o4oGu9#A@eHNZJYtf9EohTa}o_LtoB7>Z5}y}5bKF;ezwp;y6hftu0OD)IXx zK<9lq?a{v1*B%hMY*ts|dMW4@oJs*I8)^jR4|{47&$XmqyjA`9sng%}1Wk3Y!zDU+ z(-=zsaCi>n|8Ys^En>cR%lfjy2HK{j%xVrdDqIPr0D~q&UK1M>z)%5B0pD_VaF1P| ziQAx3B1b=6Pr!J4d%t7<*JeR}rpif?!-Sf6bY@5r&+5bn-uFh?ZM9zU5~3V0V3vE< zYfP|1Du-?AXOyIu@e=jvN!v>I5EWESMeKkcUzK7cskUa^ymYmgpFQA(j+>mlll*x+ zcYE9Abr7Hlr&@Uam*h=?l)KNyHT}f8LG3nX@Yj!81|n%EOz1urs>C&Y4|qMxn3DEP z<0fZpogy3F&N_JzJIfkG0t#MTmp|R~UGLqwCR^`Yj<)D4NgAaIWM*gHt%U+nW0Z0U zcs|*~5MocHKfmuEwtK6cVZP_}r$Gl^8USM`hVR$h`0UV}ur*@dP78BO4+q|;oK>)? z(3X+CSDkVeT<6Z57`NXui?4j162|Y}jj-ElhmwUo8L$cPr%M7J?li-0lu-(X-y}IK zbz$jiu=^P@+;o|3rLRB*EMJ1j(jqxW1XcoG?*VEk z5crvgK1CVKUf7?8WCd%l0w$5I359`oEn*JzY1pNH-=dDH)L3(fB)8hq#H=j!H9KPi z9QU|6kqC^c6b1L|0q|f*)VQXRX!$r(c7_uO&h`+K>i2MHgq$mjx2 zE_Ax43g;dQ)~yU3q`&cE)Lf&%*;{%Pj!%SdvsSOtz?8Dxn4cRDN2bm{8BCwr=^a~5 z8?w4c-JBXpj#BMfk$pdqK$y8IBG$U}A+Sc)`I-Jz$Jyv4ged+4HKMwOcMpfzx z40dIwUSMt>)hU%GP_DBV*cq9EXV0I6`-y#rh~%3HNgCfg9@IiTqCTiE(Supb03(MZ zEihP5PI;UUF4)XPBqQm?ue!LOAcMOCeH1*j0aVtam@&AO4Z?Y-BC~P|36~}9+p;lX z-Ii8BR0Y+`ky1ST&Tii%HFeE(1dI^21EVB}Z_dfuyhU7*#^0!{UcG{=GhSKs+U4-W zm-eUOl;NlL>ML^wK=fC7e>`&)Fs??B&%~p?oY{!q;{R1}+r)c@& zfd-Hk9~CD4>!NY3p$CMSzwZ3?4Ny<;lidHzJmB&i&+PQ#FD2<`nQ0s*|3V;yX(ud? zjWa2`DJC7I!P~}j>niE&wz%wFio0+O=5i|{eV5vy;;#<+P{BO^WNiWB~ z=sTOQU=K@-)k|Nk%*fgX_XSfNC&fAE$}}eHzITAgel>f089DQ_YZ&c9GXH};403V! zqbC}3%VW*AT3+621=R%=j1n(i#|&TOv}SGVCQ@;*c+dcKI5D)jy<)`PZ1>hANIe`+ zQw2bT+JHD(d4n6ABxw&r&X(J3b(WK&gg)s%@Ivq|7-$SX%YoQ2vOlsAYt6fbT8W1m z)w;oRx+tn?=wMh&&Um^fH8uZ+hNm^@JmHG`4`K1=#)sAHQP`h=dq-A!&+(8z7&*b4 zKcBr#_lFpZ3C+L_4Lis`>pnJn3q5$2L*k2UwkAVYyHaD`72n(xIZXljT4 zF^e4_37QOAHP?1t3q3C_D~H6a72MlNuS7lh@L6%&l^I_KyM1+fC;l#gS>6HTRl=8G z^0e3vp_Z~Z+PqmJEw+j;^;v5qlC@~WY%B+^8Fy5bKPkvqH%Cr-$@-bCm^7{k%R(jc zzmYA++5i6iwp!y_{w9qVn5&`UFqfAEh2`A;KWO^yK&t=m|7>x|=7#K%$mNzTdlV_5 zh|G)1-Xm`_T-hUg6lJT7j1af#^pVuwZ*uvI)$sy zR!0n8S-V%<2eE2iwCUw1|Mny16jNlswx8}b9ko(F9QCaG{@%E`V}iX*;t6CnqMB& zT~a=NV*j(J8_1ln8r+|L-fZtES^YN7Cg(eHp=vzh)qPH;$~2)x^5WtCmsv562_jAZ zaq*o!BXG^|28%iLJj{v>NAVUNjfKhFP6=}m9ucyh6Uue$&+B^KmY zPotiEka+~cJwD|ig{z8_DL90|4%=*O1r?uDjM4a-qmg?rL;Bl(qxAV9X{lbeX>S4F z5P<^m61IXmwK+|@q3k%Ayxhxi2d+_w4tDMHL9&~f&ybtMH+BFnP>^Pj@{D90B7U znm^V340HYW-x}e;Nw?Ry4vFVCHmeWnzPih6^5pKq+JqYa(M(?^X1u2>eao8Xy<8{n z%wcxDJC|~3vBz8UFfCIP;uAC9v317jIDBDT@w?_ zQFL|lRLzZgZPMG;=KjCdY58mJqp14Xrmv@9&06H7W(sRRi!90KD_pslBO)xScoY?G zwK4-ej-49}=rr`KEsg|&@kIcHWw*SU7W4=b$@$Rbcv}4+Rv$8}FnOr;dwz&EE zqi=)0eck;rV~*0p9aAK$WF9uVmm7w=K+9~&g9p@U8jE8q_V4(4sdJjbn%C9o+xnPj zLmQ$L2aVaJ*Q;OLJ0JLlBSAz-=|aEfH0Af8Rq(G$Rfkx*+)fJ(moOodSKSfYZ@LA1 z_uHT=V4o>5aQoMLXTMMvUu4Enx)8S3x|jZMwL zyU}9xB}@GNI?kWZ zunli{xAz+6UvMm-Y3MWkl77y2Qd98H2PNMY-`4v?o512^8Jc*Dz~*m4Eq2JNNLQ13 zBI5t)rEGeBVHHzmvXf6#f(-4-2rI-e`NY{mdpz<6d4409e?e0ED)eK&EwuHhg#wVU!Z!y5V=KT%eqg< zbqU|Nr%!&?@lkitPU&{2*HFhjs5zBNXx7LPuNGg_*!(;@c0Rs$u=VAqt(Ub!o~xV4 z)-$%9ymyl*u1K!d#E$%5eSio4%MVF$NfU=@tC|Cr->nFC{_!0y;~}?lPUEH>W&DPn zzse^SbO2YGLKh+xwgQ(wf^Ca4lYS%XMi8r{2x0{W8;#eV**jvm>g?z0}Fw7z;oKE`A2fre|t7e%s^DQPo?`gbnBFGx=Lg8B@>%nKezL1FFTH1URG9p#a&ArKQY$DA#FY_k z%p3C`?U7Eroqr}%vgf<6@zfqM8Tu3P@uSY#;RlO&Q=Wcf)`}eCH7++|C>9&uh?5LP zE}xE2xj&>ND3v#!{FI;V-FP;wBT!Lr=sJvofEA!h#z_49P7g7V z7q`Rqzn1(q))CF6=?R{T(3_ z!YfPJf?HdbB0$@=m8uYcvr1K0phabFxN0vIJZ`-^hAt3rWi%bk;;fNagbFBo@XoB? z5$xo8@lvMN83Mb@4SaEYz#^S=#nzkXui(N%;fia{H}z|~K2-#G_cgY1QrdcxjzZ-G+oL>Ki>x`l_)u=Di{VBg)OvT&_`7`=1R974vKW}#^LzGKI4tVK zM!!cw5~v(nsQRKA_^5FqCtu24Y^+t`qTDr>9L5nsR|0-MnhK9{!_-|USWd%pqV-Du zBmWAsUe5hGm}20a`6@GZA~h2M8qb_>`aS$GW;S|H;b+q#VLCT-S9hfTzUL*>8R#=? z&W<_o-QxZ2zQdOci^P-bYoj;pqDU6=r%E!m>q_cQtesd>0mBe0{mV50CFYq`;%8%# z=Si843o4?bb+sq?lpC<5Bk0w2) zm{r(aGXAGt`s{30FFO2v^8THxuz#yWF6Kxos$F5VZ8zC_Wrx7VzC${(W|`YQhhAR^ z&1Np4avv7H-F0eC*v2v9{6=$yCTmgQ>@7C7oqBYJ-_I{}XT490h*mkEsrM||FyOwc zlT)eiUYUPfc&szYM=8d@ms~%L*hKw*)7@cWhyE+vt5@{HHKo?uf4f*) zZ(wKAIovNfCmCRlv`#s`*v>ota=+(T^A#ZQi0;qk`-BTOJfn!m`cK|sQyrbEV|B2P ziXAOErg5^%vK~NK;D@MVT{^VG%?f z3r;rid*GQ_mNv%ojcFX{0r9u|w4sQuud2jxnBbP~USqjX`Nn#? zEX`vdPBvlB92$?gHoJZy>KWVyD=kB=OKlnz@;WvA+dk3f>$~MC=QF|5dfLky3f{KM zxyW8{iXgJF+x*9>f+6ji_)I^wS&ynul~i*#b3RB}ycXj);57QT<=v(k*vXG{P6_tE zseRI=beYoeHlK@{Vg(QMah(vF?JcvtGjU2w{jEz+sRhBfmo3cB&8xrOEAtgQX6n{0 zCz*~8m>zunQ#hzLHH0U|%!4Esup$nFRC=IsM8nGUsen@!5X4;qzQvKU?TRzYgMBx- zsHrQ;-!}}?9k=|imTzaGPi{pu+pia+e?*W*K;^1O+RR}aK{vwXY1fQ1-m;bdUc{A^ z_RUA!zAw*qv!_5&K&5!@)zrjt1-_P)*B*b*ELx&rh~zu78SC@7AEI6ta0xDFx2j#) zcZ!HLm)ZVOQnz~Yqyk@NG_it%)b91LwE2>J#w#cKJ`~Md9~E#87cVPGW5@0C|NPy* z?yCN0_~~I)TMm;G1iePa^RK$PA1_4Ns_-_0fb~Ysf0Vgf!NDdf)}-Xr1Zw9VF2-+q zaU?X`UUs{$PpKz=t(hn{K@Ayfd7?U?l8m+yr2p=sBPA(-6bn{)F&|O%a#0+Oe@8{v zUzx3#o|c^$*5IPQJG<#Ezm+J5lx#zlK~njRRzdBGZx=o+s`-ahl=shi2qx6rV~p|G za)F&qD7l(ItY8}>mKHodX(fwUZo#IQ%X}IHs=|mEJ1kf(&bCmqiWQ zi~JS#qlO-c+IWgyw(Gu2X4?>Ns8>r`@f>zfp}*<<5QBO|z&Ewml%em=F(RAmbj+lH z(KToCMeZa(Mnwn)7uAP#*bEvC-^qQl-@;eh`+Ei+Eq$t-940Lqe9DYw@bBY^EPusa z;4WrcbBcHgc@&}Rgk1%{$@20ow)XQsr1kCfXK7dXT!x*NfOkDBxzPmMSq&oj>0P(nHHpxtXb~Z$4hn6l+!Ki z@X51Ig_h4!JZ3NL6m|2n-aPpw9+ZdYvu8>f;OoU2qkvP48r@10)N9JLiGk z>eKwpQBwUq>11}oh>5HMGok7>%LwV}%inoW<^ts1>3=6JDEkd+{Z0~ zVHJE^YImoMDKhj?|Ep<{$+;Iv2lbXGnce=)is4sSRY}_1qEz6AP*uMi_ae-#c_pfu zw?ip8LQLhDeB*%ZX3Z=5?%YdCJiELGZWYi^wsFA|np~eIN~@;e3?2hHR&jU5t2^T%oPXOYh|0G} z@FGs+D);kl_Fvm}&!K9e#MR(wa6fLP`L@8$(b|$&k*T`?dm_2y_`A7oa>!4g3+_V@ z>(`(5%dnD(nPGT6ZC7E&6KAX`7AiJ+BAkGz_+!P86KCrWlu8TW>kH zmutk&(sGK%5))8KE}J$vjYcURide!-M-k~)>LFLeZ`JVKwEy~$h5zjsEC#XHPh5H* zNpH`ECi60Q&@vM%Kr2`OPpR%W)CA;xxmI4tad-UyLKfeodfc}9nz^z==b{n2==^y$ z@a!ym`t`>6^=lO}MEcjguWMLTXl~*#5fERBd`v%_bnfg{l!Ev6IYC&k<{tCtd$I1F zVx^y0J`LQ|uzd!uf|&YBkDxX6%#0Odl}ka{+=XU-K=*c{53 zC$v(zoRc#z?dQ(^zTByNAr9FLSx5X_iPM~t$6-(#V0S}RkliF&xCl_L*nsse$*%Ck z{*+mh&sgl)xaKSllkc``wk|i9?aas$aFfQV632DRklx!9lIMOH65Z<hQZE3`A=JI-v25N5R+-|$? z#S6|!r@ziUbv#cmGRK}H^aqdsu-&&>-0g`GDngzc8Gkr5cdE>`ZqMPiciuc3 zdl;_Lt3TXkW5qX)a>K6&Q!ZDqsKpaGIrrbmoj~V_Ca=@&T)r>Bv#7%b`68RYQVt0!F{c$lx}$Pw_=v&N zF`PZCt-riFyqkOW@czJhPi^Wq=ji4O947jdr}~M@t)<1u^tq5vx0}@62o@!$8a5pNa1v2=ubOZRLGrgVO7>`^;G~aC?8#tFReFVa{v~*%~HhG zm)lXTj6a*_N1hh3#3DEFd`UhH21O-iQ?&vLiUesW@whtQ^W;qBp(2gfLM}uZ%pJx| zUV6?a3XL|wJNtcdKR>I^LoumO>Wv|k@EBb*QP#i3=F31rWGmU)IVEOF>J)|{y*_t< zgFK9*p)q9fVda182P2dk-{#51eTl%|rD#`C#)Nwu=<+_k@1~#m#MvYvLC@yy{K@zK z(ENvluvyVMu><{q*PHVnQA&?jB~c`9AgZ&c{DM}|_8k?lSG>8!2OKD>dtqJ<)qSq)`@SIprxy`+rEF-#U{ z>!PCWSCCTU!O2~r2YI`W?Cov^w==EFU;B$T=mbt3qf8$Vgb^x-3{MDuwY0Uw41+J` zZY0msrD$EnYoewA#r>7wFRH2T<#Z_3^jP#`_q5qUZyBTpxWmsmpHGDM@M0C`3$#7; zdQ9V;$6nOO*7}qIjwOO>lIOjJF20f+WO8eNkPIh2j9iTyou_h3DAqg|#8A)6UF4eR zvugigCgN7HLqg_!jb`@zK{w(jH@EK=!;hqg*Ircp493Rjgv(Ux z++|anW9s76O^0LA69-`7m)NScS{ER?DhQys_ic54p>yv2N(2bP(xHC0Ci7`?+=vhQIotexq%B zaZxFG?nOGCe`^7HYp%&*{F;VXT?Hp6RDU|QOUT+TcAdU6+4T70pU&C%|2%kBRaB}j zJ13cqYD$RRE=WR(nmE6Z%f)`?8=H=)%0@+j7BR&R73I#Bmf&BJH;fE#8M4=uvKG#F zBHG~il8dK(RzTdWt9WEAFG&sOZ2JFng~osHfWE)sT7CiY>q{?GUvZeA_$l_6mvq}c zpc%kA7ADN9Pc46FDm0+$v=k41?x$VUz+i1ZhC9OL7yV=R!y(I_ig8oe=GMDN>{P5k zQDOT90p*ZeJlRU5eiua9sQs0EmR=E1E0N20at=<;dPx|K-;(J;U~~9mVA?%%1nmXu z{Te?zB@m6C)GRj)8xUYwetWx+H>tPs_Cmb=0Rh!x)C7$-*v@J7S&=ets@3jQ{Bx_= z)4JD9{tmSSHMq*gIjMpu*b$?Oy1d^~76vDUFf2rOHebBhb#nUs^jo~BzJBEE>S_Qb zTwI{U?8|^%{{H1AQ8<^j7vdI~Iqv#}Eh+Th71?zQ z6iM|qhlyQXiuy2>F5$PuUtQ&`@6Qn1OBC6QjNyg_vqA6$0L9jk?MSGrV413Ab;a*) z`zFS<8j^Xi{g|J~CeAWg{*J}oV#Clt4vX+zQYt&KTQ!s^22#Y`E4uIA#&qmBA)AJP=9qmw6&0wqvJeulkGSGxKW zskGwB2Qss7@w}1lOtCOvA_(8!y&m3cKFh{ZPKOJk>y7Dm>+`dnG@)xctfp`w-WUU0 zS3^O%j?~ww9b zUDKnYzFgY%4=2`pWpKOiCIq~ZRk?m$=@`yH<#}?_|4=wN5vdy5+t;I}4*%tOv%+;o zR>8ZyV#?YhH6*rq5!U^Ges_Z0*_e?s@qt$PEtu-Yi$Oa>w&Uv+af1790Blx<_|7Nb z>yPxedzISqbQoMrbjlIAa8tWP5EzSP4I=$KsqVz?rPn4;1GuKs$Tiv4&Yd{uE1vcD zd$BHc@HlWkQk3XUmtJ5hPAR{|SzF>Ysv$=I!+tXK$lj7q`>A@BH-`rz=4VxJ4>!08 z1i6aU&AwfH$&LWj6ajrP%ilW-v^-Oin|s^pNN|_;mjqo345E_*rjAeT2QQ*7!%-T? zJ1ydS;mTthl6jqa`bR2tWJ2BjGo{r2lS$tF=v#!sh2Vnt7S;q#vNX3s8H#b~{)|yM z-aeQTjaY&Yv8vpA7AB)+kWgqL=~srTU8qM$WPj(axknejZ@-w4I37Ze*N`c)N`L5S z=WHqdv^gqyKAej6DDCohW;YezmoEj{PBy8b7~Oz=h6_qfw~raJd^=+FXX`6YYeAX1 z%E&p%e)K}T-fc7u9Sn@4_7_+@MXq^JEOe<@SHPVQxPELIY+P8TI6&9tMeg)c#NCuul}st$XMF z7=S|{-}Ypi@4vHQ7Faplmuex>IMD7?6TflcLl@W=4yp)UK6dP_eP{0na)u+&K^GH< zAJ#Gy#SLo~7#P|@qr#1s*y7lsAXT_SF1R*}IW8<(RYGNe&r!&l2auP3InH}DnV)%o z=uc9VGV`jTSt68GQmT&HQd?%qTXd0t1Us+{P3}sgtPfFBp-;6$Y_yk~TWo@+>z~wf zOE}DSjHK?QT=Vwr&6t#4lJ%EdB3{Q}YxD7aV2n(M=0W=_9^3M5CV%?u`@9#6nUW;l zemN|Z#}eC*4Tx%(UB9kGLhdkBK*{C(!}<0~r*Ts>@!E!I)H7g;9)GQi__e@umOS97 zk1{e#c0_`+QoxKxbaXx58=rDvSX6x)+Xu^H>ly3-8A>vJoaX|$Cd2x@^ya|^9bey2 zMb(mcU~Y4$CR^{`N_w5h-=#2fhnHF3EL&0kc;N4@&frWZ^UbXa%~Sb;e#QJx6hx}@ zFfLO{p7L%d-cld5VR6NOcP{@?2qj3dk%(GRTtZ&cMr!R3)hcc0q!&9kc^K0SsuJK)2BC5eZIc!Rk(mb?Z~1HgNZ5&#LpiI1CW3^@-w?75W1jL#W91v;cs@u zRfkUVnYJf=o!=e&pmrh=C_ zGVoFVkHH*A!xnArQ_OuSHr{XSN!9b2ugnJrgm34g(;&saRq5eYmy?$a-BXEmWsGPA zc*lRL?=DSOesLy5=|2LD+GtS&Mx6e|@%!Xx;CrE}^?fwv-R+oZ+oaxy3hP4UQxfLG zbxLe;K{+BXQV-GqEwnZI=sd1;?J;CURx`pjNjw)Hk)`MQ=-PQ07JZD^5`?O#<{Kph zTObJTaWd;)JdGCn_Tb9<3nv%8biUr1-Z(zwWg=y`yul3K`6+CZ8WVPe?Vwvb^dV8K zp!UmaUD>~Vz=4@eRd#oPqdPa9@40T<$>aRrLB3mi)?qWgO@1o}JhxApQfRHRA~H}Wwg2ziB8o?4V*d?{y=xpCH0QJJ z@Kqu*TtJnP>@L|jaAKWZ*|@-5DnLTj0}`k|I7!g$3T6IYkpnFaevaL%6X)ne>qPTm z9}L`E1+?^S?H#Vi1~XnTi_$xy+3)MvV~_FSzaafD59gnuHiY7J*Z+=ht6IU|iC4XU zuW4;GE5>b28_RBN*)M#1MWw{^bpJ(Tl!B`goDb`NiHcEVfi}{=eaL)ry<>yUUhsj% zAP5A&A+$;ITFW1M+IS>*eB_kG9iPX+bl~P zCbH(Pr#PZTEvT-*?8H&I;#<~sou7=GX@0n4j~SE8>!gAG+az;~Ev!=6IUhPtHYS%e zAB?5dXmTo$P+{BD88O{%WT)l@>i=*j^|4ALtjd%j0VNhBkQ+_+3ZK2cpS?#6foO#d zVAP0Al6W$`b7T0fubOr+BOX6jbrrDK=i;5=iQ~#VLlM`$0)tQ#Ez)#`V2~*Pz`?m< zatR^lCo42Z*o^r$*U<@`fH@oAy(E5SbPLqI_XuzCPd(gOiq@I%*x{Ye?ZHsSNqwQa z0K`M~?dhrY0i(eg=_=kHD;X)W{5Pas+{2AUV7Cmpht{Y|Qn%PhbXfXIq9WR#% z$5=+^|uL6fj&QM!q#N zAo9PvK2}@K$)Tn8#_6F=#(aldzR02RE-xE|w$paZ zKcgm+83VqsofXIr1NVONUSFb2SBH@}OaK~Qw{-U>(r;vfS1f>y}DHWlEgvUe?2!h z)ZWX!d2wz!O9fY{^p5DCaLL3@R=SRcP?;ytSsdDQS-HsVvzv(}tGsG*TJ?(Q zR?bPHTbYcu6CH?mG+mpWy6u(f7G<_)g|M3+)w09I+xcHFn~el;g@y*hZqtkMty)kl z7@kEVbVKLO1-cP)INs6t)6A4Pi$#JgAge|D^h~>n&!;!QY^L)NU^ZE-q)(~r9;J*wv~w*r*8+5K+KJ=BtQSL z+&lF*ZV;_JZ0$d$Tg!KHL9mvguUURPLGPLZtL*b`e#u}80XspRFn5Tiuvpcbzeybl z&EZF>kU$nOsvS*kUo873`Lw5O^_@P$(uT3DxvG#~C}$`K1^~^3KlD2((t68P?tUOM z?1qQ;f_$|R=#xUM4STBPRJXeETK&?_+kLsr^E-wu-#TX{+eEK|s|+@=9pO}>hurA-mQ{-p#DE)m#= zC_Wwugj;P`UEkaDdJ7s>sqIKUBk?pX7D&Lc!5m3ok-o?#c^njVTv zIRAvQpuq!#09OW0#=(9b2M?H;)hs#z__N4}meCfk{MM7>Kc#?U1wyCJxqB6JR3^O9 zF+&sx+(!X{|jO0o74rnyTDG^e8+8K8>^t?2Hc3?^@ zrm8}jA@ElT_*&nE&#U?K;jmZwY_pl|K7NPQ)tAD{z}Z^M`fje1-1f^`!Gr&D2&#i0 zdsPGL!>^cDpN2QT8ST`L5C9z%*zlfOTSJ`&?Ll1AW@NEE>t|GyHt6if7gIyPvV9uz zXiTSb;luvT*G5J6*w}2i`%+F%FMb_1Fswl+n$W0VqNsXF2BtUjohx7R92C~X>z}G( zf0XNC`k`2-odb@*_2v4*s4E-azVx=b9_bk1nCRPPsZsiR$%SZ?sMYuA zBfN>A0Y1XJYcRfcTsh?7R`;GQ0%PnjORan1X2DYVa084OT|WlcboQO|^a8A%4G$k{ z`yvx>Qf8IIAT>aK`7O3>9yCS%>GgCc$&XCbv2p7z^Q+6>|)g@ z8fr-r zwJ282h4=$5$fccP2w3G63lLj)yhde}H75Bd&Pl-7Tm~dLkxXZhvMcSqwZEEiZT9?4 z?Ov61pwHV_wp#zeY-`}SoV8v9xwpKOwWa^?!v%kD`m4o8fb&Kco-2eZs{98D&rFk{ zoUS~EMoVpO@$+tm*5#-yWkOMo2sB07aPL{JPlYBoH1#{I*?sUi6rzBlL@1($rajyQ z9y8;DXt9O{22$cbBdCy%Vd`^%^t&4?lb&2xo<0tVgEZ7@EaF?5k6wdXbfNuP#w&Kx$3cB|u`~p! zi?(&>+}`^zvR%RNy8T(UNvOm~)X$xs3l3z$4f18kF7!U$$qDl=ppf<*`zGf__AG^w zVyXiKRw-V<6zS=~JC5N*QFkC!Q?`1eQSeAJP_FAEBJoW;kBErUBYV?q!K$LqW(%M^H`|CoTwa%MH>Z;y>%u(l1}2u zn3S^yhT8zIl7`3co(j0ovoTupq3HhPI-_{*M>Ul|{-x7!{U3B#!~@|lo~Cl$0}7|t zK;FI*oJa!keIhBzJzR{w2FJ#!rP4kKgGj(8z->kmns~_TY>#vj@tV6fw-{2e!W>Jv z_7;=dSISm-b{%=PGdI>Ha#ffdpG&$k1w!E?c7ahJrfz#>jV1UC-8WOk@$ZSC^kejJ zR`q#;Pg^3B%*~;&`ZAY)uAr5LmCrqU<8CgDn2IBj3C~mQEP5H{^VBd4^>_M$sPNJ4 z&p$>Yxdc-Zk2MbbqL0z}qM;RZ3k6c7!Cqq=sp%tT->QHz@Je$pCHXVT?%9$th zffmpePdjdud=v7-j>#eY+~jE&L?+g0zVpLWM;qdXenQQiAZXi#*um~AtevSS&r425 zGipRYisR1c+hKtmyQ(03+3{`bZg?=!ZxS;uCx1vvV_xtA)C#p92L`gc+4B{QCA5mx z0)m1EPP7H;F4rHV-m0z!*BKjzuK)h+203K@y8h>0I+P+T{*JP z{=b+uDc%}qUEFO;5g@`#%xkxQldYcK@Z}WC&=vPp;xjBM0UiV>G~gu+rx))u46lJd zRexEJwOywmEd1qq@M6TP1$?4(qkbTa05lCd@wJV?h-y;!JC*Y(x$b{lN2^8_wcfX_gw~mg9gZm2H5DCQ3p-+_G`@8oFU-{7a zM|s+Ybj?p-2y^i^TGoHmsr$}GN`0E5xSE0!#BPA^)x}cRDThLs`2`Ff@RIeikx8+L z*H&8UUZR1taEZYq;@Td^QP}0DQ8DA^TJv+b9o{`9%eygK4(>Hd4cjt@|(WP2}5ulbnI*G1kU+Wd)Dh;yZON|@)&5Ex8LzrdTK$3*3VFs!ovmBGLOF~+1U!6R<|X^K%Tu^;6P z$=>_xeY*>iBSRi&ciR>vxyke^NcjaPpbvTV%cmWxZVq_!vg<^?Z{wuVKTxf(@xn2c zi+X9#c6aq^*`8a>QVzsZ0rwdtS$~=wu~q!O8J;BN|uSj9CeBh*&jM!UN?IEd#Y7 zw^(!Qhr~qnIXeLa8G;&*VaC^I{iq$iq;x!><3_JCSQ#gmu5hrEFlG-O4TIf#3Id>7 zlu?M~cl%!=>v>3bYqdnjM5dU>_sDr?RVY)~ZAPMbD0gh2_yOn7%aigbNzaR#0C8G+ zF}J%^^1*%$MvURbBY(zX74^%}Rhb)i4ag@-!E!bR6}^?Q!1;eOOvP}<-Q(pZu3+b} z&KS)Z7?vuIu>)dD$mn*5_vu({&0LKet2N;Uvtv0^CfGVsZe=|a)zX|`dt zLg%;2Z54xWRQ8d5@F#+PF!Bv4^=ie3te5xiDeQkkUtj7Ihrkd+n_^}vpez^F91{0+ zusCF@73AIoVy%sQ=k5~o_#@z8Ahfx3mS)Z$)m=Do&N~RNpNTNxgCONQCQQ7C_52OQo2-t<`I{?O<8*2vvP@9V^M(G` zq9Y{{8QC>-jx}WZ3JIPOvvi^+pmJEQNf&jC4NvR+@4Hv7Ox0Oi*9EKHsIs=Sq3L|{ zygs(vfT!aFL+S#^m`MeZNC63bSUmUz6y#umoL#@fROrax4(r5r$Zz&M(_EL2+ zRMJMnkga(!Q zs>$ld4g)__lFJAe(m=_{#h4{J5@eNE&7#PTNj{BN)B`pJ%C0o z9HPqRq;hp<^4dbt*@Y(?pTGyRoGGJsN8GorbmMjB>vjdLm1K{+_LqXfP1j4;Qu(;K z5#Q6VpFWM<`RvXE+W--o+~=SW?#|(>%(~{iQLKPuoNNI&T<`g2m3sW1mX`F+FK{J+ z!SpH|EDU2kytX>UT-Y_#;ssjkut!{wE+fAF)<4OqVS~rRVDJ!YYuLBY^e}Rc0()o# zs5JtQPbyO^O1}Z(1Qv^z85Ahs-$t^q)JiKE`+K;qvfmiY9-><0-n^W7=-|Da6E?DC0Ij~C| z`!^e!#vRsn3wx$}PS3l|31XN0Fp>R&Az&dvi&>fS{cCv;9aTNwevY1{Vp-sTT$wHa zz%6J0QhE>}GL7yAQ6AOofGrDxSRTDg>9@1eE)e^~nPII#?*CMkB_y=E{tqZ1{B*I= z5Gj8p7ecOB9WGyHYJ@eoE4>q}ccLcHfP6n7?9HZg!A_daXjO)_s4D+N0hna)8XWO- zm^b#Os@Hi|k5>vPpBz6V2{dQ{#B8OyBNdj4rV?@eSq=h15XS_>3O1_w&fi$6_R9Z9 zk3LUNGM9nQ8urXiKS0aP@meKDk#o=H+M|*r1g^dy1FhP^L^3v5XGYlAV~qoc{aLVL9> zf45F(`>)yR0=kJOVE>$5`SYg@B-FP8KARCyvBX*ooFf9}UTjD2z3T0sZ&`ryy>Qd` zk@mtO7CTpBGy6PT@-l3CN5Z4(b~z+_hVTm0QiciA*J&vG$V$h$^GyiTwDDlU;Ltp$ zI4YN?PD3z=hbv=2nE@80maqCJ$80hhq#L?&^dXA_ia-FUdS6d4h_9yxU19`WLi)?% zsBl*|&Hjhk0q-D0`Cq5Kb1P{Ukt`YpiX9FYZZpo@x*x(ch*yxYz;SV)|x={N_`L^Vjj*U??! z!wm*yB%n^=k^x-kUreIPQ={4MBUpIm%tpBXl>`DIi*AH1l#kR4a{o0y1mUSEWXe8o zcY*nzi=pW|I!gNR49!Jm{dE*>RFy{M%6Ou*8b~)`2Nc5^m`;B0mG+$T+<4ar%0Hx z^cTJ?d{|{2e(z06-`SkuMC7j`CjE=U)zzUPUq#Oa{*DL&|J&EaX&#`A6@-QGa2U%BNG4DH5WEjR`L8A4v zf_!_L9WKhSThUN8T^}Cd@$0aWm&pLA^4Abh+Tv9m08Fx@p*%O2$a^S#{N3TqG^3i>2l6QiJ>U{=O38aRbY0llrgG1BfCS4-rovo6vX%MHK?<(V84h{Ee!2 zj>CRydax}nrcnh2d%p_U_x~lM{v!p7UTgUq{~)KBn!;w=|3S_#==ltI#zWYArITB& zl}rc0vn6*KBuFM^1aQt3@r%1Z_d**`4iJmaBUd#8Vu^tLb}k*5EJ(FK7A1iU9s$!m z0t%jL0Y(mV25onCRd(eB(wwGpupinH!n^@V3xl*fZ+SjJ`wLvT*t8(b`}yYO#RwYr zzJsvgaIOZh5EXurUWYtvzVy1QIXI^3iC8Tk29;^mXdk>i@|zQ7F%19NXe*!JCMV^> z_k4jAPzjdL(SjUIa-jHZIsT`X)%VQJt{8ooD ztuEXg80`;AsCoZ~0SrieO5i_4Qx8*Mz%nxKZ#7ZEI8Vcop?`72nC2hsl{7`v_4pE>GE8HP75 zxEtNDa}dCK@z>^Ka+&!iE1e#eX8y;hk2?wWtN}2AAP4nicAhqq?`394@;@x%Y9r2vY;h@EDbX9hP?(f>0Hktjz_t3BKe}ispmcH#AevUlVySeB5o*(3Q%=%nBa3Ypm9Xj}{z&1-iH*3XC9)zsf7dw>bo zm@)@~c=x=I%BJ4t!VVZz4+-%@5*<%IJTK^%Ll?hSDHn#I<8-&B(l zD$zPz#ub!_tX!Nl+&4sVP8bj&}fpI^kry6d)o4wKm-U zWD3z!N}(}Eg^YwbGA#oN%&bT=!~-(xbPXlK?<>a_t2|MCs70ZH9)K2f5OwEo6W#{# zs5TP~;{rTRxz>GVcY!>|CE8y;fSH5be8X2*ZT?x*LUOmA&EiSe~Vm)fGn)|?GXuyScj#Lsvh z;@ZDZA+&_!FnLLMSiZr9ti1wG3}y)mmTB-ly8q-20I{Xx`6n?@C|qDT1icruwQhq+ zm-RDU;@PvxJjCnLx;VQWQa~~g`uMqIn&D3u<+#Y261g7HP&ueh{#KZv1_FACu!`Rc zC#ZYy7I0mGRxcYg{liqH{G2P1?NP~&kFNd`(WgdPK@zd?PI5!)ZdDeX-uWn)OkcIzkNfd2x~ajaG^YL7ZuX)>9v=}byAJL;c8(!mh7 z5Xe*|RC3sNO%ygJrI5DiKEFm3%uX=P?`O$ z@Uc9zN|MGqpU`gqeGY41URWrZ@9=KXnSPNG?W5kBlsh$Hu8+v!({~x+5Fx}s)bR0n z(m@B(zmVX2%F^{MY5maUC4N@sB{J+bQ6U#TS3IbbNUi;2n*E#UafmZuZZ(pyzQf5} zPS*sik-raXV2OKG%=lgt=X(w9z%^-j>P_`nbc1{F7c|%u2UV|LFa4GdonOeIZU>$y zaL7SnE0w2syRSJh6O(#gB?JN}yN&2&;mZ(M0El%cX}+(v@pbJ2JR=TAK558Kq#!5{ z`Ih7_F_rljr%OfD{{5&U`D)CpTgM0akOxgy#-U9mMoNu_1d@582pr-#eV3Q{GJ=PO z+jV?D;=b{*m8b)b#m~iW+?^_!5AOpjaY36Lc%DFnxXaHBT57`87*ROY2Va~6)rnez zk0Istgx{JlyplhhH|YKUkxoy6#f_E0{lj}XcplKM&KocWLWl-qnKpdllaBId!p4^Y z8T~&Qx(ls9Hbwb;p?RxyYFWrzqtEaa(1AKZF3?X%3NZO%hHSVs5>*^m$w}oWegFT5 zjuF-50^4MC^BiWJ1=MvwZT+&sqMO%(T*}KhlGTw3}72;px(+_5J(RTcArt`ds$b3IobPs(^y$D_%qna{1sPF>T*7+yR!$hKCmJaeMq=a zLHf*tvGkpg*HOd#{VZ=UclL%)z;Wo2w?$qYOq%`Q+Qc%DK8bHt#hCm4ZXivyoFz5=zYxyLN?Tn^GMSC{IwFoIUqHqs@eX6mZ0i?v z;*HlGk5wwk1yDf#u7M6U{k8i-oN<=U$N`K}?7bu}{~XUvorCwAEn$3@MQU}ai(xqb zE>8VfRStN2*k|G#tg2sNkd&fD5s@-Vk(IVeS|F?0(m82?YXwR_{k3c=R#Jf5)BFvD z*qUOtM|-cE`sgE$O0z6FD4~}S6O6x>Mq!+)iA1h7ZLpXCEYQuPe1<28e2@@=d6`)Y z6AZ|}q&^2Xil|A!gbAGiqF4KCU8r<$9a(-Ey`>KA5IjFEE|+e^yCakVdxsBIwe$N{ zA)z(j=l>e>wzB1*cwx68V4f)WbBUs*5T>a_wa4i^PAOcDf1-n4r3b{6gs=p-2l9oi zRCjP}5ZC5vtGZ6vQ3~F|XGh<0k9{QQHx=flz!FW-LEmAL1^H1J4X>$x1Wq7iyQZ=A zWm@DCB~jocG}hX!{`@pZ&7Rqlr|Xy_;~lRGCFsB9Cil#3QG>nMfvVtLCOlIZN&wG; zr)LH4V_x@QF{1Y!>t}(G$KD}l<7jD_BjW#D76}rZ;H(cW$)njlX@U&xDVnonI|tL9 z&kcv#ALnn)_@*+KoDvmvJ8vxvb}P|eE72Ra70=K)tSXKXy2u17V4)jis^nMYA7ErA zTUTx`9?nzy?@6P!KKUUo#>xEr&3_Yb06sacX8MyneuIYsOwB=LsaZHqDS=}lZ}yVv zRUSLEm!hm1*fu!jo~P5r(;-TwDw>97WM(}+X6rQa_v6+WF-OEs6He4=hTnE6&N()J zHjjq^7jlDMgrl?w(OjHDL+^F_C`(%Q|J_Bja3VG7nl~tbrxs=+2)wV4Z`k?hGf}at zYSr=`l|}Xhg}zTSNO6y`b7JQv!g?QF(x#78ogeb^mPUm>DA+lsZAtk`EDuwsrlk=}fvCF!x7d6}J_UHi4rK<^ zEyZT`1vlTzrzDZE#iV*3!~yUa0fXk=S-QElEP5^|@g7WOvz((o76tSJJj)dowi5fE zAC%eLU(^B{3dlfkV@BnAG)^s4*MtSM414smgxA0{JveO+B{JA4Z)PI}j~RSEGDJKV zi>!E6XuqHrc+Apibb~ZQs`mm1!lG`K(Fr|$a;d!x-#reMXK>kU44x{S)nFl;V^kHA zH@=vIUYz+JU%Qgl%GD|mUq*PR2>OK{N_vcv%q@rLuhzp69gjzi9^z0&6CM4WH;tW zevGVe9sr<8Kj8y?qDP;Ng^z%{`akHE3X#eKcDaul9MDaowDdK-2f(_M$*CYrP55Rk z`%iQ9|BPPnZc%wR-!q7Cr@keZ623roBVJGDxjOInzk)0lyQbpc*N$74J0;nuk9>*h zq9KMR1?&pn9gCJKSK4dBoG?z^t3bIzTbPr~RS)EZBK@_2m;7(FG%5P4P`|{BfC~W^f*r3i#_UV47@b2vYb~8$$mvq$+)Z}2^fX{ROFHdMp z3IPc!+N73+jwmtevla1y#S5<{$9aE=!+ZwAq74HUcEc~ z%vt~2!&Gt0Ij)t=FL&WHiijR&eP@I8V<)E~ysz9#)$@AkCxkK-^$$0op3lI;0X*k5}`)Y#sGE$J3=Sch4b5AFIFoJqcQKbejH!r4I|NWTI)2t$#ZyV4!{Ms{hJGi>>)M zua%AuVE!BbVFXAXUwq{MG41Mdo0yMWlQ!^mY7l2 zWO-~^CuONYi_${bvMZu7GDawpA(}xVSz1WyJ-6@gec#{T%)MOqb)Dt&`J8ijw=4Y1 z3mt8s7{KltbzepA{}##=2{y308~NBbmIov($Iobs4LDS23s74a$*lVkR8gIOEUFvVB2t*Ii57{7{smegen@f^-}QFNR`|_B=&9DRDGc#)YgZjTeOa{vzax15>JY>L z@ZkK`Li62g@ObXDP(F%LBO$S0UX&@^3wv1@B3o#mjvS`s#P;;8!)le9!FbITyNPCh zO-SL)n5t0SdN3=)@8^ z1+u6cXL;(!2ItxWRLNL=-U=sO0w^|2qI2`&5b6*0`_oW1gi& zOFuY@5JT?<2QT;dg(BM_L>-%RLuSM0<-z%)@ag$)=wRXP?eZAY#`^#4=m3vHRRZ?BLO_!R0Q57-h2e9 zCxxgLDY7#w?p3G#Xoe%zX2#3w+U4K}|k?&sJ5s9`2&*1F47 z*%R?B&yo0d=_vkn_y5#apSap=kPoTJON+N4 z4}=>s)uVdi4eB-s?mx=N`p4lRWIvt)mEa4XgA1WfRqx?GSWF!#Cjp%&l5#!)Bb5@p z2U=H4=a*b2t|^U41ueaBKLi$9?y(9HWY*`INEX7n#zpJgxTd`TuL%3;gv@E@OYt5L zp-h_IUbm3D9?4YH|G)0sfu^@M2O`DW4}1OZoOhEct2}W5(zVj_)AgrbH*UbLi5Rke z=4w9j1XM?MCKtQvV;3_vZ_U4%Ft!8iiji}_+U()^ z6woz)?3#~Ngomlt?Nv`>!QE7+1Pihd$F{|m=}isU6X6w20i+$UGAJFO^o|+TF-XvG zlvN0gq-Y+p@1{0hVcKyvv&sIy)|Yl!lE1_rr|T75l?CSklmgOI203VXDi!(yEYAP_ zJDpMPtb?zG*aPoA>v|9 zTiW_oNkfnZz}3%SWspobl`tM{1E-CH?ZOWirYzco(E&$9;vTmPP|K#;1X0jbU;=cQ zpr`We*f0@wV%kFIFbG-;CX*nugl)vD{XbvAMngxAmorK9 zy$M@EiWpKkpo;i*4L;{E78K{3**(yPC;P7Qy(IB`^mPU@G8SY5C#n5 zIs;AFneZ)tevI~Gy;dom*jNUls}395DH`Ue1HLr?k-gKlG*(qpekEPq7ul$Vn7UPq zFV7mr#gGq7%tobwrUkUtNL`mT(s@Xj^0tq|2-*)KRisLo6vjL!rE8b5xRE5_I^qU# z!-YLn?E>ptpJWY!Y~BKbr8Hci3n)S1i(pXNHj=cu(R$zLf05-&2ec%~O5bb@8@QB= zIU;I$!0d$mEYqm2W`5(~!sO?7#hBB*!R;$09ip^zJPEHLr&O%s_6;;uH3J+UsgW$0 zPm$tQtZD>sd(iWhnm&Sc(Dx^9lHDO)3@%8nb>iK4>9y*tWo7tjDYP3TRvEhZbHR#jr-AJ>W3GbCKzB2~Oip>H&PlyDR@g394(J_uI%iw6ph!tVz{u?LDvdZK|7nstT#3{ zuHU{7zlyrSccEu>LX6x?`wIO-e&@oh33wH`1T?Ipv(r*y*G>o`aC`PZI8#u!ErS@( z11~b4F62|~uN$4cjhNaYy1YsHY=Xb^Jy;U@oq@)j(S|RRZT~?nRMBh(g5Z(QdEX0AiE;kI=m-c*zaIDB37XFC^xNQmlG7wH2>KQ(j{Ta@}uvJ zgH7%f92CS+#2#jxwo|%z>mtf-h_XlZLZT8kvn8nknGownN2 zHX;5_C&iF3r?qP-pKHnJW=U$`;d>jMR#6G^B-~Wf6WH2 z;OGr}tWr|c1{xu1fd~|#(!=sH?&-!jJq>vB_Ne#&xrr@vyTa11;6f->0tOmfLkjEg z1>*Z!oub3Ai8)N}qnNdU)*;%{OPXh~ULeDMdQdOuQFR}3=v4FK<_Zjm`@(*KNs2EJ9$?bN`3QcE3sc{ zSX4P@yAQ#1NgCy_>KV}O0G`7oX+~`!hxSg8DZ4W_+~f1_{p2zzB{Q=t**WmEV_+0v z9y{z?6xz7qPSDYp{W}+4@~3C4>A;I1FNRWBuH$Wbv@!-lG{rFeUiHlCgvw3E00E5`~Se^SkAkdX&{?Syp9End*XRJSiB5=GIGEK16 z+OYY}=7+$~KPj3(guuuRi}-@nxy`0Ck<%%(t7#c}ub4PKXWV6G?}1k}&M>Hh^Am_{ z_^|2UCooi};4R2Y1^-b>kVe1>Mfo!%@Oqu`8f7MJ{!p5D{Nle`Aii^k7EOUJ2*lK2 zh?=6wF~c;NElgfoAp$ZQ&zH6h?{nXV*b>+7d2gz=8Eo9%9~MBh0D%H?YY)oS=_dEY zkH7+vG^$&B&$1t_=h8M!GexZ#=~><@2e*FMO*PSmG7vK+j_0=o|CeI=1yOlTJA*O80Eh8JNI(3R$pk43S`@(~ zr8BY;MGp{@qLoZrahF*+Lvo&1sQhnHUV1w(0wC}62@6W|b)El);8xcbC1!n~`ak*Q zs1t|L90;G8QzAjjVAG#+yyq1{&LQeUQjoX;@Oq#*jDy-!2Z-u99%n~~LH`lt4*?3K zm}tLvSq697i-uCL2rDTVT(1q1)V9acaA{QOd+{fe{o!m(1M$5_0!~;!CWxsJHHE5J zY=F-BP1--fWs0m=>BR&tSR?u#&?pqS{3BrW7PH`bjxpPQ9=tC6Zcn)($S6WOk+%!+ zlcdFzP7czzZzJT?6wT6^y}tQ*@WLkKiF2i&PcSV?J0(@~!XqjyG{6$b-9>nV|2ODn zqWwG!w*5X{w|M%SjGc1jf6MX&Z$y-WQ)lZ8Z=Hfrc43giEoq2~+^tY`1*_?8u0yD< z%ZK_B_(V|bOZG#pbyEH+V4kNiQzBWi|5BbX)V({tp91rSE& zL9kp8Mi}*FfMrz4I8*P`Li;mh^V4F{$$u~XSkM_9)M@#;9P@M8=xfv9d-gE#{=>&b z@5uU5J8mR&ymPQQoV$~dq<~M!*>o&GnTs29Ot>|3JlZOoli)2a;%S+r*p+MUBzSn6 zviULP8wq&(xyr_e{zcOU?Q%oSgN8<7pWhq{J6V15eBtU`*w?@3dr(4et1_N9tb z&mlQKteWRefwImHRsG&2ujw&Uh!;Xrq4}$IwMcKepi~E4Rj;@5__yq!VCwrqG!iN` z<4~;od2!vx-9-v#uQ+(U$5@(9H_8?)4UCJ33+?G_K$LAJjdX}3nUCfD56P~$l z?y{UQu`g@e&jLm7UI^r3Cpz#i4X`iSC5bLR(qeE;<`dUCAwD)_2FO4 zd08nqtE%y+VO+VP!3Fi-&jr&IDU|Y5| z`Z-)H0zfdeZ}MDceZ+so1S}tJ@JjpIo+sMwuEibvPZ!mn9`t{cl43gDhl#_8~r77?=r^q2%lP_oB6A49J)VkGG0PWK3I~>!X*TqbdPf&r5?nxflu&ZOa zaYV_ucuP{i~7RKIiQImHm4P2?>pIhr3xMY;zP#1oL>`|qz z`BKHpX(p?aZ>a9w?q4aPeF8bA&n|=)?0p+ggE0Qw^!ztZPuw*#T#4c5S`P{a+lf4; z9QuB|tgR^fMM%&A&qN`$mE1x1J6s6}`n=E2h%I=-ye**x7kzdXD?j$mLs(4lRJBV_ z_(i>`$_Y7*-lPLNpoVE|A3NZ``10GzQvC`8wZ0A-$ios3h)Jn)6Abm0D_>apGDirU z!F=K!A=psB*vO3^*q=~{FzawsoV^Qg^RNz(V-V_WR(?rRDV+F z*R^~bFIj3@bOXE8AJB0-@UT`kBY3Q!sXABG(5KihDYUaJP)ZkMv~Y;aL}Mi~X_V7v zCT3nVc%+Iur`M75G0hX2JWM!qok^%JZgc+VP&T^Qglr=3IPyYxCYOd|M|OF&pOTZF z*FGbIEtOO{N%-JGbIRf|^vQDPn&IX1we2Yjd}p&02`HlV$qTK~*a&YCycKhsb(wo~ zp5JRexHu`mn`%%;RsZ#Sfq5}Hnyq%jT;8#nKAX#d=m2e(4nt z8})*vaD-Z^Q@DqF-SXuY=Q!CX?l z_V!a3^2E5IZ6OPd;(c&)i^xn8t?;nDeMB09o1Eo!pt<=Nz3F|^yxTxuE##te@?Fz3 zh`MlsjQ7_CT6B%8&lepDD4Yx+94JZq@rZD#{;$C~HQ)5v%iCdA-5soE`azrRzE)v1 z1YD5+!;M*ua8b2{{i8Cc8;`E`HxS?(vJLS%tt@CcZ#Hkh_mK5({$k~KS>vbYbA`%_ zCtT0PS`Dd-aNc*^K=%dKK;`q#XI zyCG!Jy>H^aX5Z44^1Y)CdoL*DS>S$^`L9ow?-&X#EM~}R!kKaIh37m;RC*R2PC4o8 z#S5x4ip#|Of5MJOc-EyT^Duql#&s1WN_To0J6@6QYV?n*4b#@TtYQmo#OIFgG)b=* z0~@2xySLsHP3zQWE-D%RtR9$6kcN|fx+jA3iNDt_GhatH!BLn|e_R@xfnNKExAH(E zu-PqVnPuf$fz+qG?tHvcMQR{JEo=b4e6~grXq}6E*AMEfe&Gpoe4h^*)f#0ZyA`ee%yL~ z{U|p$Chg57hT6>AAR@FzL-w(1$6KRW$QScr;=y;`E93#uy*0Ejn3YaOl&}q6+OIh_ zCYy~bBQkpfPmjm78T6qkkDYY33CX%~Fzpv}8`^qysgs&~sO~hoVdDO4R$+K3`H%?{ zMb2I6_)$LE0R5cAaB)oV4C`_Szq}*<+;nW*6XIp~x~w4S((`=DKfevFtk49(o=)sv z-v<8l)RoZ)AH9zc7L9w4Wa95jlt8lysk&k)l6q&gGa*0waj?qwD>NK1Y-OGY{MEAf z*PO+ppEJf;%^mzdHC6>_x)~vknC?wb63MBh&*!&UsJ+`A86CNXEgo$=APqMA$|xkU zlG9>xP!q`sFP>xWP?;}5OI75;*v-yz+=w zOJTIHaiEcnxbBc07*lGlzI2^R<)QYof=yh4U^HB68S2A@YmxrZXP2?^BS!70mh+xl zV0hi~QYig@fyMA65G#H=#IYlXMM@0Y>@-iY3QNCnxjizqX`^;COglMZeAQ=Mz%b*8 zp7R#g^$z|(_q*u!tAq}?!41xV3q)ZtVwzm>dxKtLZEtnKKW2@-?;Tv7ZNgWOZJkAz zW~9e#!G{&0TBP8;l0v&`9!L+0BT9@VOCEQL)zA1Pvbpbj6xAii@s?oO8<~@$t}5Vi z_1n0@RHLR)3F$MYH{naP_BbS_hfWP@=gCSNkA=bkx&pUZ8 z6ES7YgO#fX6 z0~UD5*9&nitZykTlT;ecJcVT|=p8ob)z;uv?8h~e88M=HX3t~9NMv0;;8QE5@|ok# zYJ^y(@?WpHpYB4iv4e(XJ7<#V4OP8sLwc(dg?#UJucd_cl$AHscu z^eSv_4d@81a^aP6*M{5GDwFg^#{N*wk^TJH?$Y5Gr88&>!Ly=!9pbLZc=hwrTqlw2 z@T(#l&y4!zA00CtW87G2S`}+0x!=%u+?r3aKnFSU{H+`ffAi*D7Q>E&9p|iFE15 zp>0zei*nrk?)#g1zR1QTg<(^Cjb6L3#ayOWeiy6_Z>&jk;5!#)xMxkLeo~XoNK%a2 z7Oc>4;X$jp?fVtE2!F7CBl=Jmjw7Fr@xO2Y|m_l+izlY0XSr}rn=)?OB+ zJTB1Pb|mv>L}+;8`dPbcE}mO9r?cr?D1Mx1QmExDvn=pFDGj;$kd$zo`S`fz@N6Qx zjf)NIu9?<+2hHBp`@`9y6|!zS=}ze7C4qPO5nl0}NvzXYh?#KDtWV-JD`UvZQYaxc)f5QiLn+aQl;=w_{qaFS-wlMem?y5Xw|$#HA{ zg6Hl9S?&PR9HI?ZbrNftiv{HZ0o(VRHGcX&oS&Tec;S6Cqo#a#&p(Z=qguGyG`ZOa z2O5=tet>WUW{Qb$p|l;kTiF?lHWR|K=_7l=1H+R)ZSTl{-u#49@BkWS##J#%Uq2Xh z@(tF$Q;SRqwS8M_Wc;2DJ||BCH`Q?08p=3%D@)K@XEJGpEoa+mfEojCrxi2?fpg#$TKO$^W+VD7>MAM*4b z5sV0}@4h>UZihNlsZ*YTccJ!f>I&qrXs; z`KpW|;7>X)LkljF!R7?NxZc3x>8#?5LhBAtlmfnN*oC?Be!>TKWZ%c_3Z9`^C|?T` z-dDnqE|RS~5V6@(V2~QIYfQ@W{fh=AB&Yp)UpIxRhAH1@zlP_nIZ)`rO5Fz{Zhh_fTGEPk!g{c+hDY(OD&Bq)O6oHYmtLX z$p6&nS5dU#dXEQ5>*{Wlbv@jqhI*pL{oL43nlE{rC9<)*(J7fI0Y2ykLJ{-BiAP7n z)-G-+hM?7~krqNb{?ar#4-8eWQJu2DcCFVwf@aXDDh1-F`=ooXq*(~Kw1=E;xkke; z?Oh+VOvp7QJ}6gc|v|*!EqWc@2f! zqexD6A!9=PvD!4?dWR>#8+$+6br_=IHw_+rgZNB{u&Eb#&D`Z0!td5}rrG$gAR5+l z8GG9w?k5sn>$eJaQg)~@Cq&P)EqSnRBVenI$c#w`n{}*(GdY}MnYnNIW2jZl0e(tr z7pRZTYX8j==I)oRMiR*`#Iu}xrOuFulcZ73h_u#2RLoHP`cO+SEf>NayVy-=&yNZI zF25J6zfSB=*k;zK(nNO92={F6RkCb?d=l;1c9@Mf--14bc}_j?SznNQ#eFGZZ)Kvk zj@>+HR$>omzf5mgY5dRc`m5u=R6ab9$n3eDY&C?Q<#}6Z=jRnjvorNhmUZOtMo6E0t8lJlNrQpb@A( z729@;JqQG9heMsqm@#_yN3)I9z(yQ>*l`{|ncQZW+cuM=Ld4=nW&vU%M|&<~iQO<5 zkn$LbB0Bu?N>%yP-se8zR=t4|1W72$)Zlhkfr0*J*oY5#eV;C71lX{4kf(Q^xSt;O zRL5@{;+HZtP%>fKFpD}00{S;o1PC75PxP6V_*;Cxa*_yvkKE!5PRcR+zO2;NnQU2J zt;&K(=*_f3rkal&os>YHw`CK9`$(43--^8TaT`<{^330~zrKpS>g4Mvv;6?zr&6zC zJpxKP5^whk6*d$f2`aQK@vkzhzW1@K&-jDXe3eU&mbq;5ehi8ao!GfQqNPpMHhq(bv!YB-5^O{e6cN!o1RUn^L0bwpyLl9!qwMdk4-9>r% zQzq>Qva|2~DQ5Vm1??w{PW0?fu6F!>tmY0Gss5eihU{9;f(>@H^i=5mcy&R6ks}2G zEZJ1?$bvAR176KqslR>WkBApQ1vG`-7PqHP8E4%1tmo&sLiQaDk>K(}1fabU#5J(* z>@O-t!|;W+bnmzUTI|#OPcKtKlYy~(%>my``mB3={945L?DBz3UfL+d(SusQPuha3 zs8`(!QT=3Q#H73K;`_bQz_YI#xz023b|Q+! zV@AC~V7YKDa*C&Fo>P|>`o!I?XGjfNUl!(K*Sx|f!AC87$vu+^ijG_|ux1r-@<|3f z$$GEt6tiNV2S2_rf3Jf-jeh=2hJk_PNKym*`70jSRp>xiG$teO_~B#Djp_R5&oixt zbO_5!0rUa3X;A@I`{CZ((&k&Xd^0UUL!>;2Yme+|4urCj6zU0H@BFMQ`*E3Skp)c< z5ot>e-O)87L`gpZEw3e3^QNQ{0xC9?FGrKsu!%fojmk~-leu}Nrq9BIDb{JbGYr?I zM1osm-O{%!>=>PZOxLe@CjzI}ygGV3PLEAa_K|&Mo5TkOQB(|NQk=>VXXB=L42njl{~myvaDexWi$c5LLs zX44XTncikS%<|kyw(3S7w_mqFc>EzSTQMOUGR%AD%?lyJq|b=Q9pk*b)1hJcv%QCx zJM5%qBKax#6jrk=?g<0+_r3E}HDVsaGTE-WTT;t!kK7o&oxMi-`@O>&c3c-Zbl^nyyzA5B z-!uiR^lRp8pxg!GIMREm@odbplYg_X@35?P*l7e!O46Z`yCPEIBpJSlQz5oCJg;MR z>M<84G6Ie3Yi+?kx={6}!~lI=0I+Iwl8XLzsDqLH_O|r8)NWI=Mz6s)*tgaQ`bZg| z-zqYXF=?07$F#Q9uOZ$@zeMlxH>!Sk4tP@f7j`TvL27tkWVG)rck!4pM6tp%qI^@f zJ%32A0IDjIk?#*zalEN%I#$5mF5#H9)wI$3J#zUfkUP<5yaa>ZSoX!7DO!o7G!VhI z-w8oA^E=EIpn{PmeGTd`P&B`7jqk?q@q)GR;EJvtDiDL7#S}@)FRl3Hb$3fkZ;{aC zjCv>cbFb%bfmk5;=K0PLXtzQ*&pmE|760+gqpgC$!ljDmQZQBMa4!d_Zy70lrOq30 z_exe^(S-et>4*>SdHWz^S^gExSk6&`+_SQYdMxVR77^3O$%fzl_1ya%i2>9|?IjO_ zv9PJXbsZgA_@=!;Q+(|^U)aGvyG_3Yebkq>4UG!ew2_O)HrXd3*(cvn?aD3O&yd}a zTQT=ThS4H0ZGji2=9R{smc?Ic3}*U3vo5ML5E_S%LEl&z`DBBKz}Xu6>cb{L&jvc3Rr* zlRW9M3i`~^nV0kvd#n)n5Dy+a8x5H>!S=SPctGUQ1shTdbN@9*kNkQM=}bkV%3*Dw zGI{aL<6Yd-QT|9GBX792}}{jEO|@gH!$24Pk9Q*v}U1sSA+YZ`>G?B=n?sVr{T)rt?3mA?yi( zBJ9`Y&jB5embWg~-b$y$vINTw6U~3^Rv0}tovT7bh}eM+tmX?3kLUVnMn-f&L0Ste zj$8)jtyB#YSe%WHU|*qokn?!7(^6lCY|H|KXa4Isu%5O|^o^Za*i#df32|TB?&Aqk zA)E`ldp>o7A~w{NH#g9_5IWWl?7~n8gv!KMM~1>jbDGaWBCr)hPtP~UHj1xzOSHsx%phh z%M-_7-vId5(4}xBQ!xCzv2S1PRV`6X&JSw^;VpnI0>Ju!l!k<>NnB~}y>{_`ZJta% z#m#TdOzz-!$sp$}X)h!X(4QOAf8Pi5L%jgF5I5lD4bPu5r|Z75Vf`UafCfem0ssy~ z0POXoR|W0qDJ+)BlBVsD_~wP6&(*aM{KGvC(<`iu>Wb&473X-S-&vJurnA(}rJuaa zz%+M3Cp|(L{!< z@w0|@fOi4%0}$HJzo;0y*b!cga5sShyj|!9GOI22yMwkBPrq!rAuyRa_ik?iD>Kll zpuv5FfqL$8(4V&ruLp^rqJBAl1sZFhhW?d8f53)0=ph$g$;ms9Ou_D7UHByp@Y>bE z!aHY~0szfmzAVx_lel2KVuQ;sg=0x6p_VsrIrrdbA=OBP8SdBd5C^{3S(sf%(-yBd zD&Y0ieS?2j@c%h9@k0wpGYW)XFVeq$FzU{tUX1s0f>?Hu8>_k6oB z#lAIe&Q2%1*S!!)EtN3Dcl&rDk-z3rB=cqYMJT;{StRG4O1_BNjP7jg&Z1C6&{so!b5{Swkm zyJwpm;`R=-YM4(yJt#n7t5&|y46%0ha>e%&0kWl^8K2)Qfc8X8&&r=KFkgEVS0)iX zIj(rbR?w+dU2C%_S3_MqPl@5#UWbO40CvD74aknXMRF=@GvltV=Lor?d(k-|du7_A znEqe2enO{4MOA9S1Rm1GFaq)4lK~R2XZofX%gE=PUjineq4)fa&4O|vL+>Ig6G|tPn`JiLiR^kbAd-cCvhX6*O7jjl#`(MGvP7`;h+~BE<&bLSoUPzeqNOd; zT5&n_?Fr+^<*xtv_m9RTwh;l218G{Y4@I|6~x6iZq$-(pix>DluJBydaU=uPFH#<{L z`XYy?^7xPX*Dj`<`g}d{C%vlC>j1LbW(9}+y9N4V&_gJ3ZdR~*SK#wP%>wI!rT$2ea%PZwnHrnA*6UtDMp?33DFFeE`q?q-ytBf$q-VEylz0z&|ODR*=hqj<|+#)ByX zZ>Xs6Foq0)vw`L1dA>h2`c^R)9}ao+YCqP#jDvc#^3^whWP*hEb29MotDh%)^wJQA z1vPS{rvGt-dYswhJRsZ@ldPl_`(@|tG`*C>%R0Gd1o=L*`Xgz`-dE<+U&1fZUxY`z zHaJCbRKV2P>d|Rc+C>5Bz0v^$MUo5;PFpkRY!O!WwNPqSz}yZ}6ZV9)ob3G4i*G6G zlUPRfd`K%ps>Dpgti;ec>_@JEimf>^j+@{FjYzdFDS72p#TgbtHeNkZ+r``Wz?X75 z-`BV{cXcZ~Jb65ECi?|9@v<>7>LS{d z#Q+;+EX%As^-ZMVn4nI>Wa5m-X@hz??5H_Acx+mrKF zX?}Sj_Bmj38+&BYUP{->?*13rUGX?yf7^OKEXtWHrec0S1){Vu$4Le|D_^LQM$}Ob zaV=!kheX0d-<=dQQ2NXS1P*2q3*FXPy@ps)g8(4sLsX=i#nZs(4(WGT;LcVZBlXcqGBN>9> zkHYN`UU&9Golr+Tjgr$5 z!Y=?aGlf&y{{DiGc*+fd=c6C?IN)zHMY(ilWl|esp}+G)@cjLUT9eucY#Ms+<>lg9HF0Rd1D1l*O-a!{pe5&C0f$iPZBB%E^cObf} z3&WOnrQK<}fpHJ_tm=KadKDqX!5xJ~YLV`JqCk}3Re*srOGCn%OwLgrm8QGvN$nNE z!N^T+PG*MOr}f1s8JHN*qm%T393Lus2^MGN*8=|y^DYo#9npZi_&x~+m(uRsIR)4P zWS@}=9uNcJeYONhZ1_QVrV2MGHiFbi)jTOeeD2!esynnD28GugC;mCZI%q2%QJxo} z+c<#aR-RJ%eRv}S7W7b&HzLcE47{~_Glasu>$_uGydiIp(BkN5;ciR&0o>o+m~H%Q%tAkg_KvUeDlvNr za$IBspr1_0mg0iaMY1p|Tz#3S6aYN~hNRqGVe?Pky3-BfBNXey>CWsN$U2(32}d%2 zz^>{%)>4w5`kzASkqf1gqQq|FL>}Y>K;a65xdQ_sm~;>DDf9tv(V23Wj9G&Cu`%$2Oh+T26k3%|3L%E;sL<(lagA63xzy zq(sKtjg^Ox3A!528Mnq`s*5B+i`1!Oo=4Y$xRcAV;lq_%pOMRBG`aD1Ez z0JvS!ctwgUjG0Ur3@4Sx9r(HPgp@d~@C2sJNAO{*^%%+IH5jWvtd&ITIM5<7DWED~ zyAqK2`axHLlgl#n8_<|PU9!YDzMd_7*ca`2TV9|9qs`Q=gSlynq<^K3N@(;aswo{wS%95U@>K)zmZbJFQ`WwF94ajFY!kAV2b8 zl?#G2%2x?fr2JnK6Y}9xY;O=B^XkPCLx2t>RZNSDunVsOI?Bpn;9V^J1{7db9Dt-e z8I@D(J6!9&!6`I4jZ-4Vi??Z$`O%m`v%`lAbvq&4ia!p~hw;QKY(A)L+j57T1% zCrd`0*Ya`Ha7x>%X$BfcGj7g&<6H`zZ-MV>nOSU5XQWTyr1lvzu;ujPN10ct@jed+Ov`x%lTCVg~Zma3K-#fUxpal&W(Ym4e~ z!hT{kv>hq@Zt6Xz1Dnvjbgo)XU@%RTAXjC0$G6KmN8z+#W5?|B*4@g`9-i{G91Vk$ zAsp$2G8mtc{v&|=J?3xKt_a({IztQheSIj@4xBn)Vu-#jExLR%LjN2zunEgM)T#;5 z{eS?)Bxe3#Yj)JRHxA$x3oMhO-Zb2m6o3mu24B`0|2-QBdlHNlK?u#pz4$kjmi744 zucn?n)^~?T2g&NGw^D9|yjUcAkmaS#0GQ?aKQh`O(C;wq;EpqYm4!h zSeZ`9Wo*6X2zWB~{UDxB!*J07tJCo6LpXXbXb|2^HfiG~5eZBIKx3u8EWQe09JBY3 z>VwemSA(es&3Dgkb;Z$RQNBg~2&L25g;mttH`idjv-c;iKnw^7(tK*RLOXl8b-8c# zKrE$Q6wWV$i%6FTZx$}3ckr7y}DyDhlu%;wVQkk=I1`P zHc`2dM}8K*z>6_&i{f*kf6+g)2hqR`1 ztKj+ZSG$&eo@G-MO+)Rsu^4pds&?Y=+tmMJF(@aP$ z%)z#KNNGv!Ps1zL(Ll8hpfD1b-*5Kme^WCLM!&K1)`RrCQVOx^*mtU!dgYHzc0Z@U`I?0(@Mpnz8eu}7)zOmmYiE-GV*$6hzzHq}o8iei0U8Hab_&ZN zOx5Y8yufW~gdCr^c|GTlG88rT?*xJCq|dl@n^S%qxoYQ$U4>EiV5Y%^t8;bw&VL0k z>Uh|quFz5wleT-}lQ!}46+XTj^o*!Ly^k}CT)j4#__91#@dO2uAwih&DH+BmW}5IZ z3VnjQQc^;*xTZAstR&p&=vDq1;n#*A7Tv2nUGyqjlF}YA4g!G9wQ;d2^z(bvG`qKH z^jbuFg(te9se6Ygv%sH@Ii^RD>U(iHrb)IvKrNdEJx>P~;KZ?PwH>2JCv$y9gc-vn$uJt>Ncwm`9?s?e{^G=+XRg zID7Q0-37=i=Sv17G<{nB^VAlEFy~xq)qyiWaz+TV&IDOWSV;&s*aPt> zFXj*W$qA@WcbtMv{&?#F+4}mENySf48j{k~0fi77g0L)FrF%SUJ97=C2irNwM$?Za z{Z<@YQS?E1W#c2VYolU3@x!x*jb z0(MTkZUnV2lM$^s~rQka#(o09iN-CZ9#5%R~Ty*L2=2P46_zkm4~C%gQ1XCf)X zEuDIyDNPz+yYL8uj~_iR7eI*^=_ho0hTS<_xPF#--^o)7L`~Q*n(np5Pijkh%eA_A z3kH;cQZd?`j7~eu3DZ|P{Vl0@6LR(KnpimbN3w)KGgr(q>U7?N&v*zm%haWn;Nnsh zKJ@J6*ghczvmgd(}}- z%<6vfS^@H&q2<0`d~P2xdU8SZ#skis8$>O;yg8=-vdC%UdOGw5wq=6ObPrq}w8fyJ zzX8zoOIfU?S&3Kl)wu@=@p;{I%F_;qv1Hl}Y*N>=@r7q=d;XAu{{`NFrOCb?atg@u z?r)wPl>Z1-V&L9KaO{wZ!O21d?hX1Zu7nJSD#|}4hG?%hZy48;+-zXSb>83#N+-r? zXrNAqwGu9h%DT4#g{HHVR=)UrA2kc8+?*@s&S8MRRNDV|x91n|D(N|oUKK!Ahe$;WAwT%baS9&!u@ z^(u^Jk4g^;unM7&-AXc_ZR`H?W`)UpRMa}-!Bxjwx|yfn@|esJQJ%%jGh@+%*wcs z3S(PS6iCr{;hs;4v4;H$h-G$u9j=1Z$g-qYl$YLmd=wH0yFxy@^Mqdyz7{Pg@GNt& zmsj9#u~{<6^BQ*3>RNO#)szdXz{x4o%p`S-B78}$V(t6g3bVAhVbn5q@4THyT+eMw z5rAjsj}kjKfNMN?A;36{t>`;WJ)0`d!Vf;A`%Ohx>dEb+_Hsj37j&?y^Enu zP_lyNV4(Xcl8@Hp-Qny~T5_|2Jm4q59cSJ_qY+^TqF^{S3GFdq+BxjZlbiQ=8D##R zQ(SndXgVHhT{ezA*Qe0lrn!haQ9Yb|1Yuf>3Pcc-BP4(7mZYfs=sSh{5SDJy@M&hm zrv*_s$^|vs%(raU+Cg${Fzn)wVb~UdWTDtVPaJcSm8vki7TP0zXtB->rjw zI+=!@sS=-%$-hWhg84}H=vF`+D!mWTx)vHe&h0pNxa>8l83u#*7?DD*t~B7b$)gvX z62LFAu&ldcZ6p)10x^&D1r+JHCl7l?-~h$r>dM{GD`d$%f4=@N1Gpz`VPb~F zL6C(8I4z}?ldmd-xat%U%5sf#-%t1T?c;kJ4>L%Rf({5pxkZ1N&fwncV3jOMtUcjN@0B)XRHb0QowFcM>z6O>Mu6NoyT-QB_ zj({_bcl4H&7vCLh`#^A6mcLr?vldoV{jhHh{z-2sLRPfwia2s9A?gh9LP4a~>H#IU zf(&Q?4CojR5#i+=@~+;6z~*W#K7F;&{(!>XW#B*ya99|qrk~^Ed=Zt+!#ckk)mYJp zQkPf5{S|~efOadlO>VaZ0pHWYDldM5B}j2UZVG>rtKnEJXphJ=%8vd zavg>`xl!d77kjEfIvCWbQ@j8`xm*4=;GdE_lB|$1wi-}>m_1L&=iKw`(?d@3oYd=& zV=@{|BR<^qO6Q;!Ak?5;_KK+&XZ_0QAem=hpI!fp$JZb$1OPbnP6a=I4^24|RP>Tj zKalkOO5VMV+OUtI^pe1h5UXVNmsWq42tdw|h?JEBwwj4{++1|sJf@aD$Cn)23}W(_ z3yt^qY08V?n3cbUXVyQ=KQ-EN6}IecU|hmLJq3Ph#Jp>L{rqj7`TquS1NSBt!RuhZ zfY-dD1`2J((dk&N6doub5e#vx+16eAQWKGZoQQAN8I}|heV$D%;L2=1_3^(g2PU#7 z8WK7#RkD9ut}}?$@W$Qo((Bpb9+74yynh%Mia}c~fib zo%-r*PDh?V@;Gvr^m%^4`3jeXoWz{5Y7Ph?kfBJhSi?|p20B){UElP&zYFW~v6k_P ziG2``LH9wVbV`_zs%_WtZr?vZvmwd>oq2CCA3`YYd4!%aY*oN435u#_bDej;B(ZYW ze7yIEOH2pV7RH1H6>zCTSgORI1~dq$=iON;kQ{)(O#lk0Y@y+|gt7o6$W%wcpb9ww zy?;|fO)n4~HSV{(gpX|;-;N(P~ z`ty)x8R`FNJ=_agpQ-|WCpUEtpQ4?o5vU@<}vLf_%33o9Q8hMwVCVG7FuNr@S>)YG!we+68&nXqKQ zpxc_Lz&BB9-%H)SMjf7K$1b+?T)%9Y)CAcs@r^^i^j#h^yM%Nx)(ns`nJR|Xy&GUJ ziwuM(E&B69WN7;)K2~;EHr^xMAM2MfM_ai$uDVvhH|X-Gh5%g$Ik`MI=djGrJAsJ+ z!ccw~-)?vYsj=b=FLG(vSwaJPZVSzJO(2)UN)h(w|7q$=z@covxG{`kjICvcQH-S` zVH9HtWkM+}DvC6+W*tMMWJy`xj8H^K%Ns31WXm=v#?l}qV;z~$8?uy$_|BvM_g&v} zUG=`#)jTujzVCCN^>@xeQU;HvTOWG6BZZ)oRor&m&pow=8H#7uV52J2->lt`QF>6I z#_u+{*LR^-&@CKFF}x*O*K7zruk#R-m|55u*r9lun+_9nK2#c|PeB!P+ac`v{{0G4 z5_-D{TelaSx~&9-w<+E&We)8l2O)B#hm{#_K3W*2DQb9BpDrhrV*-q!VHz^J63^KIS~%KoJ#0neqbs1|T&|wTRyC z55Z(8{(wtfeh0%EDc5p{>E1)hgGi}Y!+w%D?Q!F*dF)D?a7xIS*YHZud-$BUK*N)# z7d=9l96IA2(i#~ux%XtgBb>aa_`V;gAyWW%Av!5U@BU`Myr{EDZX^U{{Oec%PPsbt zF*jd>VH0ev!_!wfNwZ3E(Nu{@Q?9PO_h+;CX(NfDE0s-PNL^@(>&bld`ZK3V!DS6Z zip&8ZXw*~RA5%MK$AlJ1BrCik1lL^tS3~Z3Is?4Q-CwXp`(J4gHY}ZMmf$nbGyl9! z^FE8pbf_IvCw+-rANG#W2TO3lDrrC5w?iX4?mr=$cfBd@lG+2G0SOb8rqECFm7RMo zghpCm^ofEWI2sY{d2y>Aj;<(`bI>#HPQ;+CVCeHtGmWjMf`DyGG^rgrAd$wc3Xt9o z@~G=VI@bFgk)j{uUv_f#LqC>sn}?=4bq?o^FW;U=>B0!}Z^`8Au8A9W+*x9Xu3zDp zwQTl;C0}Vp`cz|}1Su#76EOjM3kIl;+XipTosexQ4DyC5d4LxQ?4It26oBmlO52MvPE&FY_!BiPFaCHC8= zT9`-aJQXFYG|a06e`;v?s~Q{fsK)W7d{TlK(zMQ0rcF>==Z4hRHPJ`UXnA^Sg5@&F zAl4tP)z1Gp@m*cuPQSdOuBu-%4!lY@)R$QkHK%<4%({#yqOU+rHD&UeLmUVn5){u9 zQcJ~dlpx#S1_Nv2q-v_q&RlGj2)?DDyVdWXWNh;ERPxo8Z`bdxjGk_BI}Me?6d%P# z{^5g1>DgQTm_R;Efs_G=gcxK^b4RuGzYqN*Km9KpXpvNot>|&|EVceMZ}az^ z%lC@JRxAg2&|ii%@j*N>G|r+lE*&Zr!44?Jx7X2R=If4rZkc60%u2LNAYYI3l+jjM ztFrbP)8?+Saai`xFavTg^0N0(_>k8+Wm#|t>*f07Xh?G2M+AF|^=VFeCAmJiF468b z`3s4Q#!fgE4qhs53!AxjedS|u>vaS&lxR0nCi^E9_XbeY)P}OBX|Hxc@Lsa~MjtI* z>GBf2ZIef9O64|Lx~No62LH$r5X%ETwSyD7Az{xJvScgoAc-X?yS$|E933x2ZR~96&lfB6@{8uo5`+R-*Ko)_0cOOz9z@by3WDnBr!oy^-uSdF;~Pk1Orc__`QGZt zS#O5cl`6L|1C#H|$}2~8SO^I}nM4j_PPgX1 zINz@Yn;0L9y}|Uq99#1eW9Y)lOiBn^AI?{#I#pD};=I?}oGFeIMBW|Pk3O~r!UjlG z(!CFI;_7KIXpO$*W5cJ>h^(f9%>+WZwA4%)7}QZ{ zuuTvoAmVtQU~l(tz<^ZbVW(ZDT9T(f#!Se5!qK-VIV%s}ZHPN{%_W@g)l{?mtoD~a z?d4@@r!WwR4x|1}C;GE8zHX&YCz#;E zvyRZvq>GP7B?u1{gDmz|ZyA>HACMwoy45E>9r*Pt*TraahCh^nz-Gd5*&B(oRlBY# zWO6^;JMV|wXDdyB^gOP=TGKC+zY#ma4Oa|0u-7M*zhmfr!C4>1f06zx29d31E(kyM z3S#O3QymTcC_FvE@G(%-YgN>!>?q25A+U(v8P(^wwtQRd15)5G(a=lnGt&?zf_Y9B zNQ@3@VD`je&eeew9Bzx)4=ElQ5RSUTcTe%AAH6ZYOWA>!kSiCJqIV`m^4o|kZ*E%q zOMT&R1V4n)VlvLfqZ-Uqp$HK3HEExZ?FbrB+wYim?oo+hnroLj>uUK;C!?HV~4)%!LmF325P%Y81{fF;vQmB3_* zZn$_!YGF}wQ=iv@|2BUk=nFv1IjunfaC1NDylS>L16XHq9k!F1tfm}RBx2Yn<3NT1 zqWuX8y+nPSp?2)jr;by-%}W&~NZY@4bS89wyrgw5D&QKbDBHEG3v&+UqfdM;NL8*b z>n&PQs3+;&j_=zC8-~VFO=vOLj`K;gTc^H>ExBL&HLC&)48{#Xv*cJ)+6NS1F5DwF zxI-h9aybl*x^AKPD6dA0{hn_a&_?C5Qgmw>6e>=4-y^Jq+*NIZYiCbcdKn#AI6LlQ zG+_ou&s!(+hIRUYg|2f@)Vm}XA1Itm>{Z~y64ot^B)<=h*42ZRq$J3m3rD@cvv-{k z)UAH3bb1DdsG^R&?R=}U}A}an6FnGv@33GJFz;;qE8h?`p^O)o4RVF zgC~EI=r@&k*?8aK5X+%%-tL_B+36{(3M^_cRJnir4xi z(LYFWQsPzbOIIwJpGrNKb8A!JrqT=dY3Kd^-Qfa&I~BB6VcI3H$D5ksUOa@sBiSsD1Y~W4@Sxu-9yYBzCIvL#+{|O^qbHR4XzjB-WuhV-cpAZdRyj(mt zxZL)=(OmralCLMijmM$F`~=k9oxwK6py3ZWH97Kz2j>D6OJM{mE8 z;Kao;Yysj6={oIaeWcu!576-#HtG~Vt?`%W;)L#{ImTFm*j2?}NA^x`MUh_JV6{{L zTL!Sbb(e(v-9d0CN~@>(CBtlhzRdATWf)z6bspeBD2;@^r0_CLdA)bXTm4PbGZE~=EvQud^5<%e zgRI5|ya1h`#LJ7tGN#AA?Oy%`8H^QuyV;y)h_YW&L#N`|C(sS)8PEF`pt)oZ*t1cN z){+~wF>7$HW;^TUnkArktXuU!7XS#P?VdJXQeA@^{f!ccvM*w(FSZCBV#Ayy$#O{~ zZhv6?JY&or!}3c?sA#y@5^_G|N}$7s~}BE%p4PoZ42&6H3@73S5L() zrTi_Jgg&?VYXSFCLx0R0Ez)X1cVu!|S_ugi&Ea*9goTe?xDA#@hVBbxdj=7W@Xawi z&5sc=9S>mPLk&-9(Q(7<^o$LfK}mSXH3oL%Y$jGo=vmKkwm|`JLCOKh%mUvEm=&1p zR7{oBe2A}&%WIxIUaoL=2qNKPTJok4h%C36Ih^<7V#yr^vXZcQeB`x0>81w|)zp~x z9OY{GzFIPILezlw27q&EX)doBP5*Bk!1b7Vj6>$(7*`w`-4PcW15hCO4PM!s>|o8x zAirF<*GD1EG<1pjt%~_)cFaGYWqxYG)CS*BY}~hZa_z9XaPhR| z@Eg6zmHzu*{Bh2R<_Oe2sa5^-LXKafAJB}j@rWv912GXq%If7uWWalI$nGCovx@Eq z$Ry+|nVOdrpi;rbf}lH`eeV4SoDClGTrJRlNOkxu%mgxs6Vr>AqPL}*21Hp7=QPHy z%UCH3FrpsON32d=A?-Y^fpZ}nfs1dHxD4oA{6bEUBQ!?nxS_YbOcrU4d+}j7#5dd# zRvUPx)l`_YixLM=~e?-lUH_twfjy+ z{f5v<%?>Z!rN_msjjOIgJRX1^o*3O~J6da8aPK+S?LB&n(B1(ecY@k|57tEZyJYP- zh$M5ziV~q<&;pNC%B)kM>pEw|foZQOK*b7QzcTc5Q%yP{C=$5mcEa#|AqO8u7-_$f z?}E6ndX{!16w<{IY#Kyd$qD_naieJbdZu$v;G53sF%n!={> zkFNEdXeh-~!=bronXSqu$BW};@YABUTlG@-v|L=+ z+j7WU1aE2BHOm|>Vj5%FTgmVF*OohBSgs%JwZ)I%$)9kVHzfE_8Kti{7Y8!JrUtIp z}yxCO~hJge};ZZ-L2A! z)Z1*3zTBb*J$Rva=1dj%sXLPAD)N8Lucw|Cu8;6yX{96>sG@u-A8XY!zH`H8GYf~9 z{@Z4)oBp)Aw$$JqVZsFH?qhW5 z>%~JJfJwhu?F8%jC?9EO5zFS+fzavuuPm!DBh+xk##4JI$-}*^!9i@hz{_(Qc23hn zX}>;Y8oLxs0$;`Cy8e|)XAH0MpJ3Zx&}%Cz%(%7N8d2N7q~tav+MNA9eP>Ie#!r%< zxjs>$F-7)cK|y|1Swqj*YC)xuee@A`UHXNCF&ZIt^jA7;*VW)&wo{4R~x~# zh*tVnHskk`f@lZZ;rz<5S|oB*L5?Kwn%jO(W-&c8v2oLUws}1_kP#fuh;4x zXg%w{cSVc)SbeLm-SUeyy$Df2)`Xjh_jtTi&1;+P!)(P_9@S!_^1spqF#Q}$-NLwL zKOewel8hY-zOH^grq2kSTCX{+ffZRk5MDxaPs}@D@ryihv@pMh%th#N&Y2F=klnrN z>BB31`q3p?_dJJ0(TDNm_vhDs&8T~zlTxvJ@f_{D!L=7D#+w~m{zv()jt8W<{M&Pb zZN!>lsDtEM+)@75pJ0I{+fVj>cXpb7P`wM6LCE+-e3IMed*_`z+c_i&n{|lW`WMy5faX8({VOzxOcQ>*VMx`PX9ZLVJAXjMZ}`3$bgQ`l%8-> zq(Z$s`~s?pKf0;p%q_;G&~u|6ztHVWo&T}NOQGmJ)JrPUBR<^!NT$Z&iuFVVQ$=#? z#=u^bV*n`){a&1D5WYW;h5A#NPp2U=0>34RqZyv|y@Ow$9>rOwCw5>q9=3MO`~ zJjXlb`u$yN^%;8V;Xd-2sZz#tawt=l$%n$=(eMgZX*9-Ey+X${-}Yxm{Hzmy2|rvU zcv$tpI?p&)e|`7uU3_H9-Ri%Eh)}alAE;SGDK34re!XnaD;vsYkPu-D>g@Q?5xmaP z8RR9rIT-r|`&ma0V3RvCw@pNGE4TgH?VG=_#Wn6F0Nvq9e7Bv9iF4_i2|b?tq0qCiySC!$mb8W+&}e?{q9@yZOkE_rlxa@vh&zJ zYIT9Zrf{3APd}IEqVAL*bzNPqbCDKPK|JoVwtaF&6{8bJ8kZvdNUCSrW~xt!b_82b zUM{K{aeLR@Ec18cb#9Cu!K>YReCBm(nX2p|mm{)dQY*Yd*qWh;Egt{RE5Q0wX#mMs z%260`6=8F-6CRx2AL94W`+M49!|rB+BjF3H-~cOIDq^9|CyY6Mxb54>V86pGNw*nM zhA}aiiomF^N#8cR_kRC@TD+y0Zn0M<8J2OQK)tcrW0*)Jd0jQt zNj+?@dupcV?Hf6)lDSUeH7`}{I<{nVPas3~4{_0=H+WA#APN~PAt{O1IplJXpt?}> zmbIlBb0QprD?Fv-+^I^~AW|{m=^9pfIU(#^(A)eV(&2n0;sS3-bv)SEU2|lrq@he3 zoJ^J535V45F#^v;W9>?J5^S5Uc`Y4Ko6Y5}hBzf9Ib6~YkKt#{^9GTzFfC3kIJ$*_e*R)56V z)IDzD>(wA&W>4As{oxAfXZG?W1>L%aOhsXu*YbpW{jU#y`99aF=>b1B+1}84D(ZGE z%9$dsFtY|jnxeW?`sy7MsCmye>?IUJ*A#fZ9SnF9cOABTqx78<5o=9(=|J?J0F}jv zAeTokR|(MG3N4r6Idz!u`g!8F5$XKEG7Y^CQ7ls_;!OpzznHz&=B8fmV7!j~aU>o9 zZTYb{^9zclKNu}Zv&rMfVQk~zZHZtEno&Q{b8(RVZ@|UiEwcm^Y4yW&)wJQ_m%fGE z_X~A-7^{4(C#e5@cO<_g_9yDQSDb!V<-^tt-y?se7+qEHzQ}kw!=7}aXT9j-ckA#N z&CxX|jHE>jA#$wMWxhQ`>1M6(O&!BxWa1O(taV#St;v0_@}0sy-q88v!ITJgUYqFD z$tT{`u(*NPj}GBS5gS>2k*57jJK>1) z&pAEkFW#+v@>gVRfhgghk1R3a-b#;bE}LiyQJhV1GHVZDBbg_J4{Ykw=4u3;SmGhm z$Kly>B+*gJ@B783&0=wvTQKn~C(9VaJ!tZFae>lJJ1*{OsGGZcXQ&kd4uj%LFp3Pm+iov!zvg|KtyM@fBY7-ich6KD-y?`RAe3*y(A=Lfc>%SuDN;7K z@dCt~XQ9v3R?>%tcfyvTe{%W5sveCr3jgI2f-u~k)Vr3I)mlE5_(Z1^2|X-^HU@MY zUcXd>5B0qKuEiE9Du8E35P~lnQ^`JD68AlK{AM^moaxQO@a0mZqBPU-h|pxJ>cTK4zx(K{KX&KeB^Y9B@!Kgz z!3Ey=SsLRb4eo~F{1BSsk75nqlF;K9@Fb)tX3Zt-31Dw(GE^fga{h{8b4NlkZlU#J z%K-g}i%ns6D>vkp0^Lgr4kyi#8Zu;ZKd+IKAe{s zW;Z2;ot3Src>eh@2l^Pn<6M@Ez49pR39s!AF-$m@NWZE~u}b6;UJ}aL3My`&Eb7H6 zIUoP>m}z4;w_S8R%QD2hgjZaKb}w@yv_2kH~S-ETg_$amB81qI}bDN)@A zYcFSuEc?^Z=$$bc7uP0UU@~QgTcQsv-;W9_eLkZ6e@P&(!=jutIiEj@##&-7#bi*B zj}kC52Y5y6UJ<=y-!0KvZ$sD;ol6bnrqO1Q z)AR#Ml&+^>Owm3*xSFh;d81UvP!|TIz~k)Xev+7VCcaXoZj^{orC4QNW9hqceL~J_ zO_!{x;hewdx*AocP`Yx)+HtL0jJj@A3YD$J$pc`H(xrvMY!Hs9IUD~%)8P!(%Y~P6Vh@x+1Qz&=Q$V5q|qKpOn~C7EajnAq2n|OGaPXHIw%C z%~`Y2sIlWQxzcMB#pQ{pbW=WtTiQI)svT+iS7=+sjvbM0QA_kZsp#twcX(gUUgu2sxMqi*q2`WL zF~@6zUA|_BE3MLCW#k(-N&nnw2@`$F6!g~4_Y z!=?v1UJrR~&zXzOtw#56vhM4Z_l7GLhQdyM;74PNr^|Mk$lXU1ScI^KyL{`p zSa!YM!53ym2}7+fqwXk^<^J-G)qSt+@77la6~3*xVYP#hctJxU{WNHw?0-zQl2x$I zOonuGIF}k?AS^Rnop@I_Yw(>x>8{ETaeu8;`jRqi<3patsPy+3s|HDqgjC^EJl90` zs=rsJT8K%bNHv>t=GLFbRRd@)(jF)6P+Jf_q!NY0xWTNbWz0PZJ351aY z73%3#zn6TLt6*_=|7W(|D+9NVe0-*-{EeNHiY!s~@Lp#N!RTh&OttyFw99`^q4EQd zR|-0buJTzUE1cIZ)@NWn1y`--DqQWsUnfAo{lg#mb!*=EwS#br*tHjUD z>+P&!whmn_J)E+1>MNK7vM2XCvv3Kd?dt-+304kW%`>j>>P|wfe)&yH`jfcFf4mpP zXJc=!lGD9JV3B6l)o`Q z*IP32by2Nkr(w-1`#ay#ju)}owRxeN(mU`fbw0rR_;p3naVcUt;IJNS@}0(_)GmSe z&2U0m!V-z;r%-BYiQgLS=0;*ut~|`ld|xYx-?29F5Rq?RR5@$W^hSWsP2GKse=Sy> z2Ylp5$K9|@6hC--T>r&qUH!CK)m7E3vlbAHyXd~C z&EDK*2j1&YYWsN!{Au`h6+P$Fv>gjYe3_lXOV4UKtK&O}q}EkCglu~Y!7*Scfh zfTy~C1$ybX#mEwpD3?rBke0)wT83QOHob$Z&2%tzp0$YaI@k0dV*2Y|GC z6EX=Md;?Gm4RnoyuxGsDNM>W;@fa_CAb1C#Lj5tGuM=@1Z{gSfsKq?2KlfQwS~;vw zysxi0#_PaZNj+lyfDiBc#*7@VrpmHbuNSKh2j@N{f2|)^xxMDTC|+=b>#UP=EruYO znO|{5Z`$Uh)5ualc;;V!)mc%C70GngrH@{`vGSTwe!*NQo;8vzkruf zc`Jw8rktUd@lYqYd!YH_ey}^%vgPYPswF<=rs^5XQSYoW#y5uFaqsP7adwLh zgPV7h2jmrSyo-lpSf7+(IFOKLMsJFAeoK^h{=y*A9~Cr8l@?WRru^P?&&iSBq@ue7 z$%PGY30y-AP*+W9n^Yh!^v0C%M5c^0RJGndfxEzeY*oi~MZR~W9`9849h2R>m88r6 z9O&5}gV`o5gBndIXlk#KxLDS3A1e_|4*LB(!ZStDF6iWPx}9ujdmIA5fL;sd!u0}B z;-I4R(&uN1{sj{-G07qZqkxjpmZ?6o9(uKKY(?0Y-*NhzsvEg`{~oj6`nX+YVTC*~ zZJ~`pGS7H1te^x5{5f_#9s+z5eHV{o?=k+PHV%9@Gb;L?zdbX@_3vf3O5vjfog|jZ zsn(}&ByN6N6&+)k4YyajI&+%U8ho%N_Rp~Q!1})TdCg%jZ!I{)jja+H>r6Ny>whEB zAltIS`5EtpLVXGvW>!iOXUd9Y#)R#<4eH(wT-RQ#-p#!Jm%Hdh7u&BgHs5`tYwqrt zn9Xe1*5^mzO2_(O}gwIZat&jxj{xahqNzYHZrR@W?A9Ft_2IWa1W>Gk-XhpK)~a zc=uq%klU|6rIG#$=vA#pP(xjf95Oo5)0=&Iu4FvaG7Z+SA6QO$Lxy6Cv~Yu+{W;t2 z-BV}(bG~;&a`pZNR|;?wY=}V`<`tn2z4Ptj`(D1d3deH`A?gJX)zdnF-8fv}#dRc(xs@p~&)*F^jN-Wzjk z@=kPTY^m`4T^N_z*WAi9pXw2aaWnHRbj@XF_LC`+LGoaQtb~J76vQ zQ*ZZ5=9KBmfFqDk}{VNxZ)D$F4Q-d28hJe9lxo&b*-g2Kn(#^BN52YU96L zSig-A`y=j;nvgDAWq>0Z;+z}Bw-b8T!s*?5nCz%bTA(&ML>ssrhbhv;RRu@7M*1P! zT_ZE!zFhL>!eDnz6AUKP$w0orPVLz@96=S0?$qNFHN#{_?sTcc2^|K^XR;)%qY(!Q>Y8l zONOm_UksXC<=2Jr3nM-c9&XRP5su8`WHs>cm;RW9Wpk67l2*v?nW>7zZxmQ|>{!Bs z73voIq<7DYE-zQqlCxH=J61y?!GmR)sI~0s#{K%>sFWkKE2UX!UCRiV67& zSL3^LwZ>nXzS#w6QkCh8%=wFAHtLkTNi%mhO^WkwxEtG?Ow{TRIG;P)>#(`cJ80i2 z{Xvux!XsTTRSHPq3u$j;BTD0Uet2y9C5ELB8*9Bo&)KOUUwX%j)>#f}rHTm9edpB| z#F6XQx^8YL@^O`l%V0%Swi%_X_*zT`9qmk2CVga7=3jlCHQ#h{WQ$!|=Bh74Z(fg0 zKKgEpQ59jLg!A60aNWY^Igy939{atNE+JHs=}??OrO!6HsrQdtv*)O4h%U~F;m2a) z(vn%DBO=RQl`dJ|_BrJSK8%F7&cIubxgq7U!P?wjk5DcBRTrGK-^C;#{$hbs{;Bv% zw}0oFi1iTXkcBPlQjTjy0-V&Y|&%wldH;pi#^}fdU;LlJ7qrGI2*G?F9*?$ zHAMuO5{V2Y1>(1pG!Jgi7iQ9yyDxSbOLKRwimF8E`fyBDD0fPJ>Sfydfs6NDN;l%I zo)aWFsZzwkf#7Qz>XhY951yLaT=0p2Cy1s_9A-l{Sb_enc0<^!$3uA}?K@!F$FR&m z|6XXj{|R?_F~Sm?+&%fvS=pwHvv@MH_`?o#7W#RF9d`ZP!UAi{<=XsBO#nG0%EHoh zt_fnOgF{I+_;rrVd^EXJ9?5TTfw?#Qf`#)Fn6eV@Wj$sKOB|4NEx`T5u?X(l_lmDIA$lt!aCr@~A1r2ASaBO-bJMIpb#o8*#L zI_o%pU0fXl)Ul`rs={>ag7YVJmBl6I(rYUej7xOZM)u*P{tqz-LKj_MTyi%}&fK`3 zZH-An%#-v)zvEG%y7VWqW&YdzrYS9X_OT7@=KWZX1emU{_jgg~b=Z9)YQ@ucS`zc) zJ}ynBv|fku0eCe5;nmCH#p9p9yb&%rQT@KTT|5S2z!Hg$$N0r}HY~b{W($jH3?7{& z3dPCZlA`#j`%iLyzr$6UFl)-);MU@w1{Hx zk0dZmXJdmlLBQx?(aMkNUr^)r2;O)__#Mf#0UP~pD0MI|V#R(aWFs00-EqbqMh^X8 z2)mu0TTWQ0te^P9yz&?l#+pFcX?_c=XspH$fnOgri>pQ+XSVtac)6^4bFznMw8&7Z zukO^x$L=r>~^|v%-8|aBch@bTcq;GbDf=$G|`d$6Z{|<014Lm}NGdcQekazU&WGG&0 zn0G$E`Lyc17iWI7(C@xTI~yw7&4CBkfnIGoi9^NW^ zQc--4Q4#51Z(m!HecF&_wed8z15l3(Y`DT-DbZsG52h?d`*8313oo6b1tRyc@E(q9 z(%_b3dAawXWB%@nZ;^p>&sw99{;3e$OMIuA-kGH~f5f<_4|ekN+}({^wv?@gTdah@ zQl;De(^r*R7GnAC3|9AK12Egn8zl;oO*l!k78UyL`KdJySM-M|UZQ}`5p=pDA20&i5IObDh#KQ260QZ9Gj)R=v5aCnvyF; z%h<-*>z?U}?k{_h`6nKojWihXk%BHR5(ErZL)qx#jaJ>-kXLh~CE6Q>wj!Z7x}hL5 zqS}wPZKfDKnKyLHT6y!q_^keD9*7A7jOR{V*<(;2QkXs>vRIrGuyB3%m9TyMgEwJy z1l?T(+qIRxmEL#OvDI^aWu8O>s_~5l2p0k2!Vc~EG5TlEKTOfXo2M2h4(hEz&RIKP%>td(Narc(@G-(m ziG{HM$IbSt-8S`i0=qa64BrPkdDEH}!xo`gYtXE9kBJGaRNe{>RP%S_BOtzx&PE`U zqCs2n_}ub`Dy%vyzC$VR?hb}}@hzlFDjfNY;BD-!$5oeK^KOm)SXy5}s=;IkD*1Y8jXCGEnt`xH!f;^eavKiP1|ZV;6uYEhpcYyl5cm-> ze4rJH32hB?FR%IO`yFQwjDOqNmo>Ft*L2ltKym?;Xud@cr@_6SpI0&8q(rR*TfY0X zp1lKuFhG4R=nfO0nf`e$^i{0P$`qa3T>seb(%^Zw7%TuhekUSjBmZEqzjk-OG#=FA z{x>VL;?2D-87QEP?64ON|4gO5tzqdL@60VB-@85>UY^mbB5PMgeYkbs4#Q59MW-IC zPUXPA_MTj9IsAOyhB|_XsYFIS9~dHfVBV&A_%)pq5igMqlP^CE5HDF%njbHbmje+v!B3syr4yc&U7V?vub@OjyRx9q+CY`R*v8(Tb()aIdUsLj6` z$E4GN^9f_ZP)Z%7)9@#=wYqwG`U~rwfrlm&&IWea9`4~SCB57DB>ztb&j-Cgv&lK% zR*2*p?$pi<$w&d%$y@TKrE7o4*b;DAcdhJC?lZbhjp6r z0F?hvDEk@nLH0=d-p_c>MYcu#%~vERv@@ckc-?pib++?8PfWK->hI9qe3N~mpZT-j zq*yKn3?5P?+?*WS0A?D7UEz9QOgMBt_g+oi)hZ^{hE{@G2Q(O3p~(pko}n!9uOpee zTM{k7*9uea`E32#O3fg7b&6}fI-F=cpvHW}r_4KrdOe484nJQS43Mdi=UdUZOm z*6d_@%q)&3Rk`SJiX*9(70(Pio2$D;q1v>tTnULTqcA7M=H>*BH#kQQ?9e#T6-@pmGs!K z|Jy^)p`nDq5KVly4QS#|HKkWOPi-7lzwG3_#KfakHahLn*v9l=iUBqFK(wGR>KV6M zs?2p;&!U@U#dFp{pXXQfT~5A(U-5;b z*Sm08;8ABBf^>$~^5j>y%N~$65#AAKWsf(O`DqpQX>~h9BIt&Y=ddWWwA{l^?Xtj1 zmro}nK3CpbIq0+$k_IVtGn5yDow|pLSP8NWeysD8qL-DaXiw45-QYw`Q`Qk*T6 zqU#j&?`5QKl-AwsUVnc5#-SzODpuRSt$94Jn`}0pI)ZGWtK!V{+Hox?C!>0g_1(SV z_Bto%E_AHXXe~>227hDoYxJC)wNy+S6}+zn zqMPSTViE+7q!-DQId1Fz5^Y(r4QGA%ceTCiNm!(w1M)2}X8pwa)HCs|=xFZ-d+)xW z0+&&tGMeY`;91wu`vIqG6;M8KQ(4pJK+v1eY#D^p7WSMQC1IAs(jDbFB7Lu=()JUe zRDd-N!_M(^tRSpXPid5@gWcp2QQOr%yss&En(tn}pxGmBsmy-K1H!;u!NfS<=Jw5k z*+nlC7}=ZW--LWtFB1Bdxl1z;n=y8T48n~^u>kpVtf`i&T-TU1`*q<>x7X)~FYk^{ zK3PaUIdWbs0pU`;4QQMMfiQ-iOgmiZna6Xo)Y|_2S;*teD|&iJJjv=R@YYC5#Cq!E zsI*XOjcm~79TNk7!%rv1d(C858Xt)npDuGWL6sq~g_TVHS_M9aUO#i5Io|cwxzq(W z=&-=A>obYQ=238-_iurjmv~DVOm=fYdA$om-qfSX^{@xh6Pr|=z$6s^S8@q zlEoi(Y7pAO!J3T8fOs!V{OhCcs`t<^3Bc~sbfbOq`rP`FiC0V$)lb{(>%#sAi$x@- zGZ{z4qLt(gaw*k?N2b!eLN1uUeOOy{fgA5Y&Bnrkf*&uL*ywCRkHPX!im|J1tj(5w z(Z8#0yXvH6!_7QFxS491ySEjIp+>jD_YG^ti(nZ?#RY~La6yx0Qe=k}XJgfVM^xE) z!a_~yYk^Rl-4$Mv=^|Om*I#6;dg}a zMv;N)bA4p%o_+@Befb|lcNa7x0$FHo5b`}FQbL4keY3pgr)Oyn@qx6;x56#0U5O1@ zfC_Q)e0R8%@LY_*0z-xM)FoUGC&3>yCX}5b{RvXqwqiPV`ok0BK|3R2L+=mboWT-W zb=--K*ZDgh1Z?=-@Kd-a@XGT*t9Ukx>N4yk=f@FYT2SbhW0WQs-u0IDA7E`^Crq3f>VxH6Ban)x>c^ZH6S3b zXAt`-&rILoc?dftYSiaN@cu2c*0mg{8QDK#LUR{b+$in)9qp5D8B)H{duH42{L^Xr zdR@&ILCiU?0Wrs;w3y@xoG@;G5ywGeW%|S_FF&I;%F`yhqTGsQLmy^x*7FNPr5-H* zr8Z=VV{-j$W5>@}*?-ioE=lrI)w}hmc<_tn3855Wei9@v022gl^?+UpIe}KeRDPek zhYMuzjHxRwBLkt_#Y+*1YZUjn9uAmO+JaA5dUbloXSPT$$qg1%I1vsml% z4ej$EdTuf|&BF!j()|fHocvN>lLb8Qnhb1k;cfVsM8=+gN&m9%w6#@y*oqRHkniVs z_ZI5z;rp%W4aY`)tf_dUy+MvZ8(w2h(a}f6?ZTpWqESwjYY(1`(4!jZiDTk4cBmND zNCeZ~I4VVgV9{4XRDljCyO`X|*G<_88Nkx6uM0dIds9kw8j()UzIu5#Ys`72BXA@d zRidp(tbfBV+)92(sxFb5A3MPN?`ukbMuzFefZZD>DY_bjs}Rc~&K!b$_tYquqAh_!}L8B}L!Io!P!_4(;rvBWbmK!i5p2_?TQ>FuM4znmVDm8ROwB&?sCoPl~ zT827%hT-)MeR`XAQpWD<{ysymY=?8TpvPe1t+#wmj7Wa$@dkT2d_IHIZ0qB8NpkY* zKBKC2w}d!42^o2sE*)Y58l6jkksF+YV4Lf zl2fv?@&h-R|1<=kMX+Q~491;WzOl?hnf~S2=aKTdO)!3L>C}}iWJ+}1gJcE7_$M}k z%R@A6d*Bmiig$hJ|3r4abs%K|qe<$S$3OPBdOoE0NNnZFEsl?g4l7(1usJMgNj`r~ zc`~_O>|nl`&)B}nCvm8jj_mN{epFTrn-axocmmil=<&Y##lRu=53nN~xXj}%ni-iW zTvLWLsqM5_AK9k6Q(#?FRVK;qQIT40a0dqp^JrZ-ydudS-bUL0DxO4HLb7%-!T=pnrPh(%HwL-iV?%_%RJv`e zStb%T;qfeiokF0soF0)ZIEMd$Q8`s~o2s+NJHSkqeORn*=Ws=i=nnU|7uP$nRt9<% zN&bO!_CMiz=5W*?WxKWvZ}r!#eyRiMWi+-c@oA6klXtHA>>!=2Djl82UD6O*frWtcXw!YhVGK5$YAxY9hqyi<(6*Gc2gXPdIq`3|3nc-a$+ zZFb)IE}EK?-}m{93r*%Sz9zf-KT3PM`^tASH~v)q5xGk^ zgq;zm6w*}Sl`8O|8MPU;_O$Ar8=7b8EKrZc9?1>omK5cPoo$`^(In;Z$lXQ1<159@ z{95PLB7?dTu0%#OncHAWNQ~BwCv}0|4@YtCN9~cD_i)8+ zedOyEuCBPHS9L4cLv1#rWYPb0+NiKs(0WZKt(uJ?N}O_%;5X%MZENSlvwWPH{WR3W z;bOneW88lncoEIyZpOh2j}8HFcmOX37TrTIjb=^NDDgcHoRd88gl`s07u!Q5>V8Ivn;C6YfLQH>Oe z6zUR;M%x~2*L|XJ+p3ZKFrl1p>uGkU+Hh;V&`YhYq)eWd?$)bCyYzB|x;JH<;Rj=h z20y-lMsme^9e`rl#g=T)#hbfCHnYU~zMCaGzSPNq0nn_=TIYY@YM)Q3>??KTSfbzzjYn7b*HOJ(|+@A3)=j=qgJ!R}+) zC68xjQE}_ooXjY<4U-#RaZlAwT;0PYw36#E0`Y#wcgBRHn&cI>B)L7|7naeLVQTd$ z6gAZHYY=4SXAW>T7j{4~?@30*<=Cdj&i>ww=64{Z2Imeff9PA~04NV3%wPy;P;|(s z)>=aAshwqw@_c9ceKg0w>2%-hbt+YDtC>ts)0J1PUWd~rOpY>xUFw4ja#N zgYicjR!EEXXNtu=%6OD9|*gc%hi!ufF2c3tLuPt7)&;U{0#XbT-j zyF{fNz1^@uzBp5RyMLakhW?-sG-TVZ;-w(@&{~k@e=LzBd4Hmv??M3Ef_=(l_=s$r z(r5->L#d{`{$Qg2ZGSqRYN(Hrk(U*J%6J{|2aAfly6Xa$>U0pbPv?*ju(m5M|ZHnRF97))ig$M{ER_iERy%3`Vs7qoxUGs~9o4_Mw4RRH1VM;F>AgN<@VKkySs?YnGAEOf*!bPM`SBo*ej>5~P8k7JC0#P@8lc9E+6z<

ND9?mYJ9{ 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 A?f?J) diff --git a/kos/resources/setup/innosetup-wizard-small.png b/kos/resources/setup/innosetup-wizard-small.png deleted file mode 100644 index 4246599c3ab08497e75de312e62f3ce9ce6c2244..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3640 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 aQR6Fv-bboSeyh;@x8u2bxt!vJCH)g|7N+n3 diff --git a/kos/resources/setup/macos-application.png b/kos/resources/setup/macos-application.png deleted file mode 100644 index 476d0f6c57ef23c18fb68ae4276c73ec3c16699c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76237 zcmbTe3p~@`A3tu!gc;JsGNrkeFq_LZ3RzSxQMpAf*+sdHxt0&p$|WPpZMl|D_b8N0 z$Tfx;-9#zVgi0b^RN{Z8zTf}v_kaBUzyIa^c=Ty;-silo&)4njlj`JXD<`u-Modgh zZqIJgelal#@KHi+6%729psLFPe@WAKdoaYrWR*mJAYvB^mBqv)PaQqz9^vj_PxKEB z*7qZa9--*R1k=H4F)?$C7`mT-ASFWW2!(buWIOs(Lo-_KD0w^D&Da6sK)0p@9Nm39 zoO0l}<3a!9f&K(C+G2;Ac?=P35KM{iQ;P`>3Skgqwxd_}C4$eQ9~+?6R+dBrZbw^* z4peh@a8k1l4X3D?>Kp3$V=x#sQ-VIh-{0SeOd;U5sbMi#tN{jRU}&UgXhg)A60umd zzYiK57*3`V_mgb?9tZxl9UTx6K_?m*L`O&KN8|KE!)XSF1OmYTgEhcn^}q@}Mr=rg zUyNP|L;XKHkSGlQ@T2sIqoE;cq8Vq&9X&EUXytISzX2tP5=;q+V1TuT|6NND2#pA31cd(IZ2jL~|4#=1={h+4 z_l*A~Ey2P6J%SOjD+;jjS0Mk()r^C&bc(@#3L`W!+@G>53XrKT;)YJN4yX70wOuFeV4FhD2jyA`YXA z!4WZ-KT91#$w#TN|MOBqql1QEDS>EY_&+TLc0=}y@cVyUO!g;IL&Jmp0Lw>%{b&>e zdI$}z_Fq8~twV!C!@NAdH+n^LGp2uCPJ|9PJj>K`f6?SI}U|9^acU-(hrUVcIUzxM$? z@PBgnpIxi~M+~4m#$ONF=|}tfDd^}95t|}1ai&K8{y4xF#?Q}B&j@cs&?Df;etIVU z#uQV3BU8K|j$m#eatH8|+MiEme|+@+w?_VK8y!FaoBnUi@6R$uC^aJ5FPvgU1KRt4Vc-V;5y2l$4F0c7 zt_bRXz>)tf2jC?7_+MZI|M)MoQbGWph6CWdYMHJl22uaChh%jy1~NS+#iYLT7(Kc? zGig3qXzDWa{>J;_;#<-(>gnP=M^2s6SmPuiovoy{Ci6~9LygD6j@jcMAC~mdZl?>6 zH!0tCxFd&GV6PJQQ`AU2Wl3--e&;^+Pd}lb5gMJBX|x-6q4?d*@tf}x4nKOO_~*%L z(nJ4!PI#04KHnNs{yx*Of7dAv&;Ms#)#R?f>&X3Sf7fw~*VoG%H2#*)OzTY)J2))O ztX@{mOw>|FVjk24DWLR`W{e`7^oX^%7DNjI$EZO)PiMj5m{Q|1LoO_jU6hkrz4jt5 z0Imf&XGI%;mEi)|PuRURg{KFHQ!oe&Vqil=>(Dl~fc}1*kez*{oPIsfMQO*md5Lsqhhjhg4%5nDm%Ex$on2qX)OY7krXW}h~ z!m`VXs)L>U)K#@#**+wk+NinOs}=b(Qv|uVmz6@60X7!iVcZZl;^*x^wWl=}%w_2Q8dv_#ymniINXU?Y;N`ywrxF zlup{uPBp@OIgUp!Jzb_5cZK%leDy$EkW{tw%(e9X=a?r1oGN=YPowTKZDfD@HAa#& z)l6A)A!4-dUefU3JDRfAl(ze{z+HNi>IO;_l{iIF)>78gmRuN8qi@~7D0*V&m~*jb zpclh+GRsCj7{G;8Tkz+zd0M=3blU~kehPTY^&+x?OM4&r;6 z5<3v%gY*YnxeLsF@aUGrHM8Yy0dl6kP0Y*dDqhkrwc}fc)S!o%T9$!@If(}@SGu%d zT5}5cK~h376-lxhGH6|9BKgk7^z>D-dR|TMB%*7?Bd9?V%Ih=bv`&XrVX9gqRrp6f zpbUI(iZ;P)VJkgKK|BxANRl3u^~bePAqA4RY=xfoRQXswJTMo2+3o*92rppd2rfPLr;EW>YkFrkrO3seKA|LJfz%hex~7 zW5k=_(YbWbVD{=;=|yFlBi<@S$h-`kr)1$)wq0#u7&8oFt2LlJvcL6FkNdGAzmsOV zLP5tn?YgFlS}OvQyC|M49^8xU3UIn$~T3)M?C?3?nUI88*FlCj|PJljHlDzj5tf3G?Use znL!-bu+5ZnuX9+29(@!QBsgf~NU9@Gqx8mRdhpn&j9tPQBUdd>&S02e@I`)LE7Rah z`olp(Q()dn-5x4t?Gm+L*gWDWIQ0kTEaa4^nH~he_W#u{IQ0;%^7Pbf{ew#97OzQO)E#f- zXcwDzjuwR95-d1F^)9UBy-HE5*W?#0F9Negz-Y2GnbN+>b9^_@>qN`{C?FZ_FviG4 z@14Bm9ex8Y34V%K#yn|Ly$6m`EUXrHqG*fpq37YmmOcf-rllu6{`IB+z_YeIM%s&i z!767$phW*Q>8FDt3MVq*J@7mlK4`$d06K$-irF?Buyl@Jnm2YquIr-C2#ZlN(gHst z_yXVC5<8315q~-Xmpi7$@I(4G@eWYg=V6lHS=Rhc4WUvE(q|lYS{SJnnZ(#vfJn$6 zleL7dL7Wd7Img~-W`r0-dX|S>GRBlj4U|eX&6HUb!i<;eP zL{vZ_ZlrD$Mdw?X>ThM_5^Q10K7FW>QXH2#M}cMYh2kSJ5tj6;sdQV(lIQ?PZ{i## z%m`Tk^Q$OBY94ASsvcAgi3&gs;tF|pApgP%NY5fGEFBOz9+(OOT--k}<(J|LVo-4f z=E-1Js!imTYuL?Hn@1iQn1YL21dqj@OXi17iKE1dSE0h8Qm!hrU~#_B5kaAfRN-wZ z6YAi*jOA|<+OZ87EzBMd94-BJcDo4#g)RV2SOfe~93q*=oL&yZ09Hxn)a6&2saTgZM#0nqfOWq*gqSiewbY{9F>oSwc z{t76BgL%c>e1h_t*6VO|X5Tw0WidrM3yYtRlblRrpP-Wg8Ek_vDN}xUDShHN{{WL; zw8t^1-VlP~+7*RT%49QKI8P-}__wn+k&fh{-JP4LL+{yp0!N-nOu>OU{i6_NcNX&wnNOwAM*M(Y){rgnWiP1d|+;3CH?czV&)b-)bftnxi2* zL+-Zog3o8nsb$IChiju|==iL$`ELA$^g?z8jtfI51Bx2lfXcpI5|dC9Q`AXZeJB6= z365XRmK(hA2>rA>2U{+RnTnm=)4AWG{jDe8hwg~uhd-BCnCOKK#Kjh+9{9XyhHcMpTh7_0!Jqc#*$WT-H z-lNEg=bJza4H!^pVesjfLG+C-NcoSH;_=R*R@Kn+u~OqNh6A9R`euK1FSuiOJV$mB zg(RgGRTKtrdgXxVKgA+bpk~6)85WG4BcrhH?ge+kOTWe&JUPn`%HpK+k3y_x3S7f;RJaki~UtU z>`29Yy1;(#WW``z@7TAXxXsAuNLaa*2{=0Hpy>QFP`(G`8gpxVDHCts@fi7(19h2r zyy`6E#KE!n;miKbXz|ow{!tj;gyw0&P2u`X1le_NNBZn1l=Z~kZ7mDK@af{2iD9j( zdU1_R3q4%IL6uFtorjCv4RA`yhQ_(+-CFv-Y{^MI8K=B*qTT6_vPIRSP;lnXs2Y!CjpBx0S$?x<}XATFw6$TKeT8K z;EfQ&044|`)UWrg60s7VWJ`@XjxDYp`Z;ZHyUNkeNfOA33*#6tNaWjLEf5@kFB3|s zxi_^9F4%+kn8+_^+k@Dmvb??hHcfbbc!5kq_5X3O@B4rXHm-qISp_<6&=As-RL;$D zhxxR#coq@oM#;$L--G3Kz>PCS5`x&0zR6A&i<1ho2^yX?9t<6$I}KTxdjO)cjN^0-ufe4VR@*sV0Wkl;$HR3?h>jyQWPJ+Cj){OHqIiURzG;EUW891cK#BVGz1K{cGN*7hQ( z%Z|FbpE1=qmpxa`|FNk+@>7`bd)3@9xljhEGJZW!<%wrVBM4F5BrzF&-ePWZf%&b7 zPi`FOWrhyXw1a2=um)b2SltJ7(p(q}xzx(VsyoUup-Mtuu9r`(`E8cJT~SH+WE~;l zsl<-Q$dY5}g>nF6hYbLrebz+aC6Wor9Oomi4^1Se+Lg@@aWX;=A6=AzuNE*>$SgLn zLI9)}!d#Ddp5kB`Wtt&IY9<307+Yt}h8K#Q-&j*eyeJ^BMb!2By}H=P1depVStR0c zHI=%`+_9s`2V=7xCC>JEi8T}1koQ_@XzVkT2pjjp5MPSAz7jVQN&x?;!W!vD`2}RM z1-J9+$drO&*7hxEbd@|Z6MjLgsB#Ea>`vPl4RUJIq;FBZ5i$8a@1ppMrhwe_`88rW zRum+h3mc9d4C>N^3J2@QyAJuA?_PSRHT$w?ZmBcC%o>0<6G(MOmK>a3R^l6*iLZM~ z_sx!p88dI-$33w}D`$$Ca$!ittfp4Tq><2WFU^UvG0?L0=uD90;J&qsk9Foo4MR3+ zW|$Sy>vY>+Ay)$uKLBpO;_X5H{uNU|q( zBxC@X#sN)0Te5_1d_)=CqJZE6}UG7unCWEu}{<;CtUik;k_2E8sDHzc6R zEfutuK>bA+3M3M>1&STmU@v}_^C{+az>`fLl!*^Kf5+pQtQRr2eAh{%Mm5^ori~kHKpkQe$ph8+c?s* z`Q*4s%-qT043`H1tHJM7->mRcQh#xj4!w#TFK_vzyg1+X<5O!r5;$~#NE{V7WvShr z>B-tldXHjv8dUDTf7lx@#bT-8(P(Ti9OKBKtAk8%9dTXKu#Z3AkhSk#XaBv|yw-(+Aej>syuN9TpF>I@Hf6LLDqcat9W0 z#4|!gKO(N{NLMtTKNK_iO4Gsx-yAX<>nlzc;U;P09iYbS-LQ1$sLuIZ?WL0}z2}|0 zBRTawVE^rXtxTB^ulIONNerck@JP8ikL`0T-QqofjaUUB3^N@l$X_csAL|~J#E|=A zf7-vfg0&I{(EzCJYLv{i|D4=k>2Am-n~qd(Iiv zV>_@AQ%pA4;3{BY7ugaUAQzZU+Z6CyvsceFoAa#AqgRwt8=J1e;JD^DkHvHx`CzLu zzfVJ$cQ)r+iKj3d3WPmUAbLA9kzZ|p zr?*x+iAfjV^`<)Iz+yp4atIjD-{@^_otT0x&HxfLQ5PN(-H*bKUf# z;{CG-PI#$mQMTYs7TzMxFXSZMWY%VL&KG2r%&-%lNoaKgYZ@K}N|;QA43`=Qu*Y=f zvg0SDaU5*~cJv^TB0i!9F!-~8;N#kI&7KY44U44C`1ZP#@g7aZ-H~1lp%wCANh#bV zkR}l1QCv$N524fxyykRn?b&k{nkpKl#=w8|l|?#W@ziOn&%*KAvhkDrGYL=o7mxBr z&iN?~Nnk;O4wj8uVboM2U_!B*O5Tir=W5sR)u@OlQ5up#S3{CZ`6G3YXPY+$#ZB`| zN>Uc%Xp2|jT_E~s0}kZdG%PK`zJ?3?H$|X|?aXP}u>r7iiGcbl28yoMQd&~CO-#@0 zC1vnTQm8UH#ICbML%6*>3=MMe6RzNF6hYu5p7w~dePH1O!gTSTU;t?(I{KhU_BDN4 z8%>|j;Z28D#g+26^=T}rQ8S3B$RU6}-*Uk!LJv&UYhw15luF1MdMvC;bTIZe))#~& z>~n+K#i>M-PzUi%JCHvtF0|Y}ZEr&&%L9#%$al9SlD#TN7OHRZOb7W-j&rPG8bIY_ zx3C(?sfWBn5!Y~e(DvTi#UF@-VnOX8;XWB_68Sl3WsXQmp>|qS@26DU#P0J6iVZZV z9)gK{pIb2j5{^!!UyTrHr`Mk^;i#Iv_`&bZ^B$fpDuYlM5dgG)8p!PXW|ehcZ%m?YMcB~<+UY6YTE3T&YvSc+Nd=eYyUB6aa7&)p8NhR%$eWyD z=d2*T)v9h!Ic`t+fwV*G2<0Lx`IZ9*c4{@@hf7V?^lcLClS*Z6pR7e?5J!sro+v7E zyn!b@LD5;A%Fj5VX85^iH$HY?pNQiJd5J-V&-%Y*w#!*eFC6wv{7zo5c(w2<2q0lO zi0LQNt;TqVE8m8qG!wpH=D6__GLClO#gs)&NKYn*5v1R~xWDTyOK+K{F`G5L_y#}Q zJ=g*Pk|qLO_<;0-oPWDWUFuMAY`~j@rpawJmH?heY}i%>gX!ah^`7HWDm#uOiZdBoRuiGFciIr!e_!))AaNs8vohx!?Uw> z^hBdT`+m!9KMjDo?m!l6rL-XHhzT#kdsSemAC66SP95xS0U2SI)aEq<@<^FUEe7?2 zLqZ>Z_Lu77B7U|WBv*WqBr@gBDT;gT=<&Kg@VSPS-h3Bi&!Un6J!u8Z(kdZ4=5t)v z`L5Pb;sy_opdXvM7oQ+Xrr2_(z)ddzQOQnZj=i61{K3t4vzZiVsDU@vr$D>akjl68 zq8d+JpKnAiJZn%-=pSpEuvbv5-whbNY77g>&O7`2<9w0DiOjkO5rcS;LxMQjA(PVT zm-)lygS&Fbk8&kpZC7G~DI&^~Zc7^9c?OiApz_?^>6KGYz5cd4P$}WL8e>nOsCAH) zrUxBv@ip3Oe)It=Y_@CUa8={TY!`Az@mN36Ul(B7(k>FY->{VVV(i_#YUn4*gVZNl zB8^UCK+M8o>J}|~eSAU~uU9R;Ijot`FOWZ3oe1igbs!{BZZe%G&k~KsUj62Mb(y#? z$dm&|a&|&6+F!t&srD9++RO-A(p5<;*(jAk1fl~`?%Sy#!5yQQb5jpM3nYMzjFC(aMt$Mo@7rZVs}c4xfp=89DwKLR{kSK1+kP@pBIP> z3Mk!4-%d_N`BuI)OHK{dDe5o5#$Un3KLY3r%7x*aL#(ZgclKT9Z8@K=6-_?0r?Y?u zq;T~of*7TIugAS!k2k<^frH^5V~b(5#V~A!89)J0Cq=11(RYlVe=fS2zHA9eL#}W( zZP<X7R%&Zuso#RXB7fkyj){P92^gms_K+jwE2z*T!G%JinHsJo*$ zoQ#?RE?74{JJeh&+xU(&UP78&o}ui31&M=Uk^Tc{%(eyv4ICS@ZVfL4~Z}+W?Ud6Sv}~Xcb!v^Z2!m^-+jOk%@5s>9I7y zcs9-IUq9z2p@_d z3yu7<7dqo7E=tFLn&CDdOZVx6JpxTFE%4Sw<5hR+OvYk+^==o~#reZVL;wNGX@1RP z&UGBFJS@hfGU8qS)ph(&#q33DU=RgrP5QcFeacL>5L5t3e|t zD)ZRCKWz+Jd=j0#*r737U3MyEIt(NwQ>XyBL&8#m!3NFZNB!nkqp0UaaRT@+;idSO zUf!7Z#2sk*941-*ZlaIW;GarueGU|6a=wn78FTbhv>Z|2n8gPPj$*w)q-&~K@wu@r zDw55nySGsm@-j+qwSV(Y>QKVRBfYYAv>F_U=)fObncfk{H_va|_eGf7 zJazEP@{0}4nZT0)9Ih{947L&(-TNnjanD{du?!UH%3f0H5R`>~Irrr>za-v#?oNaH z<<=7N!pU|mbXDLk()dZc&PIa4j;Eh_4EomsZwzaIt5xeO-JLE*`7W z{4s?avKHj!d}dH4lOGeF6YKFd?tlfuS`5^OX!H{U%fl4czL?joxjCHaO#$o@;<^Q= zzyT;+aTwTp`0GfR3w3DUQVMZCw|M?DS_$CP3P+~_Oes#R^DG`y?XPPfQ*R02`k`;JRHe84i1@Ny?#zmIr}rZ6CRPJWG28p&^i`SoAB^9#%O80= zT<)s_QUEfZhJftdJ$O-0YJ<1wZu5<0EAso6B-VtkCVntqKBMRJ0>1dBo7-F@;kZl6 znsmhlxL6urjhJkkO1otox2!XFXAmyZ6!{akkZJ?5liE<+3{qrlQg8b5JB?ZGqA*!B zHV5$Lt#OTN=G`-Uf`*qT4m7{9uj-k(o%Xr3!W-MZHXW-q86nl_Q72-fOtC9xUXtBF7 zNPq6e)&uueocF;PDCm6i>#mg4Q=0@YPG_{Y{%&fKs?Gwn-g?C>29-&mnV0b!{m0FR zTSVzPi9C8Nz4~YB)x-5~_nCRjj_w#0-X8Wl>4bW z(abRa+z>1%8&=4Y1t9solUYSalp8IkU(T4U0UZ+X#Fk!YL55WdzowNpzsWe0@bh%> z@BCKj(p@C|e2_9alq+7EUxfOg%Ncp!O@CL~swX6@UKq{?yK8{m z1(rzwkLueyK1{AQ4Lh`A{p03gEpj(_$BI0RwfBijp~n$!XiKD~Of!lSA-d@a?MHf}pYm(-_?zLN*a5a|h<~}4 zRzF&aD*Tkn@k{avfo^&azod?q-$^2GVyNMtVdgV?YZim&RSjB|sVk>MS;983;wQ$E zwHH77XX!c;mM`V~aA!s)vs28~KuG9G)N0u&JnLStV3w2`+Nn$x@j|{zam3|!$9dMj zl`lwJlCAwRtzFPIDAf%tcRh%n_r(m`Ta)t2?HAWptB$e_Kdjh@5xr&??WorL)2)m< z%=Rwy@&n`7lPTQ#Gbz(J#2S7;jZj5j_1D=9VnrYn^eh0B1p|#|(?5=-`gm#&&fLgX zj_Vx51DpD#iLUB5lH6u&F086tF2^z`Dt}Ds=!7K=Q?QVGtj$qrZd9Hd&Ju^d)!0gI|_iD!R1Gf1(3f{>~~~Y~)m8R1##ac9 zU9LP|@yn`x8l`j_hHAfRRQ%ynoQAFOug8QL+@*Dj!`A1*zGCKn{>X4sdAd?Apnus< z7_r3iHXd6{hH;W#dCfzmLu|>L(o&|u7*Lp1*6}@K_2`DuPb&r!PgO{>FZ>YmfY4s1 zxm;%$Uv5KA0M@c!G%cGyh03C{d9mNqP1c@`$Lha6&*9$-kq|{V z()h^QJ9wV?d{yP98v^an+tZ`jNO)G$7Bu!PiVI_P^t?YcxiMrhIC3Iuc*RXXUJu&Q zY4fW4{B{-e&Y3kLKl_ zecy{y7IB4ez&jAv83!5Vi0tH=MPuFa=8A}lsCJYHd$ZjfJ0VLea73A6@_fiu=N9zqm-AV9Fx|#4dXjB|N_QShyFx{uOGg@c z3@l?rn(##ADAv!SFuoOUB{#n*kW3g&mshXq-9@@`3R={P3Hun{u8Xi=+;wkW2NbN} z(ceJxe78+r$-o%Hh`)!6S4Y-zSevUrJL!sT3O^w7O{l2O>sDI{|7bHb@*qEMI>uUR zpB~o2n-bsSCrc+qaZqzk#xDCWzf6-e=n(~e&~;+e?5c~q_tkW5%-oOo1)A_= zMV`X23+P#L6Zs$`h`K!X6{I~b#IiwAda5`a#y4Sw=f`-_cba?(x%tr|SaL>34(tyc z9TCQtxr}RmF`LxxzuXEsg)7v9f#h&$_(sON++{I^keT6SXki3c4+>SpQMs*Gqla^s zhxYf{nf-BgGLi7|WY~`v8qGJO5U;$Fo1ZBQ!BqA*NJIvd;W&8@&g=xJPOlaI_)Cvm zkJZGBOJnz1-@S63zilG3;}8`?y5dF}M~AnxY^B9g+Eny83-S@LKbTpiiyz=97WImL zcJiUYmjyHA44rOGCW0t@X|Oc>MsMd*yPe$V>pzf;Za51qZw(jT)+^-nfB9IHUg<$V zAJ~Ep&oY9*RNZbH$9>=5+p9~v{)g8nJ*mC4qm^%8o~w-ae7LW|c84pCbj6W0E)MP) z5W@oQjmd$A_zZbq!-|&Ra*5PtfDdEGB-r5}gPMS!qDboZ5iRO3l z?PcWX{d+YDb>6dcM0M<1kl8b<%S-T8kAaSqUgyA9NC`_>&(2@6%DVHGm67*mv9XObLSf0_LXn{_3MLV$t@h(ObZ=ejDPtvwbxCn@n~>c z1L*VrA=?ZLB@wrb8}(E^)K8Q|c3#EzDbz0jhunkJVXFHt>-NfpuGGXJ#)0mM@4dNb z5^d}W^n~Y0eIk1OVL(PcAR_~HVd1;M-Nsw=KYbxI`s!M~?*rk(yx`bqyI_k4&a)h% zdbsm0(nblX5#P+ZW5CP8tPXoWcqZWyY{yuNmpSN6waU>& z2zkqXp_oqxt!40Ji7qMJG5PlCEPwIp@ozOj8YKO5VCZ8cjj>1VFq+%s9u(`r1dTG$ z^ZjAzp@7)AwJ|^A`j@^7B}>XtS+e@*@C<;B?H$&xT8&YZw(pXqD?yty{$V#LbtsfJ zPwmdhf{n|^R@ZG8n@I7(fXrqbh6h}Fx@k9VN0L`OZF~ug2yZzq8cUn~9Nw|alY?i; zYNNvuS`Zi~2e@Qlnw|kvTto#r+_5nD@_Z(%yt!3vWJGw?q%8QUbe1gmg$1}@k(3(f zw!ypM@obIY)E_lwNpaJG*YD)*DBykmIGc99B^V6BC8dPDa8Zs`zX`R?5^ps3$)jkN zPmU2?MgW1pims+YikZr)Y}Uoch4?PFZI-efZ#FL= zYja#lfhA_uEQYif5;nb6RmnQ&3twa*V`T-P1hwAyj zm^6K`)k18?DQ>KS;tv@_1gxwhQdjQw;gA^XcV9eLqB=<*1E3p)ifS|h6AcRv+d={b zx|`_meVd7_XQ|C|1E->+{~{B5y)#y)=tq6Vb4}p_o-V$C7bVK$Xhzk>@9wFf~+6fbX8V|;vZYE$!ND&wZ$ zs@aeciSI!AcF)8z=eohBUuX4<8jb#9Bi7+uqE-FlSqsxdrP`;9_C<;WZN=f*3u5gZ z#=1(c?+(rO^l$?IZj9yEcu(}?Bt?_kGi>7@;E1G|t0~iF2xa#i4{y$Fr=8Xk7?S;^ zdCdp-#G&H-_iv9Es|N3luf>_jW!*MF|8fj;BD|$|E~(q;jSl{$qDtjoF+;|}xv-^+ zgVVMl6VmYDVdT6pwSvm0pWDzU@^~2`YvZbz`cQC( z27yPvL{}L^z#gUMIV|2bFmAl`Cw(T7Yr|XE@uRWtFKRX4m0R?Y4tWh9SYrcbet|YL zUvN<&YkTNw5MRTC-;6d4eg1g4`cKw~uIeSYzw>(V^z-|^lXKTkCLE^D^gu=w>XShy zfL{~;=JB&6(~ZTmZs1n-AHUfs2)bGL;XW^LryxdV(f;HCK$OqAtnIqUHI8`fx8~!e zZ*%^#e!@u{G}_P4C7sV2?VF{6$$;YUd5}t__JXSVpEOm2G``p0U0=I=RXXcgFx&k4 z(0px1&SHE7>466N#2FAh>W+YzD(?{E2Ckc}7|n1y7C~IV(iUDHR0K`uElbVCv z9P)cOBh*{vS3&bYbqUcUoW}EI5|n}Pl(G)@Lrj%5Dh>t5{C;sNnz(X}5tK6Wnye|* zAp4kJ+%NMyLMRMn?I_Xf`Z|75{kB2b*^XwOkq_pGQXe)PHK>qV!oN1-QzKJkd zy6=2sJ90ThU7(_PK`b4K#E_f8n8`#0;zR)AUO}-({{!N`il8vR{FsQbxUST5`;c)M zOMhx^kz(8nHWL75rN z)q*Vj#7g}R?i%dH>i8Os&b=>h*;KN1L7~cE7x_6yLYeG}2>s#XIUTb_VI|-w)~`n> zpL&~Qs;wUPpzEwQltJE<%B>g!6_{3q$I+ONd)r?^-Tx>uWkgpBrA)5y4Egb({N&lu z!=H=GTe`(DliRGnRi#XCK&(>}MG;r+MjWzuMVpHC30jcj9J`8<&j};Q&=)s8HYc&w zf9(V*-X3QBk-2k~%LX8Sq!g^wpdfNxs;A&nOj1T8yzYrmWy4cq#8(v@y&5*}5CBcDCPy(=r39jLTi zVXgF_8TZ$`vZOkOT`et?O)0{&)pMJ$BNJTjby?Sqr3dVI8Hfie1O8W{#r$)LOKGGuW+8o^I zpDYl){-8Lxobxcrt!V3FyW#vMImZanJ&D{!pm{_j-AuPw=H@1pAj|&vkz&10 z6%yj=s^V?<fW9q*R?qGVpiZz-r(f!v>Xm{pVoKQ=j#m>V5R+R^AdyGg5ak0Xw3n#PS4j+_TK zp>i4fimVI{poV*HyC01?z|{TEdwJ|?edMpRnq>YTh)BDNzk<~H}2RgHBJ`9 zI_i8%p(#I)kV)vn>#C5UQY$@!qIO=j-0_tzo?_O!^NCa2p7Co7Px*WhR3oH^bI{?B zG|F@eZRM3hH$<5?Go3|4EYO#_vG!4YoZ(!?9xKM<1x?$ivJ|g1h)04>;`|j(#`A1P zgFm?o`hcDatxxMgSRp}~g{1xFU~@6raFK+MY@2js^R zzMkJT*DgF+wwaL&$*^xo@p4Li!ai;|x1FJCpzE9c$Crl>W1Wb0jMT{rqoorcB$o-T zb6Q|dLw>Pq{A9OfM6MKP63k~W=6U=L(^-eN_-AwMo@*_=I4@~?HfPbzR2`Be+8xvF zyJNoUUeaasS2diIhU&R9ba@7A9Z z*{nCLisAa1?U%h-4Wy*J(qg;DYwzklPMlo!Q3<%j65+bh8E)(|WCxGu^=3=2oCRht ziX$PCqdtBT-c@7}))L02t6gwTVnUjn#_$E#+r!}2r17QQuqGZQO)k`1esRvG^@O3L z7P#sJ+T-Jmds}u+?8!_2y{UsYwnsFQ6!{bW_a(Acu5#ztYk0c$(wC>s_X|@&3OeE= zDm^M37F9n;7FV=OnLxk*z-=qgEZE0KW|-k$q9)Pmy@g^rH_fkyRT}yZ(uE)tK`K0%%FK5nKIb+XgM(Rwhm7J%X_1m_^y7l=HD*H%x zcg@AGY>;z?h_jFL8NWFD*y_!h>78QMR~$$i?*?IleqXSwS$x&R`?@l=9shuBG32%2 z+aW?WT%uGaakz?V;tD@=F5VgA_D%WW8Q(kZUp=lJkJH3G;NsisqPuae;?yKCWu5E* zE&PHQEQV1v4n>rZuIwkxtfv*I&tm!Fz-Uq^YJW_#78P3Xv!r@LD1w;|W8HB@%d(Xm!L1bCK_yu8*Ia4ZEr z^+uMI^{p$oUWNf?;6*s)J@LX?X}RQ(@YAcqFG^ey6TV!~*!sO6FM;aywTx(KDSG=% z+Q26yCRPRx=oVPP( z8@kN>k?!923h&k?`rWEiKC+6JW9*_sM}d;?5^mLi&#^@pmDU|EB$f*aABvFOEty9Z zvVM(QitUZQF-=MqVC47-7PWj>P!z9GPe{yf^ zpmy5d%1>L{gvK6l?Qp`#!F9Iz0`oL?4;l2FT2%R`ZBvER>*%eIBU&-GS3oz}6bin) zry@(4&}`4BJ;*l}L3q~Ttqtdavm~Dq&Jc&H+)@O05uB?~&xr^*Xn&`O@@4R2q z|KK+rJ%v7A_xZ^S# z+SM6+nY9@VlqR>Xp_8@dirPE;S5k;8*^pK?jwhbbIy9l2RPFu^c$*B4+I!~mjeZ^1 zHB?^CT3MHprmgvgo~{Qs3uZ*(E4PZ6kT+f&IzFE9mD?70;18 zvP#(oZnIYAk|bZp+V{5XW+ngv&}(cJW3?uBmdwnymbci}$V{E|%b3ngi8~BxM24z7 z$cOp$88nWyC`04Mjx!sCOkv>|B132yrJe75sMM%TfA0+^1?K&fFV-3`^+Q&N*TBXfYGro0dibc!TqI`tAds7V3p%=DS#$Ye25gd#^ARw8Cvl}$33bgvuJhJFZDH! zpM2Y+^=ca{aiV5L);y2t3&CM(kV!}y!O7FtDf`Qrll4_yK^_N`d3oD`{r$e=yC#sA z$-t=CP5AfO9Qy-SQa!`2#JuPHv?CK18{#CUGF6qb48m&a6QSVC4rSvkb>Sj(UUl@n zvZ%KZg3>m_3uefuNuy|N<}1TOfK1!$U>AKA?2hjuYq`SH09IiRd;^x@XM<5cqy`ZE zkvL_Q1P)GcleEErDb!8!K)=@sa@%MTDPC}J92%J<;37m5MBqz~bRA|JK{#taX|Q8p z_I^*jktjBPj#M5vex(l?ebtThbC*@iHEXNeDJ0nrfXY?|YqB>F=%RnSKbDn@)VXF& zR#0z3KbB4Ol??a$l6KBdk?`yu7>*i9r#L2<%(J0-{egYW0y<5FEukE4xJyDxCYfmV zsA#KoQKh%mH>Lh}ny(cr)mJ?2*Jfyw0w)y`oo}E!ar1(>f6Xn<&EsEo{_D1e*LkMi z-EXs+8SS7eFWb1*p$7ES`SMUOMcQV(bPGLS*crL^{@V!hMT7>J|q}QdaOKHWh+t_X5t!$=qz8{s#zhV6^A`6ESnZSJO zu*vQbymVtJE?!^VY%|*DGA@NajF|(4w-)QM2y%bv*5l4-CjZ27XMoca*MR<^4y?@Y zO@VyPVoP6Q!giC8Evoe?=x}+21g*X#b}XnXaO>3>QJGgi3Ws(QDqp7~*BMij>}vr| zcdmwxfcs zwHT9z4FA4-D-T5s`Mx9$L{>3yE%S8NaLv=t=YmGU0{bl9-W4ch%IcxP9Bs+IOZa?4 zd`xESZc$T#3nf#7qV&36NC(dNazg~b9MWl`Z|&%_ttL8$GfX$8ExO9Yz3@8+-mlt6 zcsWvP?0x}Uz0g`}+C|k|u`pj{d?tNFby&3rjq!91dYfTlb{hO*{Z86|o!5zOjpc#k zcQ$QT{88N(Q0T`!y?U62T4fn=m8YO8F1r2*Zj_jtsZ@)y$kAJrrRU2& z(3Kjsb7xfAZeRCok}Mf8W8O~Hog`eImrj-syRdovUUEse8Zt)k-n+fGmq%H#*!dP- z0E2ptC}QM{@0T;X6~!~u(czmZHF`CTar-g#4R3-IpP=CN3g}-4m3%9Nsl9RuWl}%WD@j5!& z1Ym-*o&|lxxjk0EEa{o`^Dhu>aF$U@zcz9m*TLIwF~2yRQ6O`*MAVt87vsUB^T$a$ zqgcM3anG*awG0Oh_#k3N5l&Ra}s&qZdaPwoL+tSYJ`&whCk z%eEd98Rh9C%H25amq}sAIC{F|Xpp)!T@tL|d0H86O4Fk8@j}_4>@UmpLs(n>dG1CU zxJqbKyswd=Keu{ikh(q_iRV*~amLxx8%4-g%5Y==ps!c10&P5DYf`hx%v?p~++5FQ z`UQ3Q{ot}WkYhX*Oa_K!RYPYexhkTLqW(#EP?V~JBX`D-y|&#w>{2k;A|ItwblLe$ zY*|Ob4BHIl7y+qD+$?WHK8pp{c|i^GME}+25*>l1=#~n1m#^}0QTfxy07D5(V$(;ioUVw>CZH~$1$@Ib3x9)v2)enXk$)Wve=oBIA0?v?A;^54$P&T{IWSH?iHw=58cgzQw$ci+Uh@ z%j&3J3Ek_R#jyP2?*q)$8@hCI;c2VE%!mwGmy|l$rxBmvHLHR&QUB~q`gRT89%OFV zR;_+ZOJqiu7+eJK!tEd{s;`2fxe z!j)Fhodmd^7Q?Ng79LYExu0a^e1yu4xOq~sk1iDr6fkV)GI8eN;HDR?T`y0$YKQ9lCV)mMwKv%p`bu|XKfz9*Bx_f2FFV?ya7@%h|gkbM%_!>TMukdyd z+bksQTfu48W_j>k0Za?CZ3}4D{RFpOZ{S3_i}If2<7prOa6($eah2%&tPxPaZwc{r z>eKX&**1@4ybjES*z!Hdd7>GVS)|rMPGC~1?fMYhiUFJTVAv+wTGt=GYRx=c3YTP1 z9(LEXzA^*@h^UiL>HH5dBbxrpx#u6fegZQQ53fL<;9`EbNNsqKkQqY~)wx#^AqCgp zlFT-%H|+AWxt$Uep;vAOe1QKBbXk)LjmN76@*9S9P;kQftfsQ(c5%I4ZyYB6Tt^kB zcDlaD4?EDK$uWLeAdCB2l+Yj0M%jg2x?-?%TZoB31S$<eF|AP@ysyCNI4wE0b_l| z6Dea;sgYzE1%8_Zb)g=u5e_rTm(R0R8Bps7#aw-V%Q1K6Pv2e@d80T_B|5mGM&7H8 z{+uIjGs%BM}i4(X9d^{M>ij z?oHcwd*!6#fOBtoQ{0XB&6gBM|MJAB(3W}VK!l#w)U+W_LkjdX?q$C@BR|F? z0yGV?R3}9VW%4iejkS0F51Ot!p33+A8=*L4WE>-$LdibXK}KdvLquec%HH#1W@SV& zPL2^}rOfP%=vc>*l#)YqPEH~jEhYV~r|;|c-{T$DtwjbbP>}!z@B3%{;X0wv^Y>nF zN32$WBGNE8AA8?FdhXMkcdU!k$iLJ7e#$BC)m+vCQx3TwtOaOO)+SP(nz&}Ph&V3% zHs9y#9fx)cVSXQXic|1dC3rQ=sr7Q>qge|?;;;m7@Qvo7UWv^i{J`?;PsQOka-!{m z*}u6dc-CpyV%txNNtIl@8bOSpqS79ZTOlh6=mbRLk0b>eaS+ZW^~QCKmoHu{NL3TJ z)gwNn1{2%M*|TGZb{zFXgy@+H9OH3$Fw6Btf?H3xpzjRd<^Cz=zQ?%o~r5 zRuh!PM5_q+SE+g)x5Ju&%2km+PX-k7AbHbHyJ_f9-nQH5x7fs*K|7nx1-?+f&9H>y zWX&1Sag4`F%iPj#jD+_sw+<93Bsjq_=aWnQJ{j`ad;9%vxb+T>nKSz^U0<-&%&zg} zdifvHhhaEq;fFF)5t~u{?}Zb}Xmk4n+5$Akoh>)}sG)1w7NL{wGMyF$l8yN+Sgmow z6>VZMWB@LJ>j0W7a^Qfqhje7LdgjnQ7d1E3j8+ZRT`D!XMQaH7rM933s#NeI5WB8) z^m7`DHGl77*Z@U^?GxjVXR#fJtPh*$L`W*}9oboVdCo_~SLM5r7={sKFOO(s&EQz( zq*FZ|P>%W~Q9;73k{I9T*RomIlKu`Y_MQwQ9Ilnx}Dr>=0#^?e)foG%07i&FYXyJ#iKEi*-)z zR8G5WTD&Q_&aYmBmN58bzPxg^;yAwJ5cl*0)RjPIY;ZcT1Vdj{N(B#;TQ6ur()=1w z8o>39Q0cVIoXknvkFlu19x9p$pevS)>4?|8#@in|If@o9mgda|mG~)1162@+>a*E9C)pAA=p;tq&5u4%|M=#Svz{n4R?( z(O1v=Mf%awFhBa>_$B^|s_%ReOTn(MvEY$)R&CI+5A33T)SQ)Q z>_oGARU}z_-Zl6yx>p^-Iq^}%NZamA0VSgl zMP|Sc)jsi%ebG$)W%ueqcC9i?m_&hjQTk4Hb8qFKyyL)MKHFVDAHx1=CyVQy67>P{ zKD#J)7Dv4^!Ss4u;hbQ0n$x}``_+1$o_|IEadVdFp>7U)=83MvPb`uS2DfQ9YTp+4 zIC$$RGLD81;krP+I}%>g$K=O$jHW9jcv0n$lTSA_G8u2`0e)yI6&l~?buM7+O7`Qq zth8sb(LC*5nkRcwjYZm{Bqs5k?j!z5&{lXu<5 zGqVZLwekOa6VdlMv8m3u!brWqGkhR`s)&^dg?OFm-;yh!NfBSK+OlMAaFvmN^WpCa*k&G?m`{nzW_foGO3vj*~Q<58k0ANML6DP`W7s zlk$p=6EME&>d}k?!|Ag}_9@_s(x@tW;1=XA67i0c+|M_^lrz=%v_et4-BV6AF&qpv z58)#oR!(Qc`ygcG$3!zIOI=(2dgIzND_i1jmdJ(oFthx&py}S8ZhwBuAlJtF>YpO4 z*gFayikcMy4WD?e7c!LQ%MzzcVk?{lbzI69t+prDwKd(cqv(5aUar=J*DSid;Rg-@PHt$7GhCs(_Z6{EJpMY(F_0i?!*!dn!uDKfu?jr zD*wAjo3rW}J(CI>e#~C9yfUAsZqPQDZg(`n|5MeCc&2_Hyl&Ehej#>PY+4Fc1GHWI z6xI>wsm=tE3#ljbWp>`X-2YSgFn)&J<6Y`XDe5cXq={Hv3fT=g5qMWezgJg^lgGpy z)vZ=o6RZP1Tmf>lzwv#ONM+lDY3G$xU{6koFyGP!{&^j$9<1B5`a`(6(||uu^PY9H z%D*(%#|Ot+g3N0rZ1=z_4e?e!(J7=C0S0+KwyY z*8{Fc9)iwK1x9eHfDA#q$^D4tvF_EkwX)Y_2ru(Y1#!1zD6Oj%#82726Ifr-g%ux z%4>&^%5Nm+S9GT)wl??XAqlG$_i}6CMBSfrNR_Nj1@^7+D91ncr-1Wtq!d^ z_IlN#iM$9;H7nKR-TeVy$Wo$pY2&7H3Kw~Ydz#UQTP5)1!5LkXI-}!?~^v zwI`rP?>u?`qED3V0pl0#kRvhC8;4{%S=oTR3CUwdUkM~a8>oWN7iEdHwQAG$0VDBw zIj(c<<$9UQrlLiuzn@D?%68An!o_;Gt9Rbf|IB^)J#}g3i_2v0kM5cW55^ey5*YQ5 z?L(I`GiS9+?CZ!n*4pvv$)kOJktQFHEhgQ=7%{RR=0>F+RGmk4RCZQAX|=qZo4fe4 zY4*}`ZR2Y%GFP##(tIf%>n+@I*hwhh4tf|ScZRNca%$l- zI5_@pWW4BB=LiwSiSwR(NlXn)P}zLhwjOh-?dUR)+Ywx@D&=LP+RK9BWf}(y`~^5I zhDXa!-{9Wb^3N~X|E2Nb_+dF-l9JK>qs*v+$MTz$7gCay4m_Bm3qa3OHQ-PzCxwGl z&Rn)z>p5z+{pCB~JNcqO$p#~_^qeUnlI6(0b$4au)MCJ~%jjX1tq97iR={SJ3T&B!5o zo8loVfAgPi+2j4maRa=>Vb6?r_1~VhXJveZAGm}rpy##z7X0~w$bXB4oaTRS{n@us zue_r>0bgVP09Z zmimp1z5P4Uw)n*rgsilQVSR>{2MP{v+Xix1vo>o`*%wW0Q5MGh2fmo^#Z?KqhJvr{ zEkw78Y*JLbMPLlqyk{TNY=LI8@DCW@w4`n4abDVS<@q^0)~m-x(VXGE8OzAkmR&6qYuA}+%mumQ{tkB2-oa>FXzJj79h?&@Se*j z3swC2X1H7Pv(iI6W|H+bAxp$~pY*8Eo_Tr>vwOQ_qS*jdR~{ntc8{`p5O}Go3fzdZ%-(%RaciWMt(ieo{XlW94eXiNV}7m$pb8 z$R%e9CX3YOi_w03bAF#YAy9td*0K55lLs>23Wu^`QQ0{jz5W(~)~}n3j|*f?DkkZp zG2rH(Mpj}0_Okh zj}_SviEBF2FhG2Y+tcLO@OmvMhzKex%-(!Pw5E^n{x9FRUi^|O{+MYdkfnW6Kq!~L z?q=&!WqgU)m(5M;f0=LZ*vlyynOmuruaoSLR^DKlK;dRjq$<ja;^bOYJl`a~Wcn_wTxEe*k)FXVLbFTp?S zNPU`-yeCDabaTD9i~O7x{?@>f9Ad^|ATHKCN-iDdn`GKapM9t_AF@4fCFn z7j0Z+?l`3oP|S_m;N-K}ub!V|VeE%~S1vu);8ROf^~m<+E)6Wy` z$LdPBI`cft#LBR__mYirffT@JD7&o+4W@^69^3!E|IzN|^8n((yek=Oi?Q%OyDqsF ztHLcEMQKI*>u$D$HENgD$3|(1SmtNcoYWB5CmGL#V zvDKia+%Qd-7EIdZ}=NfB&;@#J~0n>9+72 z3WLh&=4Lvt9jJkxf@YE!8G7OZtEX{t-RJ0`Gnt{dD*AvBclcR%lh61+&j`jdm)(-W@S`Or2BxsXA3}L08q%6bEt1?-efaK6XBkn&xE%YoC-{`*bA@(%;1p-Feew z?UyN(5=YpY!Zh?`i-A>w(*@;oNh#Wzj^NKDqpX_#E`2eo8qph0gFB=1js(tBsfvj< z2OmeS<=~wkuZ4G*2jLB@FNLCn1f|Bm4(iN`PiE{1SB*ytY8~xv_Q(65+xXKj=0oym zcGZ%U3{VvHBiPJHJSb<6+s8By<$ZvFrejtId0DD!Lg=sC0yl%lH>tb=qg7i^Zm~zC zt*qO=NhT5%R~`mKfG)$bY`l)ihGbJlhb@J@tFOe~qSrY$0q z;%8k$N4VcS>gdvc$ofL(f__SZMpN}*1`f88uaDu*nU4kxDURCGl5%~ zri#TzRF{f#h(g&y&J+09qSM{JruV0m{qH*)M?KsTMI=PEi?hBoNnJk{Fa`Y4-tlJd zunysk`x&Elt6W**_Nr9p%0WR5g$AqJQJ#NUU(+o{3e4P7C}2ANlqu22&|lt#!u^S7 zc}qC13`(6m*97y$SN{EaTcz(6mwUV2tn8i9)b3{e-1e8_JL0#eA3;NhHsBU2`tM!d z(w;4_CP`KrHul~p$2!R#Hl(p7ciK6est#PtyOK2YnG`r$>z)1T+%q#hDbap}YhOtr zw-s*X*e~Z7_I6s1j+~V9Z&F${^{%)1og?VE^XF;%d$5hvQO!hSdvV|Okt-biS(B9| zC%U_~gbLmA>sF$BA8)0@AqQx)suouVrqyOfp8{(RS;nRPQ}z(}xrDw>3CBG4>*g;) zA5my9@}%TTGq{(lY(@l)fJflYrBPm_rVeT`7k{o(J|z{9j!jZf($#rlVJ$n7vT#`cpqK8~GW_l5b_B?$ZlLw8i~`bq#2tk2N29xCZZ&d~t@Q z4;2bTFs-&>ULYq>3O6C4t$9SYVc^?~HbL+=TaK8=@77>iR;G3sZ)-1pdGYatK;xbB zyLBcVkjR}6M!LA8Mv*MYmYW|6ca5S|+4#MDxeM#623Nk<(Qo}QQoo-6 z{l?nl+X6NZDXBco8Efk~Ft@g0iQJ)2p9YL%q~~mU z%+MGJckSvWN~7{_DMA7Ek5^BBtafJ%h*GqGh+AW9A`?kMS z#(5?--QqoYfqe9;7}LN=_+gwU2ZG6Hc1x(Ag@&}bdFxxD!EAh>%VAsMCHigf17X2 zify~CA_h}h=0+iv!F1LwU2AB1?2Y^Z3{^6k9wxNs?~ZTlK`PuDIb<$kP2-f=|*! zM(RF9Beyc5@26#(wte$Q&gDXrZV&RvaXFrLjfwUwl^8voAms2fb-^A5?8Qr&;aBVX z+N!deJ~zx|Wz;xlo#=iNlhEJcz97#f6tI6+f-#w5M+(EVem#^mSr=T%o~^PAX`_?( z!~IcqzCTO$OLaG0eDmWxtIOkqd2RN>p_2^MB$@q%zhf@Bw?Ln6qxvzf35uVmzogV_ z6asXEoclzm9O-FX@8&wmiWO|9?j%9Ho%V?Bob1Q}km2Z#ay~9&@@d5p>W6?9O-Jrq zGnZl?qJ$uH2OrEUU|qfD^n5g(gWka43fVcGF$2&^c$xP6=f@m&tDISz{YnuH^ILsd zMOkljR@ck>#U72x?81(4XE%*136G1ab6guAx|YJ~>1M2$xAFmY5fhM<=rf8fPfD8{ z%3!_Z0>5$y8|r3~LYC;q!1U!1^N1$vSLgQ`>jHHd>TtW5&HU0%0SJzKPDKTN5oQ}> z8f5IxY8oQJS@aa2i%hF!?f+PL%jeX}6r8jTmuheQ!sD^|zw@&~JK{~fP61XX&Z*H< z%4yH9TJre}fpA`NV}b4k?DoQ5Yp%Y#1hO_CiX8CGY<;7ky}dlU_vg~OZlWjN>(B*Y z&woXSRxe1d8HEnfr&^|KBA^NX=fVcXv51?=v-&+f=RdAHcZfwQU^hzXM6Bs)!w^#| z!(QtnM}AfJ`cuS!f}qv1KYyrF7iueBxaowqL8nSl+78eUST3HNB?-pH!XBy2uamQ0 zn-tnDaq(5>*X7{)QlG%35ptCNg zP=?!9)=Ud;HrcGMWjxut^@^6|3Q0dey}*DW^A5{2b^;U7fj3Y+z_5zofQrYg6X-{; zw{woyJABUFwh6L2=A3FXpLr?zKy1HBd?Vbhup=KZT6HMgrTJM}fA9#j#B&@?6s7Hj z9!;iZj|JqHh<1@0ApQsST={aw~`U&*)fCp-|Frt3f@R9~$1BOl zXKwkUlu$O_lP!Q=mnwO2f1G!Jbv6K~OHM0rd@`alIdre4>Vl4* zH8piE3sl*w7nRCyq~<3T-d5hA_8*Oo=o&m{VUeic99t_u6>xd0erkr*Ml6v-$zb!k$z4d_>3%P)5uGF7pi5=xc@T94RG|@QuS=5Qo@(2~sM2a(C ztgiU*uJh7H$vAWJ;lLWHyq|8b+lt;gK5_d=8F;5aKyHOvJsu*%({}cISmb96wd@$} zp3a<@iaEbvje^6wKUn~Y-FXbH645FIqnI1)-XYJ9^y+_<5 zgw%s5YF@c}-?=7zD^u0-bRH}-SkPuK=Pj&oYt*_){a!9U?LORN-jhMtR(`mF2ld>* zh_+vK^&nQ^!clt>@AXqLoY~24))%^T=PE?_in7|YO6q$GCL^S+d$T5e-nRVPf8*U3 zl%0SDr5m$iYQ0}>Q=bfsXO{gLeTuA%5~ha;C-}JY$$*$Q^mSN;_(@%j?q2L4Wz60Z zSicCcMC;y1>rWCchkQEBXv2c8dG)pNu?0xAOHKT>pwDcQX@1}eD6f4L=4o(f*{N^2hceLi?nOvV`u%JDldi`FJuVLD;#S2Jsqd`=jFh=R;V*FYt4#;9O-hU+@M zm&h}n4l*wq;QbPu{_aDph@IWfBc2@Hj{laorR8CS9IShWHWSp0bUpjh&z)P}`UZP* z%XjrM{Zmvtvt`QAl9Rh?G47hc%i`)ROYSkDig@$%Hf?vn$l>$)x#6|cZGX>gkH>L= zzYgZTIsEe|dX2$W?Ti&Z!x$8{EsO1LS&&#BZ9-LMAj^Z3pBh#0t;;>gvHdgb=H7}y z(N?wAgB(L1iVSHKzd&WTY+J}JN@grY1qH40@EcoBXK&*HZ=T7+i}Rb09?r2Y^NT*i zs$=Nak&+^}u&VP)eDi+Bks#+|wC3ZI8{dCP0pSw$-w#<}DLmdz4J~n#ssa~F?a$KF zYgw`7BD{$#?#+<3WI&%=0}NW5`Xe?vYvn2D)TdiV(J-RJ0}jS_hj3hSEhvTr7Y?hB zC`_QTCqEr~MNM1IZ@<6^+n(H|fxE}YQHI`_Rt9)AJDgdY;z+Ej{rGIa;7RWo0#Dd9 z%*a!1>wnk!pLRFdzR4I^x@BGVZX;6kSkk$LuUHJGXPgo1Vt1jdDLPw_bs0|1yva2# ztw?v-I2p4`3n37ILqi?7BYCr!k;8Yoj;>mq{fc{c`gvm+sec8Zo(i|qbI+|DyN#~~ zeGa$r7E%pHJ^s=8hD&R90dRd&_B;COK6jKwpVccV7DHD{h?7%qS%}8NJ#$;Jd)@gK zy%VC;yakx~7D! z;XW?l7Ydsq0;U=9u&3?Ge0Af8eAa247!qE@u@Z)_gl@740a#;0`2iIQ@bxr!5|7^aDC9?xyP}gLCsApu|P4gaiR`Tdiv(I>A^3W$G?41>)xX+JIz!gq^ zWHybPlx!HV?;8|gjGsJBKFKq}%9E{)l_;2K+gyk`_T|(8^c??EwM??(hfU_B4MZ*gg+no@KHps=ffF1R(E{^h-z)|f-)7Vm<9Ip@e z(+y0Osv!-c4i)i!1o*NSrG)KhaO^4Gy!iX|Psiwipsq3Sj*$Xr;?>5F)eM?UyFIK* z5eSP387T7})=ZL=8CUYb`3Q(=aJUAe&iA>$a;md`PD{tbV#Tf!IL~FZzf(?)Ke%%K zbPYun<*$s%VdNd|k=`jC^ZUgLT56qweM~%X z;@usgxlba9+Qsb@vFA6T2+p?jtNs*N{RwJThi?y7)PWZSLo!eDKUsu54QF2E?8?m% zkNyW56>!gex0lq)*-kOivA3pkw&*aIa=>)HJ+i&YP^UrlhPI-TPs4Z%j!vp2Kt z7oQofJQhj(PhP}_>>=G?gx|7ehXRM!d;kW`FTlYswtq;NR&Hu>no{plr9kx|lx=w~ zameKfj|0QhjTg%J7r^w3bM}BjgfU)U$)=h&4c@MgZq#nO#n*ar~q9RudSIu zPQAau&m7cQC$>mX#slom6j4gIHJV2WT#%oyE~B;m&C=$pq6bkw z)>u=`0N=5>xvNeCE7DI$m4wiOG}KP+X8eHy0Ur5*7x!Aoo1b|iJvZ>afaDl(rHz8hwA z&JOf;PP%-*!xH%|9++FZm9HP4F=bn>Tl)^R)qW%noXPzD#)fqDXil7C5=EOC3Qsk) z4DCi?N|BG*KFNQ~q^?^j$xgU+yIkb{ALrJ3UqP}5rol*Yv%#zDBe{un7=Eb@3b5I& zg^AOT#$~#2bMGIn!wgDF?G1ampBsxVUKjuCZrs!rCs(|jJAcV&^`^G#_&fj+$2g~& zs;9S&$^Y=k0VETaE|lEH|J7dmJRc>^d$Q}bGF&-T@y;i8K~>N3*%xz5aZ;JVo|-eEj*`XODk zmBU`#yTRsrwX=~cKh$+k{SqG5m}(sCjtNUBuW-t?RZ>E=H(eoidABtbG!Dr(3dcj) zdNwfyD#mV@L`le#5m-#{62&>{0PbXGV^iTKz|=$d*D@9#pdSk(HQw0D{iM7#E?&KP z*ihfqGCl2v;v)ywa=EQvb83&s(yFUQhIz_YX7R1^Xj7{M#^H3wBqaTMaA`jG4WmlD zwH43%Ue^ek<>~=m_-@BGsOvV+BbveFE^VA^G4}Q1V3!KTwdoV4kv$$=bYTrD>S)=m z?>n5)VQu|#twJVfrp#^0{;H$ctDd4Z{1fRJjVJY_8{rZJ@cf+3Y^PE2FCNiTLVs)2 zpbeTTRhGcaIstaPfmVU;qkH=YI%2ERq?x4GgEP=BgPPQarN|nPQ3l3{ont#9gal_| zOx`Cc(KF64h>vuCs}Axug3m;{f6YhYbr{xy3#UK$jM7I}8r0r?)Hx6Md_~jR6XjA7 zU}U5lX}o*iE_d(IRxY2lg{Y_b8js)=II}GS?rS^R1^pbJ;Bt7^N$kuqWM|qq&MHJa zceH#AFN6tri&hqbiY_~*Ty6L2w^FAABCiuR&7^=f(LAWpQZ@-#!+bBByLbQZCYPO+ z#mE(fiY}eEu33O2;+!BCaU^g;7gqcC>n)&LuZ7kK?Gf7G>2JGFHecq>o+Oh#660kL zUV>G2LHjuD&Bm?1a{(3P@*Han4nD{5$N)H*!@MMhax%U|?6W`RB`Ki4=?}C;kDdqT zh>w;H^Qkx2lP+VKE85@qPVUO=o>;u3IL=2z+WR#6KtKmN>sYA>-vms##G!7Jb38f! zUJ?FTsvLbyM=MoI)UUS>Bs8>rH{8Ek4BH>2AJM)zjUWHH{mXd&mke$jsr(MZouZoY zi=le+%aO04nw}OjB=w4Y|Ab~6BRpC`I}RT`5Y9|I46TagaU;B(h|J=D*)?eVvK6C3L>TH|4Zh0!|Q{yaHi>|GLSpk?+1mcYl?swJ5MmR0^OCF zDX{V?P{6xCt)H`{W06q^<&THxj5P0ClsxKy{Fat99`~6qv$}8+n*rqb8v2k%FU*b?LT*eyDRhq0ZHiHZdOdIoc0;0Uq2qrR~{o`8sArWBRfV zCN(k#+Lp?zWO+SJ3WRRxLJy}UAS{dxKjhM}#~fa_#h}q`2#9ci1cOnrfw~1~F40#U z(x)?OGgVAFl%3q$kUei-+|K)U>96giV@ybSbDcJK*iYV24}C~-m?KXaum`MZrPW9} z%R$nq&Ww1|xe)Lw)>?jk>bm+c6A)u_UsIqme|mR!+-C33Ayo|$(qr&RM_C|JZ=NmB zA`%DVoL0&_qx0h3lXBSvTlVBp#$SNVKP^2QkOu}^^V*`Q+#5_PcS+0*dapj#ey=5>NnrL^$2 zti9rF*5C+M7n^&}UEb_3hc*WqsKIcjHV7gQoS&?$LG@+#3AHsf7aXlNJ4iAUP$qXf z5}U8S!Jn}?(lOu&ZHFW?8EF`Mfr|ljZ*e-iE7ks4|JWsR_k-j~xrt!EL$=g)6btN* z9KwcMjK8U)%LRO)aiRML$BVS1Vv zC#OhO7UH2_h&0JJUN;)#wyQkaYR%;P`%TUdtGUjZG?dl@kB5mF ziyH}VEZIsHm;U{_lSN~cWSDFHzB56;GSR~W@52FFi2!LKZh60#>QRAzg@1n|XbjFk z+fcIc#1sfL=GvYf%8hdQBHWpVlQ)+!A~eYqGz`fnkAYkaVvph-W!V#4pKz<02u_{@ zUeZ8jvz58lBf(4OLaLOH!1KLf#RgDVuP8|%yVZ&Oaj*|GfuRsqRsf7_(A?Wr73SS+ zjK=g7h_0m#GbVqFmo;G`+VI92lffT_fqqshWzTkvdy9ps5{ff7pIz0ZsZROZ_+;4z zBN@I2r&JL*6k(-d|*Yj}ag>zCTt8dB6SaH)xxTfx-!#gx8 z^4VdF8hb-3P5BS}x&3oz;q+YL{)sT6QW^SzT!FH$$oFNOH}KmBIcnh=ZRKnjlgPfQ z*~6dA`L{(dih5}MsAziffrje|+6a2S?iA$G*X0e3sbC+43ewvoK~|Bk=P{S`kuvhe zc%6gu!;fpZMSI)23skONYXteWrJMd|e80QFXMhu^az4IcCUCrM1?FWp`?tv&b@{AZ zamfe3%u2WoCEEN0mBD4jHp5;G;SNv_5c{(sRi07mMTn4JQ6v5qlM##GCIrS&Qz-)6HY~;| z03NqQiB*TT7W%lpjNdGvC#f7vf90YdqFF1bipx2sZ{B)^7%_2|k8c`TcV0h0{da~Q z4_4gh3Lz)onvA3?g6y&0IAfo!sXk7wLY54V{mW2--hvLS?3>lEZ>l=dErvCI)UEHV zq!@<~ZVElx+@%@!@)b;@Y#DkSMfA$M+}#qI>-)u_(w{B;3b95h-BzHu z8X7uIv5{bg(GKcWD*4)Y#o5{wMJ~R`#K8BwC%;~bX&lk&eHhGxQufr(=I=L`1(%+IzUK3K-*&;(nHQ;hs9&*E4e0eNjdPxreBg9; zU-}JpB?=aJv6NWm#HQKN?F99;m?D$tX?Jtrv+es`_9%!Pgb)Zt(QZgt(B>)vl_TVj zl1y^%k_c@Bywg>)R~e81YxT^Cxb^L8!Jmje`=cKoJ_glK>QGIUGwt!ZI<#kfKReKW zCu>v(5=lxh5a*W+x$&NN)+?Ua6em{b3=Vzqi}suw>sI%Yz{x0pA^sR+U~_MGYPs@r zYUeamHqa5&c_qR};>LX@rN6-33}!X3r`e zI`RDuTo1p+%({5peDQc7(apVtFApq|4Q3J3QFbL~rXSEnRn?%0)}?)~1l%(Y?_WLX z(Nt?Dkn&ptcV&?ja54nu-lXGho_d>Poj)oET?r}hgz&iqD`C~`YN_sX zY&+Vb={Y%wy-P7zrc*X@%8zmt-g#|mi|Cku&wqeP9GLOUeS`Fv?iSxd( zIUlw^uIL$|KO(L+)BF_bd>pz>8!+;QucqVKbm5&r)6_aU`OiHZT*Tqyut%wWrR${R zCYMGJ9|h?y<+(+KoV9e+XOEMKJTSuD_HvpYnXGMBB$h)ubd08Yb@r;xfd>chJo12P zLS6UBCXA*r^z~`t_CT2BVZ9*vB5d)~O2LCo7Xch!?BhmgPD7Alj~k3UFg42MmJ+40 z%ZFSk7a*M&>#ubXD?I+v+wjI7L-S6|u3rsEeQo437CJT4Rw9v}pKz6Qq&bC>v2tdk9#@>hjL}#oyc8VdA?90)3!nsP8Ojn%DzOEM)5e z_g9jRZNRT@9OpXdp>$=t3@0wsOvt(pq=PDqhufao+AR7pUG34OLz({YEB|dt2zWzPhYq>W@&}WnH1#Tm@s1`+ z)Fwy$9PDI^WG85aNFN8|sb_QIVj+?~LS4Yqf64zXKR%z#K^@XW!yboMm4RYOWlSE4 zn2)EgBbnyIx-{m2D-;xmznJ3`>gwtjloAHMsNeP%4yh8)#autwMZ(lmK0Jg|AJw2B+* zEv9kKsgKX%#l=?F68goy-@(mh?RM8W0rNSwfiAlD$!JU?QyKb+_1Wvawcfo@4{ZI% zDnJ-)oVbcg&&jsNJXLzi0Aw=FVvjrJJe^cm!6vF6SaG@^w6+RX|NSdA?B$@?Al4sR zx{v$}h$4X^ktkR{v@1P)Cg3>`00$mOa(}Eef}v~3^~Yn)b8Wu39zq_z%y0i0GJgW% zK{9|PGFWn2KE%A)ihbfv&Yw~YY}Y=Bmu!@UqDEb#Gu`6Z#Sp#udYiL!QaMd?TFlhS zShZY`gPr3aZoI|J8e|(IXP8UC$w)vA5H78CW&w$H=64u-G<8dTuxEY@c4@tJy#Vyo z4WV?wv@7}L6?boBr!45`xD(}CVZ)IOe}nstTxXCorV}d25*q8`J@duH z-4N{*H5s8wZ4xL_hMUQ<`S?jyaw=Cvp!YX!f7Bz-6B^i!m%o`w0%nY;p1(gK{p11^ z0(#n7^F-$nk}W8Qtx;EW07W2$M$(lyr*r3%pqj_D<}fB)d%oc-E*ujsx|O)TB$ek^ zC3X!^P&OZ3pC7}uqK4fG_#v#FhW=7}nRX)7jGf2ORZV9?1n2{y%0dLeXb4#BZ;9oJ zJ#LGr*Pf>DLUx8yY%o0@75+Nx^PiEO<&Tb0T8PU_rE7UW=m7!C+Tu^9x{Q`XFx_PN z_5Az$3tFCTeH!0&X>4qrMjY3Gk zo3H|qHyw+If{_qC%w(Ce&m96=z+voa%Y{B0Ax{p@rE3spqI^C>biU`Xuj@&4_P54ShtCew zi<8WER)SpKAB1lsEt@Y7u^)CWyOC{7b`6C=7w{Fha`xrbc00}2@fMxyy-3`XmBL;^ zsy&0FdDUk#2~ zCqe`fU;nOhwq5V_*!HkfV~Ilg)%iXt_@5U7Bph$|`7r?t2?C+Dt#2G%N4tk2UP;;j z*n=pw;g@ryh_Og$Yifs6I8-|h5#Y8M!2ib(*%$ia---H=3#k$Je^aESczo7TyB!ze0}xS`o#Xn9wCcZkXBDu zbpSu%NL0%0Qh7UoijHa?RFq*8RRg+tnCVA91DmvWKL+GlAPZ2+rMw4&C=#*`BD#&1 zGO^R+U>987gr46_C1Uk-DIY$4Ehu*<)29@a%=i!Q`j@bPC4&8~;of>ii!FE0gqq;b z9S@JW7DRm>@avniGdBmy4@NS0+Bxa1Z+(`Rxdg=MjL5q8HM!-uN%+MiUvUbwk46MF z6gORc(}!fIYdWIBH&yaG|LgAlEeT@wrRzpY)aYGUz$8;s|5`TYt+K#JC?RuV*)xH? z61U0jAasXnCP(F9bG3@Lb6x>&5 zcu{36tBH&m9s=$6`8a4_LHl?Z?lM9)p)Jnf)o|B2_nqK{ZBn=r_^^!)&w(T};?~Xr z7u#6;)tQa2jwDS>;K3T%I~wKzYL%DKL?U1R{Ps1azk=xBtCYvlivi9$$zTDIJ8CJP zyRJV3ON(i86MT9;oLM9~sK%AxH}#Qc-e5A3kq!|Egzg6;m|K{qccDzA3s}Pka@K;? zv(UYdHXr^G0a?^;_k99>KI05nmlsnja_*4T&!dKx^5sA%As1k{Yz|@p#4H$V3`Vto z^Vj|DXpG5Ypza{%d#)j9zcr`VNt_UK@Pa*YUUKo zOspPc?rV^B8TS9*(~=HDb@9i&uVgkXzv$WRabG*7Y6heP*&wkYuaT|76jUH0< zAW1lHD*8fyf_UH?IWqyUjxzzhfRny8=J(-6ekelX^ROzy=1>p? zZ^hh(mv080Z8^vp*kB`$^ieL~bj9asEP6U`KU~Z)6qvhpOu|{y5dckp^pn!ye*odO z?1?l4CpFpy5NGq!7Q1|(`;{PZNE)0uMY-WM3CP%BA5sPI8G#9EMB~Jv8tWt4T}}RY z14OEtFwK+Ga@QYZ?!8^!xNFx`fIQa+$?th6UxxzFX85#OQrbj&+~ogOT?{_UFwv@_C77>=v}{p^rC(1L<-%Y zqw7G=&oR6q#84jW;i2nBIB|aW(2~Fy2owHy+0{@H?s;%AnXK1k^;WNtEu=iqf%l{; zQ7xGf`B3Q*MH>m`$TZw*_d(GqwZ|Thg4m!`G6V-p2Mycj#*;GABU!+(*Uuxm?z(Yy zV{}P!(7V5o>TNIL3%xbWgQs$c6#U36%f%>9Ke%?VI0N z|J7q{6fvb!{Dr2Ss&l-gEGX?#on$qyTtmcAG;^i_*`@`@3IA0?=XQo&h_gcOLD0}< z+OTA28cT^oXRjw!zNP>$Wp##dbaorOHpPbG0gPm$kT`I2baf zhlkWA)w#Jmt!#R_|D5gSQ^%sgPG+z=Bbt-Yy;}w~u?OHk$tv8A&&Ri)^!LW}l&r9^ z3jL6VgW1rI2W+b}F8>bK)|cFL00g!}g`R?YVDn`jvS-Vh`1t>O-|tL89e9nox|MZ@ zpmdrer4V^$G1pLNMn?VjMN2~vJ12VCv2zl-=m&Ep&tIAsKyEzAmMXJgNRWk z@ZN=GGNIysRGI?YPr8%$deGMH)AwqCF|>n0&aQSB{^heLX6BVSa8GX&@*BLvOhT$$ zF^^+1iXtH6C~Ufhp3;zl{@a;KRQBUWW+Awz2s4VyBGkO@;Li-aiEPM^AyS)G;hG*L ztfVmEuy3t578=t=x(^+>IZ4RJ4UJX@;-3EZ+dzfhJO2(4j43Qx}q z_X&?~Bj=K1_c1}^OZE;}d2rD`jO11Z9~l>!z%$|_;bU;{H8|sxeZ6F7I5u4*P{ep{ zDi?puFq4GFHl^n&$j08c*(i;oq2~ozPeRKxF%YNqIyULwABQtcq)!#?rCaP2?v3dK z(dE{Aq7(^v{3ER8f_xK9tMV|04$yi#$sj=Chj8)UwacsLkcVa5g%yQyxF-=NbAiI4 z#h)_TDW7481rtz*iL;k8lgxrJ+IgJJCq}ke*5bnSlmN(IVH;8rN;hG=3 z75wgFfiOi_OpRd}3o;Xcy9;@26gqsRsnhCNZKjY!v-!>@^~&pM&r#bhuQ|tLXb=TI zRzPRI^SGqSDSqSKNN5f4;JQ6sqqVX9YUI6~e$c8cJazySO%ao360- z)UQYdymKcDuPl4y2KNxco&=ChNk?}j&r=Y}x$(ydkB^TvfDsycqO5x>uDoSI5a!st zM-8xa5D3p$3Ia0TC;vP(`zc@|8*&_japebRzZ)N4QjDEbsRNPiZnWp%EtnrN43;Ub z1P5UDYXw2DtUj~+i=w(*g&_e6VNy#I1y^vSEy%q2d7`m;Zbj1HZXC^ai(MWjpkQdue$xt9CdBO*no}WhQPemBo0g7_Q zQxI4&6C-jfM6|YNa3XG`J({x&hqXd;;h%4zgiMyn{xbk0jtaDY=-*PVLusG|_%Q*x z8ELo9oeM}qF4h3+){iw;-2CAx`h=$u>L2{`*GBCtAcdA8k+ks#(Ei+kI-H+?#Q zp`Ya8h3S4OvKx&?tnrik(C6K&k3Vibd$k0tsZl z-v|6lj0a~+O2-(HkZ;-Lsq7}#$7V2y*Bd#fP8E2#FlvHplt{w82cmDPJx=ZQ^`pEz zHFHNM0%6TC0qREB4rRC>3yB51c5>J=EcD-UK&ogB_Tm41llQQrzM_C8xpR^kAmZlK zTXj%E8tL&1C8gB1ykMeUIBzY1@DHu@sDoM}#-sNcvZZ)W>R^M*sKa-Wo0!aVr;3ix ztQGwc5uFO0v+o?DKHJjF+@b{l61KDU~)uTVk)rAhZp^_Lj?&jyz#3vX;O+xG%y1@#UpD zEL$X%9A4UCUw;8PtM~ z`A)Nk98{z{xCGR0fj18Yw|{6vkBvPejAtk+DI76!RYRpg)BMdtI>i|*QBKeri6sH7 zkW{LUHtA_gcUsU%lTUbVeoG2+L8jgeWI>vacmu;-(gisrfZ+NOh7TmRTW{wqbqn@y zgme#a^mDRv?7|w+f<9e0;^%DzYYC9?apXQx+0Tn4pFjzl`8*^=ej{5#-9IPf+h7#} zA447lozQs(He}3-rya}Y@o!6UP)tKW(=8Cgj;aA|a?#XQXHJ9{5@Y2_MJb{&+n_)4 z?`@t+HvQXCf>P;jMmDIc9A$ao{f*EMV*0BTsdlF*o6%&b# z&Hp3n%j2PLzxT737)xax`&M={VPvTuiXtk?mVFtr?`uV}WhYyfdLmLpmh2%KBV&Y+ zwP6Mo$x>3Oe&_A^d|$tR`X_VGd+zsrpL3n-T<4rVx~%E7D>o1!*L@(uX-EAs`Powu z)4n6dLP|WfrDG@w=YcE+XUK92qSkb%0&wST!%b5vX8E`@gjn~3+|$7{3$gou(iysr zfK)7J_bxkKN_M6!I=&wGackyPp>y;A0=xBSb(dcCG<{I^!fs9@y%<1baYRGnzfT0v zDerI6fPnkQ4ZGj`06B>OtsyWW;$`xo%*X9?u*_X}O^pkvd?^GN-SkUR@vxR2P_f(O z0L}xlOQDsTU~mQd=-cxWT#@0N9EyHEut^zMGQ^LfNYo9HhqpF9!&)%}VSDw2t1xxBG8Agll3CHfY~Dh~ zB8`(arL05+aBBmy}IRYy*cTdl=4`Y3r_b&t!8`Phn`(V)9KB!v#l#~b`z#^F^t~u zO5RU)kU_4a>Mj%w|U}#u2p|L97XRZBcyQuKm^%Hed!Gs5MK1=W|5cvAgTT zZZWpB8{nzQ77N$jqrE_M4W^qcOwTDp?^U z@7_TFL9{aHiXdW##53*Nx|;aL8cx~Anx|RH3JI9rQz@rs_{MjwtN8bx=#nzmx|Ov_ zw=CYc;>;|yYsP2?*xdXk zS4!>Mlr5k}m3QJ%IP%A7aSTpEj=CAoX{j6aIoiPEVa_gzNBFJr217 ziMk_v04fm6(~D+Fmeo65fwYqVj9n_YAO5s)SJ^?>%?d|m(s{q#>}uKH;`(um9ibc&MN|+Ux?)^3p!0{1>9|t^k7x zDz`E(!{Ecau4?ejf8HGJC)#Qn*Xe2r1)NFhy}W#fq@e1=IRtdZaNHvtIREpnsm>vH z3!BY?fY)#-Uady1s>U_guvy334)zF7o&6u2OUBw~U{PO9X`m{uPHhZ*pjmnw{r0taE$RXSs!-wha`gl6_2G>(;o#m_HqwH zKsm+$tV-cUlu<#P?O8~LuS3N?{-@JzPR@)`r%uR2by%Kt6yP=9t>dwb6)q$?NQ0~m=DN=9nq67h3cXhUYJW`7O(s3(Z>p;+plNt1ngotP2=2hl=T-u+ z(LNe(R|N*|zz4+dIiH9N7v9M&iQ^c*x+?qZ*19#bWDHz?qVAoU{CZX1$HG3cp|QEdOP^Qc4t#-5BJDk)H$`;YQZgN5u1^%G=# zn^51!rEyRC+(5n8o^BHS_Y{^z-LUJC%05yy$Ag@go;~rw zJf8_iDgXD^!{G##$M&!Rl&W>L0+%#*uB zt#|}@d2f6%fbWa9kTiUwQ>Xk|vF43$+w$WRo$zS#7X+c5c`XnJz#`&c+`u}?sfeyR zSq)+>@5(I0YPq*`*A+^y=mGLu*?@XRTbbR1qAg`s#@~x17-K7bhsysF3KAvRUt$kL0eWmAz2UjH$5BChI7lsNWC&h}!!=l!Yc-c_D z8nV-j7*Z}c16dH@GqoBaCg9{Fg>dMjeAKwGwDbVy>#C5@NaqwU%A z0fdj4eiGZbB3}kg8(<%DRPL*!mS>#$_#cH^PFa21yzp^0;rL|J%U8zti~;ver>&6X z8=VIskWZMtZ zJ2Za#E?jWZchwRJ6r50BnDLFir{TNx$PZUi8C((|zAs+oK=J|12oZi#RU_by{G$MA zWCcNH=e%>`c1dKDF^^S4$He^gXKcsMtmXFo$l5amzi{&~10BBg(|YVT!?vSVNC>sz z{&v>?ywW4>1M={c_nc9}c}n!}pD>6pFTIe=A?~EG1;m4J)nCyi1ah7wAJ(9l7yr`( z!p}_jz{W>@=&sge3T7O>8%J7-zDHsZIgNZ&`A^(Qhx1@$md*w*CJ4&U%@!%)58V~Y7+z9b(f;kAyW(8q? zlp(xm^Mbuw5O{>$x%yVQ0>g;>zC1w@j}#6P)OA(4_<|iEKUyK5F7JnCck7U*xGsw{ z?g7CXNU0T&qk7M|Q_HtK?)cP|?PQj8ir6l7_f{U~xrp?MB!15876-bq=bjAyH3N{V+k&Vem>{D^KFDGWLoU_VVO%gRCrzDlqOkbT}g@^17U$5JCDa9Bud=J@`Ji_rvWBQgKLzOSb9 z3vzlT2=864KBm@X=|+sks(7!f;kskzi0?afvQY|j}`L57%xt&QZEp z^}i1b+O1s@qL81?ccXg`1HARinG8YRfQ(0|pBs|2-rGT^9=*$Fmh(k~r)j_9HFvK1 zM=`jbE|bc8cIg}FY>Yd`irS$~mbc1&codm@UBNsd{a@yn7UR#A>dc8g_Bt3r{TDuW8`c}=gP5}+X#^d7wK zMMfV5X72#NV$D9~0>&3(0>__VIOL!8yxe{#-uW_*4xn`<8w^ZnZFQsY?o`}XhoU*0jCxPLNz zkBaY}GYLm^EpJQVYx+}V6Z@sik6iv^aP}-(W@xQxtd2B(37rjiF?c|NU%y3+DnFS+ z5Q0sfy}e%M%HO1%#^yxHkwSsa(%{g26%1l2GL}vRm%mo{B2>8tp{&upx z^tA#)u)Q$dALP+?-gS{7=4G=>qOSbS&0f+>s7`YcyD{{oC0=kiQG|8%EnC@SD8T<9 z{Lb&`!yo zNbpJi9vXDawOH^0Qzk!z!!wrW60xW5InyB<#PTikVIw0%4I7{7YZA27&qh8cCZ|kg z;q;Q$d(BYh=+m7VV69#R6ec{&IIC0B(kJtryh;4T6K4=|w3vU~uvqBx#pjS$aFmX6P-@2U(UC;-JBnDo+R<730+x;?bFHD?4svWABeV3C+*Q~aTro##3GM- zne?w(PL^9xpfa*rQm06xZ|J{EE7ps@5zm{(|E3ALPvout8D)n?#yK2Mz5d%0OViDl z&i8oBSlW&Wsdz&pkA1Fff)hoy25_3Nx~VUu@rzyzyH-AAEm^%`VhckV>T1$P_WZqc zH)(ESl!Suy?T)T8+E;z>YgyS$tlEqWq{@riWX>Q|r^(j#WS8nwMSPRy@4Ti%bRT;4 zxVVSeH=jF9j_t17i0S;dSl@O}v7MLLF}_9_zr9PExEe+)WS1>yaU-Yh!TDZvg28no zf}+E!)Ry^>V5PNBo;{TnXLve8$I;66oD#A8!$Nh_z>J~yA~$D0gH=iK>0j-`VO#Bn zy-n8=b)BhGxO&w7c!U(fqoHym=zV)_U#}W@gZPOVA0d|z2xgI2=9iZCyv4lpp@L=m#WyU(kTydtSnr%GcTKim&mOYP6IM=Of(BZRWWVIjtMVUu=|85P z^3GEQB)w5YPf0J&;MGq&lhG`}9dDsWwbrPgc;`(jN8p+l$~oS7ZRUw!$+zRZiv8^>%@C~6ywj^u4tR}%h<=lDK z)r1}~%zJV)8#`j6Y#Q7IZ*zv#G`@@u(?Xt{&A>lz~fLzl?r_cn&gMh>QT(y(|+??apqwCzAW&(f2;AO-x zdRs21(#fg#JyoZ4dzAlpN8rBX9UJSO#IO&Pz03p7nW%9`1&++SpUdPGy~eTiVXFY)`<_>Vc$ zrR~m7N1${IjpPuus@%j02lUFV}u@ofA0y{yl&-Vj4V|0 z8MOH37$%v44GLHEyw6bc!$y2xbiDlTQ@jxK#9Hf(9n{PXOYtsA6br1b{fvBRtXke@ z%o^P~F!M3C(^dA!KPOuLCFi$a?Rww^-47Co?99$?gp3=9DEm1sqfmq*u%v>ahWse2 zB?094wa!CZ@OHjXc%5ISa0B}4ee55ORGhK#a^Ygs*w{G#dWh^U~ z_D^@QCr8M&)^Q`I0F;k20Q{%rEBq1qTwI;8v+!G3&0JFyCZHDp1HVs&%2w7hc4sho z=TY(jhR5~D-_1i!Zlrv9uhq-&`w~3$gxZxWMxowqg8@*qaaZag({0Ws5sb0YX|>zJ zL-fP!&_wmpejYDffZb2A_f@5nI!BW1HT(CFi*j*#he8cgRK95FvZy0@I1Rnd(3+0d zh~Jv~HUhNr@FAd)s*U1zw#ZDdwpZ|F$NB46>;tA!O{);;bEk@|5zXD+Jl#W~E_b&1 z)t}zsf3W-DEYzyE%;#KhH$!1I(VE!#z{?^f(ZsWuT2g=yyWjB-5( zHmFZhDVl`eORIOpYIvYO%rJZ0g$1CEqG9kIzH_*#Wp89qdtb>YbFwDbRTqlFC1$y3 z2<|q7V6YIiGv@apo%)tnz3PL0FFI};zXjJQunUS$s0o@KO}BEV3IvET(xAnJzXcyzm8 zEd#^N0CP#y1wo$*)Ehq6H|p>nkFhMC>$bW8*1CNA-6Fs6r|OG62?n{OyKypc_ODu;eFnKN_sHE*!e`pR~c{b{!7o+0WgBi5Mr8UVMz-uepu(L<}qXw<^M z#q<;{`R$ZbG&zry0Cg$U9KqUOR5#dSPYR%WVxPl_B$zJ47ik}qB{Hcs7!#v9b}`x@ z_o&7uDKxD&e!S^9j8e>t_B*O!&qtRK7ijQVq5t`--QmKe`%mm&kbl?k`{s22^rb{y zlDLmtH(zL_=Z_H_2R!&qxH=fBXZ7GO=H{oZpT_uWzKl}UX z%(Yxk4(z%r%ZGSKOe1L$fdpOuKAjtNp-hUOSX@1ecH{}QMoCbfJjwS!+Qa&^OiNVsMPuK|hgX(?M~AT1TW%WZ ztTw^+D5c1fW=_yI0si$e`l;ErEyH~y{%XCGQfq? zvzOy|-gyfyLVOS~QFLkw>8d%~U>f;l@`$kIOev`M6en!Sp)SX0ABRx9Jf@^BQi5yX zx#N1!fpOJhF~`OGWA80knpfyCaqD{Dms5DOkn-x|9{ri#iFUDG3#)}{EiA>EBA|IyEs-7J=%NXc6L~g)oz3e1Ey_Si|93AesZ@o3 zTsR>g9{K%~w1~wFEn@6BUk?OI8jpCi)Mu%&AJdaIRW_gh%PY~9teC_XV*#UkhH_`} z)l`GXjBwx77|@XdKOE!UYui9;h!DGZP7ehiIsELw!q1a=baL6e(WWka@ng@o z{~g%W!dd8+8h%1L$qNhG!i)4E1#}0+BHTbOb0PS(E9 zlYH@}e;S*p+vc;{mPOh5Be~WWy(z&Aq}WGAmYn8=NS{~}&!Q#WUtS{-ygwdA-z-#HYGDuzv05SLJ!#X zsDK!ULfMG68#P@3c-Ch;tQmDnu~s2*(5|M8CDH}?1=!np31aqWJJdlQ%X849{$reM zBZxO9i$DXK)&K9)l>RzmlWGE4I~%9x)m9>V!U@>2K-h99_#=MzDO_M_BtOgf2@9KU zJz=st-bBDsy?DIJ?y)9Qp}U$yo&x}$k+LqNKWLwG6hnXKtJk4gI3L#kzoG}{eQ0i( zD$IbX@w8^e3a?aeWTapteZ7)Q7xrW*TGq2G8E=KTADdtJm<|DxJm&yzpH&Dr;1)FJ zKzd2_I_W``-)>VzNsKq2zK$J}PU|S{yY4)vHT#!G?hX%K%mk|`T5NaV5{Wh7*_@uN ztoz=G<)|uJED^+?5fdGjVi~=;1VNeVIT09-bUnn3s_+v%0-(?+T|!WAu`bWuSTxPWCd7!MX9+qZ{yvXz@SN7eQ_esVcpJ*852e(BHG;hvHUmB_%rCq%0D1^NmU>bl>OgD^=#0)bLvH6_w9C}RB}fs{Xnvgz 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 C@MpyU diff --git a/kos/resources/setup/macos-disk-image-background.png b/kos/resources/setup/macos-disk-image-background.png deleted file mode 100644 index d824c0e546f49f1ad5de613fbd2ae74e4a3cd396..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21770 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 h8, - ipc: TerminalIpc, -} - -impl Terminal { - fn new(window: Arc) -> Self { - Terminal { ipc: TerminalIpc::new(window.clone().into()), window } - } - - #[allow(dead_code)] - pub fn window(&self) -> &Arc { - &self.window - } - - pub fn ipc(&self) -> &TerminalIpc { - &self.ipc - } -} - -#[derive(Debug, Clone)] -pub struct Metrics { - #[allow(dead_code)] - window: Arc, - #[allow(dead_code)] - ipc: MetricsIpc, -} - -impl Metrics { - fn new(window: Arc) -> Self { - Metrics { ipc: MetricsIpc::new(window.clone().into()), window } - } - - #[allow(dead_code)] - pub fn window(&self) -> &Arc { - &self.window - } - - #[allow(dead_code)] - pub fn ipc(&self) -> &MetricsIpc { - &self.ipc - } -} - -/// Global application object created on application initialization. -static mut CORE: Option> = None; - -/// Application struct wrapping `workflow_nw::Application` as an inner. -#[derive(Clone)] -pub struct Core { - pub inner: Arc, - pub ipc: Arc>, - terminal: Arc>>>, - metrics: Arc>>>, - pub kaspad: Arc, - pub cpu_miner: Arc, - pub task_ctl: DuplexChannel, - pub terminal_ready_ctl: Channel<()>, - pub metrics_ready_ctl: Channel<()>, - pub shutdown_ctl: Channel<()>, - pub settings: Arc>, -} - -unsafe impl Send for Core {} -unsafe impl Sync for Core {} - -impl Core { - /// Get access to the global application object - #[allow(dead_code)] - pub fn global() -> Option> { - unsafe { CORE.clone() } - } - - /// Create a new application instance - pub async fn try_new() -> Result> { - log_info!("-> loading core settings"); - let settings = Arc::new(SettingsStore::::try_new("core")?); - settings.try_load().await?; - - log_info!("-> creating core application instance"); - let app = Arc::new(Self { - inner: Application::new()?, - ipc: Ipc::try_new_global_binding(Modules::Core)?, - terminal: Arc::new(Mutex::new(Option::None)), - metrics: Arc::new(Mutex::new(Option::None)), - kaspad: Arc::new(Kaspad::default()), - cpu_miner: Arc::new(CpuMiner::default()), - task_ctl: DuplexChannel::oneshot(), - terminal_ready_ctl: Channel::oneshot(), - metrics_ready_ctl: Channel::oneshot(), - shutdown_ctl: Channel::oneshot(), - settings, - }); - - unsafe { - CORE = Some(app.clone()); - }; - - Ok(app) - } - - pub fn terminal(&self) -> Arc { - self.terminal.lock().unwrap().as_ref().unwrap().clone() - } - - pub fn metrics(&self) -> Option> { - self.metrics.lock().unwrap().clone() - } - - /// Create application menu - fn create_menu(self: &Arc) -> Result<()> { - let modifier = if is_macos() { "command" } else { "ctrl" }; - - let this = self.clone(); - let clipboard_copy = MenuItemBuilder::new() - .label("Copy") - // .key("c") - // .modifiers(modifier) - .callback(move |_| -> std::result::Result<(), JsValue> { - let this = this.clone(); - spawn(async move { - this.terminal().ipc().clipboard_copy().await.unwrap_or_else(|e| log_error!("{}", e)); - }); - Ok(()) - }) - .build()?; - - let this = self.clone(); - let clipboard_paste = MenuItemBuilder::new() - .label("Paste") - // .key("v") - // .modifiers(modifier) - .callback(move |_| -> std::result::Result<(), JsValue> { - let this = this.clone(); - spawn(async move { - this.terminal().ipc().clipboard_paste().await.unwrap_or_else(|e| log_error!("{}", e)); - }); - Ok(()) - }) - .build()?; - - let this = self.clone(); - let increase_font = MenuItemBuilder::new() - .label("Increase Font") - .key(if is_windows() { "=" } else { "+" }) - .modifiers(modifier) - .callback(move |_| -> std::result::Result<(), JsValue> { - // window().alert_with_message("Hello")?; - let this = this.clone(); - spawn(async move { - this.terminal().ipc().increase_font_size().await.unwrap_or_else(|e| log_error!("{}", e)); - }); - Ok(()) - }) - .build()?; - - let this = self.clone(); - let decrease_font = MenuItemBuilder::new() - .label("Decrease Font") - .key("-") - .modifiers(modifier) - .callback(move |_| -> std::result::Result<(), JsValue> { - // window().alert_with_message("Hello")?; - let this = this.clone(); - spawn(async move { - this.terminal().ipc().decrease_font_size().await.unwrap_or_else(|e| log_error!("{}", e)); - }); - Ok(()) - }) - .build()?; - - let this = self.clone(); - let toggle_metrics = MenuItemBuilder::new() - .label("Toggle Metrics") - .key("M") - .modifiers(modifier) - .callback(move |_| -> std::result::Result<(), JsValue> { - // window().alert_with_message("Hello")?; - let this = this.clone(); - spawn(async move { - this.toggle_metrics().await.unwrap_or_else(|e| log_error!("{}", e)); - // this.terminal().ipc().decrease_font_size().await.unwrap_or_else(|e| log_error!("{}", e)); - }); - Ok(()) - }) - .build()?; - - let terminal_item = MenuItemBuilder::new() - .label("Terminal") - .submenus(vec![clipboard_copy, clipboard_paste, menu_separator(), increase_font, decrease_font]) - .build()?; - - let metrics_item = MenuItemBuilder::new().label("Metrics").submenus(vec![toggle_metrics]).build()?; - MenubarBuilder::new("Kaspa OS", is_macos()) - .mac_hide_edit(true) - .mac_hide_window(true) - .append(terminal_item) - .append(metrics_item) - .build(true)?; - - Ok(()) - } - - /// Create application tray icon - pub fn _create_tray_icon(&self) -> Result<()> { - let _tray = TrayMenuBuilder::new() - .icon("resources/icons/tray-icon@2x.png") - .icons_are_templates(false) - .callback(|_| { - window().alert_with_message("Tray Icon click")?; - Ok(()) - }) - .build()?; - Ok(()) - } - - /// Create application tray icon and tray menu - pub fn _create_tray_icon_with_menu(self: Arc) -> Result<()> { - let this = self; - let submenu_1 = MenuItemBuilder::new() - .label("TEST IPC") - .key("6") - .modifiers("ctrl") - .callback(move |_| -> std::result::Result<(), JsValue> { - let this = this.clone(); - - spawn(async move { - let target = IpcTarget::new(this.terminal.lock().unwrap().as_ref().unwrap().window.as_ref()); - let req = TestReq { req: "Hello World...".to_string() }; - let _resp = target.call::(TermOps::TestTerminal, req).await; - }); - - Ok(()) - }) - .build()?; - - let exit_menu = MenuItemBuilder::new() - .label("Exit") - .callback(move |_| -> std::result::Result<(), JsValue> { - window().alert_with_message("TODO: Exit")?; - Ok(()) - }) - .build()?; - - let _tray = TrayMenuBuilder::new() - .icon("resources/icons/tray-icon@2x.png") - .icons_are_templates(false) - .submenus(vec![submenu_1, menu_separator(), exit_menu]) - .build()?; - - Ok(()) - } - - /// Create a custom application context menu - #[allow(dead_code)] - pub fn create_context_menu(self: Arc) -> Result<()> { - let item_1 = MenuItemBuilder::new() - .label("Sub Menu 1") - .callback(move |_| -> std::result::Result<(), JsValue> { - window().alert_with_message("Context menu 1 clicked")?; - Ok(()) - }) - .build()?; - - let item_2 = MenuItemBuilder::new() - .label("Sub Menu 2") - .callback(move |_| -> std::result::Result<(), JsValue> { - window().alert_with_message("Context menu 2 clicked")?; - Ok(()) - }) - .build()?; - - self.inner.create_context_menu(vec![item_1, item_2])?; - - Ok(()) - } - - fn register_ipc_handlers(self: &Arc) -> Result<()> { - let this = self.clone(); - self.ipc.method( - CoreOps::KaspadCtl, - Method::new(move |op: KaspadOps| { - let this = this.clone(); - Box::pin(async move { - match op { - KaspadOps::Configure(config) => { - this.kaspad.configure(config)?; - } - KaspadOps::DaemonCtl(ctl) => match ctl { - DaemonCtl::Start => { - this.kaspad.start()?; - } - DaemonCtl::Stop => { - this.kaspad.stop()?; - } - DaemonCtl::Join => { - this.kaspad.join().await?; - } - DaemonCtl::Restart => { - this.kaspad.restart()?; - } - DaemonCtl::Kill => { - this.kaspad.kill()?; - } - DaemonCtl::Mute(mute) => { - this.kaspad.mute(mute).await?; - } - DaemonCtl::ToggleMute => { - this.kaspad.toggle_mute().await?; - } - }, - } - - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::KaspadStatus, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - let uptime = this.kaspad.uptime().map(|u| u.as_secs()); - Ok(DaemonStatus { uptime }) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::KaspadVersion, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - let version = this.kaspad.version().await?; - Ok(version) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::CpuMinerCtl, - Method::new(move |op: CpuMinerOps| { - let this = this.clone(); - Box::pin(async move { - match op { - CpuMinerOps::Configure(config) => { - this.cpu_miner.configure(config)?; - } - CpuMinerOps::DaemonCtl(ctl) => match ctl { - DaemonCtl::Start => { - this.cpu_miner.start()?; - } - DaemonCtl::Stop => { - this.cpu_miner.stop()?; - } - DaemonCtl::Join => { - this.cpu_miner.join().await?; - } - DaemonCtl::Restart => { - this.cpu_miner.restart()?; - } - DaemonCtl::Kill => { - this.cpu_miner.kill()?; - } - DaemonCtl::Mute(mute) => { - this.cpu_miner.mute(mute).await?; - } - DaemonCtl::ToggleMute => { - this.cpu_miner.toggle_mute().await?; - } - }, - } - - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::CpuMinerStatus, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - let uptime = this.cpu_miner.uptime().map(|u| u.as_secs()); - Ok(DaemonStatus { uptime }) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::CpuMinerVersion, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - let version = this.cpu_miner.version().await?; - Ok(version) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::MetricsOpen, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - this.settings.set(CoreSettings::Metrics, true).await.ok(); - this.create_metrics_window().await?; - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::MetricsCtl, - Method::new(move |op: MetricsCtl| { - let this = this.clone(); - Box::pin(async move { - this.metrics_ctl(op).await?; - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::TerminalReady, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - this.terminal_ready_ctl.send(()).await.unwrap_or_else(|e| log_error!("Error signaling terminal init: {e}")); - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::MetricsReady, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - this.metrics_ready_ctl.send(()).await.unwrap_or_else(|e| log_error!("Error signaling terminal init: {e}")); - this.terminal().ipc().metrics_ctl(MetricsSinkCtl::Activate).await.unwrap_or_else(|e| log_error!("{}", e)); - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::MetricsClose, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - this.destroy_metrics_window().await.unwrap_or_else(|e| log_error!("{}", e)); - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - CoreOps::Shutdown, - Method::new(move |_op: ()| { - let this = this.clone(); - Box::pin(async move { - this.shutdown_ctl.send(()).await.unwrap_or_else(|err| log_error!("{}", err)); - Ok(()) - }) - }), - ); - - Ok(()) - } - - pub async fn handle_event(self: &Arc, event: DaemonEvent) -> Result<()> { - self.terminal().ipc().relay_event(event).await?; - - Ok(()) - } - - 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 kaspad_events_receiver = self.kaspad.events().receiver.clone(); - let cpu_miner_events_receiver = self.cpu_miner.events().receiver.clone(); - - spawn(async move { - loop { - select! { - _ = task_ctl_receiver.recv().fuse() => { - break; - }, - event = kaspad_events_receiver.recv().fuse() => { - if let Ok(event) = event { - this.handle_event(DaemonEvent::new(DaemonKind::Kaspad, event)).await.unwrap_or_else(|err| { - log_error!("error while handling kaspad stdout: {err}"); - }); - } - }, - event = cpu_miner_events_receiver.recv().fuse() => { - if let Ok(event) = event { - this.handle_event(DaemonEvent::new(DaemonKind::CpuMiner, event)).await.unwrap_or_else(|err| { - log_error!("error while handling cpu miner stdout: {err}"); - }); - } - }, - } - } - - task_ctl_sender.send(()).await.unwrap(); - }); - - Ok(()) - } - - pub async fn stop_task(&self) -> Result<()> { - self.task_ctl.signal(()).await.expect("Core::stop_task() `signal` error"); - Ok(()) - } - - pub async fn create_terminal_window(self: &Arc) -> Result<()> { - let window = Arc::new( - Application::create_window_async( - "/app/index.html", - &nw_sys::window::Options::new().new_instance(false).height(768).width(1280).show(false), - ) - .await?, - ); - - self.terminal.lock().unwrap().replace(Arc::new(Terminal::new(window))); - - self.terminal_ready_ctl.recv().await.unwrap_or_else(|e| { - log_error!("Core::main() `terminal_ready_ctl` error: {e}"); - }); - - Ok(()) - } - - pub async fn create_metrics_window(self: &Arc) -> Result<()> { - if self.metrics().is_none() { - // log_info!("*** CREATING WINDOW ***"); - let window = Arc::new( - Application::create_window_async( - "/app/metrics.html", - &nw_sys::window::Options::new().new_instance(false).height(768).width(1280).show(false), - ) - .await - .expect("Core: failed to create metrics window"), - ); - // log_info!("*** WINDOW CREATED ***"); - let metrics = Arc::new(Metrics::new(window)); - *self.metrics.lock().unwrap() = Some(metrics); - - self.metrics_ready_ctl.recv().await.unwrap_or_else(|e| { - log_error!("Core::main() `terminal_ready_ctl` error: {e}"); - }); - } - Ok(()) - } - - pub async fn destroy_metrics_window(self: &Arc) -> Result<()> { - if let Some(metrics) = self.metrics() { - self.settings.set(CoreSettings::Metrics, false).await.ok(); - self.terminal().ipc().metrics_ctl(MetricsSinkCtl::Deactivate).await.unwrap_or_else(|e| log_error!("{}", e)); - metrics.window().close_impl(true); - self.metrics.lock().unwrap().take(); - } - Ok(()) - } - - pub async fn metrics_ctl(self: &Arc, _ctl: MetricsCtl) -> Result<()> { - if let Some(_metrics) = self.metrics() { - // metrics.ctl(ctl).await?; - } - Ok(()) - } - - pub async fn toggle_metrics(self: &Arc) -> Result<()> { - if self.metrics().is_none() { - self.create_metrics_window().await.unwrap_or_else(|e| log_error!("Core::toggle_metrics() error: {e}")); - } else { - self.destroy_metrics_window().await.unwrap_or_else(|e| log_error!("Core::toggle_metrics() error: {e}")); - } - Ok(()) - } - - pub async fn main(self: &Arc) -> Result<()> { - log_info!("-> register ipc handlers"); - self.register_ipc_handlers()?; - - log_info!("-> create terminal window"); - self.create_terminal_window().await?; - - log_info!("-> create application menu"); - self.create_menu()?; - - log_info!("-> start daemon event relay task"); - self.start_task().await?; - - // self.terminal_ready_ctl.recv().await.unwrap_or_else(|e| { - // log_error!("Core::main() `terminal_init_ctl` error: {e}"); - // }); - - log_info!("-> create metrics window"); - if let Some(metrics) = self.settings.get(CoreSettings::Metrics) { - if metrics { - self.create_metrics_window().await?; - } - } - - log_info!("-> await shutdown signal ..."); - self.shutdown_ctl.recv().await?; - - log_info!("-> shutdown daemon event relay task"); - self.stop_task().await?; - - Ok(()) - } -} - -#[wasm_bindgen] -pub async fn init_core() -> Result<()> { - workflow_wasm::panic::init_console_panic_hook(); - kaspa_core::log::set_log_level(LevelFilter::Info); - - if let Err(e) = ensure_application_folder().await { - let home_dir = application_folder().map(|f| f.display().to_string()).unwrap_or("???".to_string()); - let err = format!("Unable to access user home folder `{home_dir}` (do you have access?): {e}"); - window().alert_with_message(&err)?; - } - - let core = Core::try_new().await?; - core.main().await?; - - nw_sys::app::quit(); - - Ok(()) -} diff --git a/kos/src/core/ipc.rs b/kos/src/core/ipc.rs deleted file mode 100644 index 90544ab20..000000000 --- a/kos/src/core/ipc.rs +++ /dev/null @@ -1,202 +0,0 @@ -use crate::imports::*; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum CoreOps { - TestBg, - Shutdown, - TerminalReady, - KaspadCtl, - KaspadStatus, - KaspadVersion, - CpuMinerCtl, - CpuMinerStatus, - CpuMinerVersion, - MetricsOpen, - MetricsClose, - MetricsReady, - MetricsCtl, -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub struct TestReq { - pub req: String, -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub struct TestResp { - pub resp: String, -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum DaemonCtl { - Start, - Stop, - Join, - Restart, - Kill, - Mute(bool), - ToggleMute, -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum KaspadOps { - Configure(KaspadConfig), - DaemonCtl(DaemonCtl), -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum CpuMinerOps { - Configure(CpuMinerConfig), - DaemonCtl(DaemonCtl), -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum MetricsCtl { - Retention(u64), -} - -#[derive(Debug, Clone)] -pub struct CoreIpc { - target: IpcTarget, -} - -impl CoreIpc { - pub fn new(target: IpcTarget) -> CoreIpc { - CoreIpc { target } - } - - pub async fn shutdown(&self) -> Result<()> { - self.target.call(CoreOps::Shutdown, ()).await?; - Ok(()) - } - - pub async fn metrics_open(&self) -> Result<()> { - self.target.call(CoreOps::MetricsOpen, ()).await?; - Ok(()) - } - - pub async fn metrics_close(&self) -> Result<()> { - self.target.call(CoreOps::MetricsClose, ()).await?; - Ok(()) - } - - pub async fn metrics_ctl(&self, ctl: MetricsCtl) -> Result<()> { - self.target.call(CoreOps::MetricsCtl, ctl).await?; - Ok(()) - } - - pub async fn metrics_ready(&self) -> Result<()> { - self.target.call(CoreOps::MetricsReady, ()).await?; - Ok(()) - } - - pub async fn terminal_ready(&self) -> Result<()> { - self.target.call(CoreOps::TerminalReady, ()).await?; - Ok(()) - } -} - -#[async_trait] -impl KaspadCtl for CoreIpc { - async fn configure(&self, config: KaspadConfig) -> DaemonResult<()> { - // self.target.call::<_, _, ()>(CoreOps::KaspadCtl, KaspadOps::Configure(config)).await?; - self.target.call(CoreOps::KaspadCtl, KaspadOps::Configure(config)).await?; - - Ok(()) - } - - async fn start(&self) -> DaemonResult<()> { - self.target.call(CoreOps::KaspadCtl, KaspadOps::DaemonCtl(DaemonCtl::Start)).await?; - Ok(()) - } - - async fn stop(&self) -> DaemonResult<()> { - self.target.call(CoreOps::KaspadCtl, KaspadOps::DaemonCtl(DaemonCtl::Stop)).await?; - Ok(()) - } - - async fn join(&self) -> DaemonResult<()> { - self.target.call(CoreOps::KaspadCtl, KaspadOps::DaemonCtl(DaemonCtl::Join)).await?; - Ok(()) - } - - async fn restart(&self) -> DaemonResult<()> { - self.target.call(CoreOps::KaspadCtl, KaspadOps::DaemonCtl(DaemonCtl::Restart)).await?; - Ok(()) - } - - async fn kill(&self) -> DaemonResult<()> { - self.target.call(CoreOps::KaspadCtl, KaspadOps::DaemonCtl(DaemonCtl::Kill)).await?; - Ok(()) - } - - async fn status(&self) -> DaemonResult { - Ok(self.target.call(CoreOps::KaspadStatus, ()).await?) - } - - async fn version(&self) -> DaemonResult { - Ok(self.target.call(CoreOps::KaspadVersion, ()).await?) - } - - async fn mute(&self, mute: bool) -> DaemonResult<()> { - self.target.call(CoreOps::KaspadCtl, KaspadOps::DaemonCtl(DaemonCtl::Mute(mute))).await?; - Ok(()) - } - - async fn toggle_mute(&self) -> DaemonResult<()> { - self.target.call(CoreOps::KaspadCtl, KaspadOps::DaemonCtl(DaemonCtl::ToggleMute)).await?; - Ok(()) - } -} - -#[async_trait] -impl CpuMinerCtl for CoreIpc { - async fn configure(&self, config: CpuMinerConfig) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, CpuMinerOps::Configure(config)).await?; - - Ok(()) - } - - async fn start(&self) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, KaspadOps::DaemonCtl(DaemonCtl::Start)).await?; - Ok(()) - } - - async fn stop(&self) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, KaspadOps::DaemonCtl(DaemonCtl::Stop)).await?; - Ok(()) - } - - async fn join(&self) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, KaspadOps::DaemonCtl(DaemonCtl::Join)).await?; - Ok(()) - } - - async fn restart(&self) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, KaspadOps::DaemonCtl(DaemonCtl::Restart)).await?; - Ok(()) - } - - async fn kill(&self) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, KaspadOps::DaemonCtl(DaemonCtl::Kill)).await?; - Ok(()) - } - - async fn status(&self) -> DaemonResult { - Ok(self.target.call(CoreOps::CpuMinerStatus, ()).await?) - } - - async fn version(&self) -> DaemonResult { - Ok(self.target.call(CoreOps::CpuMinerVersion, ()).await?) - } - - async fn mute(&self, mute: bool) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, KaspadOps::DaemonCtl(DaemonCtl::Mute(mute))).await?; - Ok(()) - } - - async fn toggle_mute(&self) -> DaemonResult<()> { - self.target.call(CoreOps::CpuMinerCtl, KaspadOps::DaemonCtl(DaemonCtl::ToggleMute)).await?; - Ok(()) - } -} diff --git a/kos/src/core/mod.rs b/kos/src/core/mod.rs deleted file mode 100644 index 2c8efc988..000000000 --- a/kos/src/core/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[allow(clippy::module_inception)] -mod core; -mod ipc; -pub use ipc::*; -mod settings; -pub use settings::*; diff --git a/kos/src/core/settings.rs b/kos/src/core/settings.rs deleted file mode 100644 index a79e41cd0..000000000 --- a/kos/src/core/settings.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::imports::*; - -#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[serde(rename_all = "lowercase")] -pub enum CoreSettings { - Metrics, -} - -#[async_trait] -impl DefaultSettings for CoreSettings { - async fn defaults() -> Vec<(Self, Value)> { - vec![] - } -} diff --git a/kos/src/error.rs b/kos/src/error.rs deleted file mode 100644 index c16cc906d..000000000 --- a/kos/src/error.rs +++ /dev/null @@ -1,107 +0,0 @@ -use downcast::DowncastError; -use kaspa_daemon::error::Error as DaemonError; -use thiserror::Error; -use wasm_bindgen::JsValue; -use workflow_core::channel::ChannelError; -use workflow_nw::ipc::ResponseError; -use workflow_wasm::printable::Printable; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Custom(String), - - #[error(transparent)] - Io(#[from] std::io::Error), - - #[error(transparent)] - Utf8(#[from] std::string::FromUtf8Error), - - #[error(transparent)] - WorkflowNw(#[from] workflow_nw::error::Error), - - #[error(transparent)] - Cli(#[from] kaspa_cli_lib::error::Error), - - #[error(transparent)] - Ipc(#[from] workflow_nw::ipc::error::Error), - - #[error("{0}")] - JsValue(Printable), - - #[error("{0}")] - Terminal(#[from] workflow_terminal::error::Error), - - #[error("channel error")] - Recv(#[from] workflow_core::channel::RecvError), - - #[error(transparent)] - Callback(#[from] workflow_wasm::callback::CallbackError), - - #[error("{0}")] - Downcast(String), - - #[error("Channel error")] - Channel(String), - - #[error(transparent)] - Daemon(#[from] kaspa_daemon::error::Error), - - #[error(transparent)] - Wallet(#[from] kaspa_wallet_core::error::Error), - - #[error(transparent)] - Dom(#[from] workflow_dom::error::Error), - - #[error(transparent)] - D3(#[from] workflow_d3::error::Error), -} - -impl From for JsValue { - fn from(err: Error) -> JsValue { - let s: String = err.to_string(); - JsValue::from_str(&s) - } -} - -impl From for Error { - fn from(js_value: JsValue) -> Error { - Error::JsValue(Printable::new(js_value)) - } -} - -impl From for ResponseError { - fn from(err: Error) -> ResponseError { - ResponseError::Custom(err.to_string()) - } -} - -impl From for DaemonError { - fn from(err: Error) -> DaemonError { - DaemonError::Custom(err.to_string()) - } -} - -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()) - } -} - -impl From> for Error { - fn from(e: DowncastError) -> Self { - Error::Downcast(e.to_string()) - } -} - -impl From> for Error { - fn from(e: ChannelError) -> Error { - Error::Channel(e.to_string()) - } -} diff --git a/kos/src/imports.rs b/kos/src/imports.rs deleted file mode 100644 index c38dc7f0c..000000000 --- a/kos/src/imports.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub use crate::core::MetricsCtl; -pub use crate::core::*; -pub use crate::ipc::*; -pub use crate::layout::Layout; -pub use crate::metrics::*; -pub use crate::result::Result; -pub use crate::terminal::*; -pub use async_trait::async_trait; -pub use borsh::{BorshDeserialize, BorshSerialize}; -pub use futures::{select, stream::StreamExt, FutureExt}; -pub use kaspa_cli_lib::{KaspaCli, Options as KaspaCliOptions}; -pub use kaspa_daemon::{ - CpuMiner, CpuMinerConfig, CpuMinerCtl, DaemonEvent, DaemonKind, DaemonStatus, Daemons, Kaspad, KaspadConfig, KaspadCtl, - Result as DaemonResult, -}; -pub use kaspa_wallet_core::settings::{DefaultSettings, SettingsStore, SettingsStoreT}; -pub use nw_sys::prelude::*; -pub use serde::{Deserialize, Serialize}; -pub use serde_json::Value; -pub use wasm_bindgen::prelude::*; -pub use workflow_core::channel::*; -pub use workflow_core::runtime::*; -pub use workflow_core::task::*; -pub use workflow_core::time::*; -pub use workflow_log::*; -pub use workflow_nw::ipc::*; -pub use workflow_nw::prelude::*; -pub use workflow_terminal::prelude::*; -pub use workflow_terminal::{CrLf, Options as TerminalOptions}; -pub use workflow_wasm::callback::{callback, AsCallback, CallbackMap}; diff --git a/kos/src/ipc.rs b/kos/src/ipc.rs deleted file mode 100644 index 195920288..000000000 --- a/kos/src/ipc.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::fmt; - -#[derive(Clone, Debug)] -pub enum Modules { - Core, - Terminal, - Metrics, -} - -impl fmt::Display for Modules { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Modules::Core => write!(f, "core"), - Modules::Terminal => write!(f, "terminal"), - Modules::Metrics => write!(f, "metrics"), - } - } -} diff --git a/kos/src/layout.rs b/kos/src/layout.rs deleted file mode 100644 index 0a882d812..000000000 --- a/kos/src/layout.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::imports::*; - -#[derive(Serialize, Deserialize, Debug, Clone)] -struct Position { - x: u32, - y: u32, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -struct Size { - width: u32, - height: u32, -} - -pub struct Layout -where - S: SettingsStoreT, -{ - pub callbacks: CallbackMap, - pub settings: Arc, - pub window: Arc, -} - -impl Layout -where - S: SettingsStoreT, -{ - pub async fn try_new(window: &Arc, settings: &Arc) -> Result { - let callbacks = CallbackMap::new(); - - let settings_ = settings.clone(); - let move_window = callback!(move |x: u32, y: u32| { - let settings_ = settings_.clone(); - let position = Position { x, y }; - spawn(async move { - settings_.set("window.position", position).await.unwrap_or_else(|err| { - log_error!("Unable to store window position: {err}"); - }); - }) - }); - window.on("move", move_window.as_ref()); - callbacks.retain(move_window)?; - - let settings_ = settings.clone(); - let resize_window = callback!(move |width: u32, height: u32| { - let settings_ = settings_.clone(); - let size = Size { width, height }; - spawn(async move { - settings_.set("window.size", size).await.unwrap_or_else(|err| { - log_error!("Unable to store window size: {err}"); - }); - }) - }); - window.on("resize", resize_window.as_ref()); - callbacks.retain(resize_window)?; - - if let Some(position) = settings.get::("window.position").await { - window.move_to(position.x, position.y); - } - - if let Some(size) = settings.get::("window.size").await { - window.resize_to(size.width, size.height); - } - - window.show(); - - Ok(Self { callbacks, settings: settings.clone(), window: window.clone() }) - } -} diff --git a/kos/src/lib.rs b/kos/src/lib.rs deleted file mode 100644 index db8ff8a1f..000000000 --- a/kos/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod core; -mod error; -mod imports; -mod ipc; -mod layout; -mod metrics; -mod modules; -mod result; -mod terminal; diff --git a/kos/src/metrics/ipc.rs b/kos/src/metrics/ipc.rs deleted file mode 100644 index 3ca02be6c..000000000 --- a/kos/src/metrics/ipc.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::imports::*; -use kaspa_metrics_core::MetricsSnapshot; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum MetricsOps { - MetricsSnapshot, -} - -#[derive(Debug, Clone)] -pub struct MetricsIpc { - #[allow(dead_code)] - target: IpcTarget, -} - -impl MetricsIpc { - pub fn new(target: IpcTarget) -> MetricsIpc { - MetricsIpc { target } - } -} - -#[allow(dead_code)] -#[async_trait] -pub trait MetricsCtl: Send + Sync + 'static { - async fn post_data(&self, data: MetricsSnapshot) -> Result<()>; -} diff --git a/kos/src/metrics/metrics.rs b/kos/src/metrics/metrics.rs deleted file mode 100644 index fca64b4af..000000000 --- a/kos/src/metrics/metrics.rs +++ /dev/null @@ -1,186 +0,0 @@ -use super::toolbar::*; -use crate::imports::*; -use kaspa_metrics_core::{Metric, MetricsSnapshot}; -use std::collections::HashMap; -use workflow_core::time::{HOURS, MINUTES}; -use workflow_d3::container::*; -use workflow_d3::graph::*; - -static mut METRICS: Option> = None; - -#[derive(Clone)] -pub struct Metrics { - pub inner: Arc, - pub ipc: Arc>, - pub core: Arc, - pub window: Arc, - pub callbacks: CallbackMap, - pub settings: Arc>, - pub layout: Arc>>, - pub container: Arc>>>, - pub graphs: Arc>>>, - pub toolbar: Toolbar, -} - -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 acquire background window"); - let core = Arc::new(CoreIpc::new(core_ipc_target)); - - let settings = Arc::new(SettingsStore::::try_new("metrics")?); - settings.try_load().await?; - // - TODO - setup graph time duration - let _default_duration = settings.get::(MetricsSettings::Duration); - - let window = Arc::new(nw_sys::window::get()); - - let layout = Arc::new(Layout::try_new(&window, &settings).await?); - let container = Arc::new(Mutex::new(None)); - let graphs = Arc::new(Mutex::new(HashMap::new())); - let toolbar = Toolbar::try_new(&window.window(), &container, &graphs)?; - toolbar.try_init()?; - let app = Arc::new(Self { - inner: Application::new()?, - ipc: Ipc::try_new_window_binding(&window, Modules::Metrics)?, - core, - window, - callbacks: CallbackMap::default(), - // shutdown: Arc::new(AtomicBool::new(false)), - settings, - layout, - container, - graphs, - toolbar, - }); - - unsafe { - METRICS = Some(app.clone()); - }; - - Ok(app) - } - - fn register_ipc_handlers(self: &Arc) -> Result<()> { - let this = self.clone(); - self.ipc.notification( - MetricsOps::MetricsSnapshot, - Notification::new(move |data: MetricsSnapshot| { - let this = this.clone(); - Box::pin(async move { - // log_info!("Received metrics data: {:?}", data); - this.ingest(data).await?; - yield_executor().await; - Ok(()) - }) - }), - ); - - Ok(()) - } - - fn register_window_handlers(self: &Arc) -> Result<()> { - let this = self.clone(); - let close_window = callback!(move || { - let this = this.clone(); - spawn(async move { - this.core.metrics_close().await.expect("Unable to close metrics"); - }); - }); - self.window.on("close", close_window.as_ref()); - self.callbacks.retain(close_window)?; - - Ok(()) - } - - pub fn theme(name: &str, kind: &str) -> Box { - let font = "'Consolas', 'Lucida Grande', 'Roboto Mono', 'Source Code Pro', 'Trebuchet'"; - - let primary = match name { - "light" => "black", - "dark" => "#ccc", - _ => "grey", - }; - - let theme = match kind { - "kaspa" => GraphThemeOptions::new(font, primary, "rgb(220, 240, 231)", "rgb(17, 187, 125)", primary), - _ => GraphThemeOptions::new(font, primary, "rgb(220, 231, 240)", "rgb(17, 125, 187)", primary), - }; - - Box::new(theme) - } - - async fn init_graphs(self: &Arc) -> Result<()> { - let window = self.window.window(); - - Container::try_init().await?; - Graph::try_init(Some("graph")).await?; - - let container = Arc::new(Container::try_new(&window).await?); - *self.container.lock().unwrap() = Some(container.clone()); - let mut graphs = vec![]; - for metric in Metric::list() { - let graph = Arc::new( - Graph::try_new( - &self.window.window(), - &container, - None, - "", - Duration::from_millis(5 * MINUTES), - Duration::from_millis(48 * HOURS), - GraphTheme::Custom(Self::theme("light", metric.group())), - Margin::new(20.0, 20.0, 10.0, 30.0), - ) - .await?, - ); - self.graphs.lock().unwrap().insert(metric, graph.clone()); - graphs.push(graph); - } - - Ok(()) - } - - fn graph(&self, metric: &Metric) -> Arc { - self.graphs.lock().unwrap().get(metric).cloned().expect("Unable to find graph") - } - - async fn ingest(self: &Arc, data: MetricsSnapshot) -> Result<()> { - let si = true; - for metric in Metric::list() { - let value = data.get(&metric); - self.graph(&metric).ingest(data.unixtime_millis, value, &data.format(&metric, si, false)).await?; - } - - yield_executor().await; - sleep(Duration::from_millis(100)).await; - - Ok(()) - } - - async fn main(self: &Arc) -> Result<()> { - self.register_window_handlers()?; - self.register_ipc_handlers()?; - - self.init_graphs().await?; - - // this call reflects from core to terminal - // initiating metrics data relay - self.core.metrics_ready().await?; - - Ok(()) - } -} - -#[wasm_bindgen] -pub async fn init_metrics() -> Result<()> { - kaspa_core::log::set_log_level(LevelFilter::Info); - workflow_log::set_colors_enabled(true); - - let metrics = Metrics::try_new().await.unwrap_or_else(|err| { - panic!("Unable to initialize metrics: {:?}", err); - }); - metrics.main().await?; - - Ok(()) -} diff --git a/kos/src/metrics/mod.rs b/kos/src/metrics/mod.rs deleted file mode 100644 index c9ed93da5..000000000 --- a/kos/src/metrics/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod ipc; -#[allow(clippy::module_inception)] -mod metrics; -pub use ipc::*; -mod settings; -pub use settings::*; -mod toolbar; diff --git a/kos/src/metrics/settings.rs b/kos/src/metrics/settings.rs deleted file mode 100644 index e42e7990b..000000000 --- a/kos/src/metrics/settings.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::imports::*; - -#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[serde(rename_all = "lowercase")] -pub enum MetricsSettings { - Duration, -} - -#[async_trait] -impl DefaultSettings for MetricsSettings { - async fn defaults() -> Vec<(Self, Value)> { - vec![] - } -} diff --git a/kos/src/metrics/toolbar.css b/kos/src/metrics/toolbar.css deleted file mode 100644 index 0655518ca..000000000 --- a/kos/src/metrics/toolbar.css +++ /dev/null @@ -1,36 +0,0 @@ -.toolbar{ - padding:10px; - user-select: none; -} -.toolbar .control{display: inline-block} -.toolbar .separator{ - width: 1px; - background: #aaa; - display: inline-block; - height: 12px; - margin: 0px 5px; -} - -.toolbar .button{ - display:inline-block; - width: auto; - border:1px solid #CCC; - border-radius:4px; - padding:5px 8px; - margin:2px; - cursor:pointer; - transition: all 0.2s ease-out; - color:#171717 -} - -.toolbar .button:hover { - background-color: #f6f6f6; - transform-origin: center center; - transform: scale(1.12); -} -.toolbar .button:has(input:checked){ - background-color: #efefef; - border-color: #aaa; - color: #000; -} -.toolbar .button input{display:none;} diff --git a/kos/src/metrics/toolbar.rs b/kos/src/metrics/toolbar.rs deleted file mode 100644 index 41fe8c2a5..000000000 --- a/kos/src/metrics/toolbar.rs +++ /dev/null @@ -1,356 +0,0 @@ -#![allow(dead_code)] - -use crate::imports::*; -use crate::result::Result; -use std::{collections::HashMap, sync::MutexGuard}; - -use kaspa_metrics_core::Metric; -use web_sys::{Document, Element, MouseEvent}; -use workflow_d3::graph::GraphDuration; -#[allow(unused_imports)] -use workflow_d3::{container::Container, graph::Graph}; -use workflow_dom::inject::inject_css; - -#[derive(Clone, Debug)] -pub struct Count(usize, String); - -impl Count { - pub fn cols() -> [Count; 4] { - [Count(1, "L".into()), Count(2, "M".into()), Count(3, "S".into()), Count(4, "T".into())] - } - - pub fn rows() -> [Count; 5] { - [Count(1, "F".into()), Count(2, "L".into()), Count(4, "M".into()), Count(6, "S".into()), Count(8, "T".into())] - } - - fn get_cols(&self) -> String { - let w = 100.0 / self.0 as f64; - format!("width: calc({w}vw - 10px);") - } - - fn get_rows(&self) -> String { - let h = 100.0 / self.0 as f64; - format!("height: {h}vh;") - } -} - -type Rows = Count; -type Cols = Count; - -#[derive(Debug)] -pub enum Action { - Duration(Duration), - Cols(Count), - Rows(Count), -} - -pub struct ToolbarInner { - pub document: Document, - pub element: Element, - pub callbacks: CallbackMap, - pub container: Arc>>>, - pub graphs: Arc>>>, - pub controls: Arc>>>, - pub layout: Arc>, -} - -unsafe impl Send for ToolbarInner {} -unsafe impl Sync for ToolbarInner {} - -const STYLE: &str = include_str!("toolbar.css"); -static mut DOM_INIT: bool = false; - -#[derive(Clone)] -pub struct Toolbar { - inner: Arc, -} - -impl Toolbar { - pub fn try_new( - window: &web_sys::Window, - container: &Arc>>>, - graphs: &Arc>>>, - ) -> Result { - if !unsafe { DOM_INIT } { - inject_css(Some("toolbar-style"), STYLE)?; - unsafe { - DOM_INIT = true; - } - } - - let document = window.document().unwrap(); - let element = document.create_element("div").unwrap(); - element.set_class_name("toolbar"); - let body = document.query_selector("body").unwrap().expect("Toolbar unable to get body element"); - body.append_child(&element).unwrap(); - - Ok(Self { - inner: Arc::new(ToolbarInner { - document, - element, - container: container.clone(), - graphs: graphs.clone(), - callbacks: CallbackMap::default(), - controls: Arc::new(Mutex::new(Vec::new())), - layout: Arc::new(Mutex::new((Count::cols().get(2).unwrap().clone(), Count::rows().get(3).unwrap().clone()))), - }), - }) - } - - pub fn document(&self) -> &Document { - &self.inner.document - } - - pub fn element(&self) -> &Element { - &self.inner.element - } - - pub fn layout(&self) -> MutexGuard<(Cols, Rows)> { - self.inner.layout.lock().unwrap() - } - - pub fn controls(&self) -> MutexGuard>> { - self.inner.controls.lock().unwrap() - } - - pub fn push(&self, control: impl Control + Send + Sync + 'static) { - let control = Arc::new(control); - self.controls().push(control); - } - - pub fn try_init(&self) -> Result<()> { - let durations = vec![ - ("1m", "1 Minute"), - ("5m", "1 Minutes"), - ("15m", "15 Minutes"), - ("30m", "30 Minutes"), - ("1H", "1 Hour"), - ("2H", "2 Hours"), - ("4H", "4 Hours"), - ("8H", "8 Hours"), - ("12H", "12 Hours"), - ("24H", "24 Hours"), - ("36H", "36 Hours"), - ("48H", "48 Hours"), - ("72H", "72 Hours"), - ]; - - for (html, tip) in durations { - let this = self.clone(); - let duration = GraphDuration::parse(html.to_lowercase()).unwrap(); - self.push(RadioButton::try_new( - self, - self.element(), - "duration", - html, - &format!("Set graph time range to {tip}"), - Arc::new(move |btn| this.action(btn, Action::Duration(duration))), - )?); - } - - Separator::try_new(self)?; - - for cols in Count::cols() { - let this = self.clone(); - self.push(RadioButton::try_new( - self, - self.element(), - "cols", - // &format!("{}", width.1), - &cols.1.to_string(), - &format!("Set graph layout to {} columns", cols.0), - Arc::new(move |btn| this.action(btn, Action::Cols(cols.clone()))), - )?); - } - Separator::try_new(self)?; - - for rows in Count::rows() { - let this = self.clone(); - self.push(RadioButton::try_new( - self, - self.element(), - "rows", - // &format!("{}", height.1), - &rows.1.to_string(), - &format!("Set graph layout to {} rows", rows.0), - Arc::new(move |btn| this.action(btn, Action::Rows(rows.clone()))), - )?); - } - - let this = self.clone(); - spawn(async move { - // sleep(Duration::from_millis(100)).await; - this.select("duration", "5m").expect("unable to locate duration element"); - this.select("cols", "S").expect("unable to locate width element"); - this.select("rows", "S").expect("unable to locate height element"); - this.update_layout(this.inner.layout.lock().unwrap().clone()); - }); - - Ok(()) - } - - pub fn select(&self, name: &str, value: &str) -> Result<()> { - let el = self.document().query_selector(&format!("input[name='{}'][value='{}']", name, value))?.unwrap(); - let event = MouseEvent::new("click").unwrap(); - el.dispatch_event(&event).unwrap(); - Ok(()) - } - - pub fn action(&self, _btn: &dyn Control, action: Action) { - // log_info!("action: {:?}", action); - match action { - Action::Duration(duration) => { - let graphs = self.inner.graphs.lock().unwrap(); - for graph in (*graphs).values() { - graph.set_duration(duration).unwrap_or_else(|err| { - log_error!("unable to set graph duration: {}", err); - }); - } - } - Action::Cols(cols) => { - let layout = { - let mut layout = self.layout(); - layout.0 = cols; - layout.clone() - }; - self.update_layout(layout); - } - Action::Rows(rows) => { - let layout = { - let mut layout = self.layout(); - layout.1 = rows; - layout.clone() - }; - self.update_layout(layout); - } - } - } - - fn update_layout(&self, layout: (Cols, Rows)) { - dispatch(async move { - let style = Graph::default_style().await.expect("unable to get graph style"); - // let re = Regex::new(r"(min|max)?-?(width|height)\s*:\d+(vw|vh);").unwrap(); - // let style = re.replace_all(style.as_str(), ""); - let new_style = format!( - r#"{} - .graph {{ {}{} }} - "#, - style, - layout.0.get_cols(), - layout.1.get_rows() - ); - Graph::replace_graph_style("graph", &new_style).await.unwrap_or_else(|err| { - log_error!("unable to replace graph style: {}", err); - }) - }) - } -} - -type ButtonCallback = dyn Fn(&Button) + Send + Sync + 'static; - -#[derive(Clone)] -pub struct Button { - pub callbacks: CallbackMap, - pub element: Element, -} - -impl Button { - pub fn try_new(toolbar: &Toolbar, parent: &Element, html: &str, tooltip: &str, callback: Arc) -> Result { - let element = toolbar.document().create_element("div").unwrap(); - element.set_class_name("button"); - element.set_attribute("title", tooltip)?; - element.set_inner_html(html); - parent.append_child(&element).unwrap(); - let callbacks = CallbackMap::default(); - let button = Self { callbacks, element }; - let this = button.clone(); - let callback = Arc::new(callback); - let click = callback!(move || { - callback(&this); - }); - - button.element.add_event_listener_with_callback("click", click.get_fn())?; - button.callbacks.retain(click)?; - - Ok(button) - } -} - -type RadioButtonCallback = dyn Fn(&RadioButton) + Send + Sync + 'static; - -#[derive(Clone)] -pub struct RadioButton { - pub callbacks: CallbackMap, - pub element: Element, -} - -unsafe impl Send for RadioButton {} -unsafe impl Sync for RadioButton {} - -impl RadioButton { - pub fn try_new( - toolbar: &Toolbar, - parent: &Element, - name: &str, - html: &str, - tooltip: &str, - callback: Arc, - ) -> Result { - let element = toolbar.document().create_element("label").unwrap(); - element.set_class_name("button"); - element.set_attribute("title", tooltip)?; - element.set_inner_html(html); - let radio = toolbar.document().create_element("input").unwrap(); - radio.set_attribute("name", name)?; - radio.set_attribute("type", "radio")?; - radio.set_attribute("value", html)?; - element.append_child(&radio)?; - parent.append_child(&element).unwrap(); - let callbacks = CallbackMap::default(); - let button = Self { callbacks, element }; - let this = button.clone(); - let callback = Arc::new(callback); - let click = callback!(move || { - callback(&this); - }); - - button.element.add_event_listener_with_callback("click", click.get_fn())?; - button.callbacks.retain(click)?; - - Ok(button) - } -} - -pub struct Caption { - pub element: Element, -} - -impl Caption { - pub fn try_new(toolbar: &Toolbar, html: &str) -> Result { - let element = toolbar.document().create_element("div").unwrap(); - element.set_class_name("caption"); - element.set_inner_html(html); - toolbar.element().append_child(&element).unwrap(); - - Ok(Self { element }) - } -} - -pub struct Separator { - pub element: Element, -} - -impl Separator { - pub fn try_new(toolbar: &Toolbar) -> Result { - let element = toolbar.document().create_element("div").unwrap(); - element.set_class_name("separator"); - toolbar.element().append_child(&element).unwrap(); - - Ok(Self { element }) - } -} - -pub trait Control {} - -impl Control for Button {} -impl Control for RadioButton {} diff --git a/kos/src/modules/exit.rs b/kos/src/modules/exit.rs deleted file mode 100644 index 795792e54..000000000 --- a/kos/src/modules/exit.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::imports::*; - -#[derive(Default, Handler)] -#[help("Exit this application")] -pub struct Exit; - -impl Exit { - async fn main(self: Arc, ctx: &Arc, _argv: Vec, _cmd: &str) -> Result<()> { - let ctx = ctx.clone().downcast_arc::()?; - ctx.wallet().select(None).await?; - // ctx.term().refresh_prompt(); - // tprintln!(ctx, "{}", style("System is going down ...").blue()); - ctx.shutdown().await?; - tprintln!(ctx, "bye!"); - - // workflow_core::task::sleep(Duration::from_secs(10)).await; - - Ok(()) - } -} diff --git a/kos/src/modules/metrics.rs b/kos/src/modules/metrics.rs deleted file mode 100644 index 28d76e3cc..000000000 --- a/kos/src/modules/metrics.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::imports::*; -use kaspa_cli_lib::modules::metrics::Metrics as Inner; -use kaspa_metrics_core::MetricsSinkFn; - -pub struct Metrics { - inner: Arc, - core: Arc, -} - -impl Metrics { - pub fn new(core: &Arc) -> Self { - Self { core: core.clone(), inner: Arc::new(Inner::default()) } - } -} - -#[async_trait] -impl Handler for Metrics { - fn verb(&self, ctx: &Arc) -> Option<&'static str> { - self.inner.verb(ctx) - } - - fn help(&self, ctx: &Arc) -> &'static str { - self.inner.help(ctx) - } - - async fn start(self: Arc, ctx: &Arc) -> cli::Result<()> { - self.inner.clone().start(ctx).await - } - - async fn stop(self: Arc, ctx: &Arc) -> cli::Result<()> { - self.inner.clone().stop(ctx).await - } - - 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.to_string())?; - Ok(()) - } -} - -impl Metrics { - pub fn register_sink(&self, target: MetricsSinkFn) { - self.inner.register_sink(target); - } - - pub fn unregister_sink(&self) { - self.inner.unregister_sink(); - } - - async fn main(self: Arc, ctx: Arc, mut argv: Vec, _cmd: &str) -> Result<()> { - if argv.is_empty() { - self.inner.display_help(ctx, argv).await?; - return Ok(()); - } - match argv.remove(0).as_str() { - "open" => { - self.core.metrics_open().await?; - } - "close" => { - self.core.metrics_close().await?; - } - "retention" => { - if argv.is_empty() { - tprintln!(ctx, "missing retention value"); - return Ok(()); - } else { - let retention = argv.remove(0).parse::().map_err(|e| e.to_string())?; - if !(1..=168).contains(&retention) { - tprintln!(ctx, "retention value must be between 1 and 168 hours"); - return Ok(()); - } - self.core.metrics_ctl(MetricsCtl::Retention(retention)).await?; - } - } - v => { - tprintln!(ctx, "unknown command: '{v}'\r\n"); - - self.display_help(ctx, argv).await?; - return Ok(()); - } - } - - Ok(()) - } - - pub async fn display_help(self: &Arc, ctx: Arc, _argv: Vec) -> Result<()> { - ctx.term().help(&[("open", "Open metrics window"), ("close", "Close metrics window")], None)?; - - Ok(()) - } -} diff --git a/kos/src/modules/mod.rs b/kos/src/modules/mod.rs deleted file mode 100644 index 3d5613808..000000000 --- a/kos/src/modules/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod exit; -pub mod metrics; - -// pub use metrics::Metrics as MetricsModule; - -use crate::imports::*; - -pub fn register_cli_handlers(cli: &Arc) -> Result<()> { - register_handlers!(cli, cli.handlers(), [exit]); - - Ok(()) -} diff --git a/kos/src/result.rs b/kos/src/result.rs deleted file mode 100644 index 5c3ef710b..000000000 --- a/kos/src/result.rs +++ /dev/null @@ -1 +0,0 @@ -pub type Result = std::result::Result; diff --git a/kos/src/terminal/ipc.rs b/kos/src/terminal/ipc.rs deleted file mode 100644 index 70e320348..000000000 --- a/kos/src/terminal/ipc.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::imports::*; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum TermOps { - TestTerminal, - FontCtl, - EditCtl, - DaemonEvent, - MetricsCtl, -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum FontCtl { - IncreaseSize, - DecreaseSize, -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum EditCtl { - Copy, - Paste, -} - -#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] -pub enum MetricsSinkCtl { - Activate, - Deactivate, -} - -#[derive(Debug, Clone)] -pub struct TerminalIpc { - target: IpcTarget, -} - -impl TerminalIpc { - pub fn new(target: IpcTarget) -> TerminalIpc { - TerminalIpc { target } - } - - pub async fn clipboard_copy(&self) -> Result<()> { - self.target.call::<_, _, ()>(TermOps::EditCtl, EditCtl::Copy).await?; - Ok(()) - } - - pub async fn clipboard_paste(&self) -> Result<()> { - self.target.call::<_, _, ()>(TermOps::EditCtl, EditCtl::Paste).await?; - Ok(()) - } - - pub async fn increase_font_size(&self) -> Result<()> { - self.target.call::<_, _, ()>(TermOps::FontCtl, FontCtl::IncreaseSize).await?; - Ok(()) - } - - pub async fn decrease_font_size(&self) -> Result<()> { - self.target.call::<_, _, ()>(TermOps::FontCtl, FontCtl::DecreaseSize).await?; - Ok(()) - } - - pub async fn relay_event(&self, event: DaemonEvent) -> Result<()> { - self.target.notify(TermOps::DaemonEvent, event).await?; - Ok(()) - } - - pub async fn metrics_ctl(&self, ctl: MetricsSinkCtl) -> Result<()> { - self.target.notify(TermOps::MetricsCtl, ctl).await?; - Ok(()) - } -} diff --git a/kos/src/terminal/mod.rs b/kos/src/terminal/mod.rs deleted file mode 100644 index 193663c21..000000000 --- a/kos/src/terminal/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod ipc; -#[allow(clippy::module_inception)] -mod terminal; -pub use ipc::*; -mod settings; -pub use settings::*; diff --git a/kos/src/terminal/settings.rs b/kos/src/terminal/settings.rs deleted file mode 100644 index 5cd32d2be..000000000 --- a/kos/src/terminal/settings.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::imports::*; - -#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[serde(rename_all = "lowercase")] -pub enum TerminalSettings { - Greeting, - Scrollback, - FontSize, -} - -#[async_trait] -impl DefaultSettings for TerminalSettings { - async fn defaults() -> Vec<(Self, Value)> { - vec![] - } -} diff --git a/kos/src/terminal/terminal.rs b/kos/src/terminal/terminal.rs deleted file mode 100644 index 1410acb78..000000000 --- a/kos/src/terminal/terminal.rs +++ /dev/null @@ -1,285 +0,0 @@ -use crate::imports::*; -use kaspa_metrics_core::MetricsSnapshot; - -static mut TERMINAL: Option> = None; -static mut SHUTDOWN_ATTEMPTS: usize = 0; - -#[derive(Clone)] -pub struct Terminal { - pub inner: Arc, - pub ipc: Arc>, - pub core: Arc, - pub cli: Arc, - pub window: Arc, - pub callbacks: CallbackMap, - pub settings: Arc>, - pub layout: Arc>>, - pub metrics: Arc>>>, -} - -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 acquire background window"); - let core = Arc::new(CoreIpc::new(core_ipc_target)); - log_info!("-> creating daemon interface"); - let daemons = Arc::new(Daemons::new().with_kaspad(core.clone()).with_cpu_miner(core.clone())); - - log_info!("-> loading settings"); - let settings = Arc::new(SettingsStore::::try_new("terminal")?); - settings.try_load().await?; - let font_size = settings.get::(TerminalSettings::FontSize); - settings.set::(TerminalSettings::Scrollback, 40000).await?; - let scrollback = settings.get::(TerminalSettings::Scrollback); - log_info!("-> terminal cli init"); - let terminal_options = TerminalOptions { font_size, scrollback, ..TerminalOptions::default() }; - let options = KaspaCliOptions::new(terminal_options, Some(daemons)); - let cli = KaspaCli::try_new_arc(options).await?; - - log_info!("-> getting local nw window"); - let window = Arc::new(nw_sys::window::get()); - log_info!("-> init window layout manager"); - let layout = Arc::new(Layout::try_new(&window, &settings).await?); - - log_info!("-> creating terminal application instance"); - let app = Arc::new(Self { - inner: Application::new()?, - ipc: Ipc::try_new_window_binding(&window, Modules::Terminal)?, - core, - cli, - window, - callbacks: CallbackMap::default(), - settings, - layout, - metrics: Arc::new(Mutex::new(None)), - }); - - unsafe { - TERMINAL = Some(app.clone()); - }; - - Ok(app) - } - - fn register_ipc_handlers(self: &Arc) -> Result<()> { - self.ipc.method( - TermOps::TestTerminal, - Method::new(move |args: TestReq| { - Box::pin(async move { - let resp: TestResp = TestResp { resp: args.req + " - response from terminal!" }; - Ok(resp) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - TermOps::FontCtl, - Method::new(move |args: FontCtl| { - let this = this.clone(); - Box::pin(async move { - match args { - FontCtl::IncreaseSize => { - this.cli.term().increase_font_size().map_err(|e| e.to_string())?; - if let Some(font_size) = this.cli.term().get_font_size().unwrap() { - this.settings - .set(TerminalSettings::FontSize, font_size) - .await - .expect("Unable to store application settings"); - } - } - FontCtl::DecreaseSize => { - this.cli.term().decrease_font_size().map_err(|e| e.to_string())?; - if let Some(font_size) = this.cli.term().get_font_size().unwrap() { - this.settings - .set(TerminalSettings::FontSize, font_size) - .await - .expect("Unable to store application settings"); - } - } - } - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.method( - TermOps::EditCtl, - Method::new(move |args: EditCtl| { - let this = this.clone(); - Box::pin(async move { - match args { - EditCtl::Copy => { - this.cli.term().clipboard_copy().map_err(|e| e.to_string())?; - } - EditCtl::Paste => { - this.cli.term().clipboard_paste().map_err(|e| e.to_string())?; - } - } - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.notification( - TermOps::MetricsCtl, - Notification::new(move |args: MetricsSinkCtl| { - let this = this.clone(); - Box::pin(async move { - let metrics = this.cli.handlers().get("metrics").expect("MetricsCtlSink: missing metrics module"); - let metrics = - metrics.downcast_arc::().expect("MetricsCtlSink: invalid metrics module"); - match args { - MetricsSinkCtl::Activate => { - let ipc = get_ipc_target(Modules::Metrics) - .await - .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(); - - 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 => { - this.metrics.lock().unwrap().take(); - metrics.unregister_sink(); - } - } - Ok(()) - }) - }), - ); - - let this = self.clone(); - self.ipc.notification( - TermOps::DaemonEvent, - Notification::new(move |event: DaemonEvent| { - let this = this.clone(); - Box::pin(async move { - this.cli - .handle_daemon_event(event) - .await - .unwrap_or_else(|err| log_error!("error handling child process stdio (cli term relay): `{err}`")); - Ok(()) - }) - }), - ); - - Ok(()) - } - - fn register_cli_handlers(&self) -> Result<()> { - self.cli.register_handlers()?; - - self.cli.handlers().register(&self.cli, crate::modules::metrics::Metrics::new(&self.core)); - - Ok(()) - } - - fn register_window_handlers(self: &Arc) -> Result<()> { - let this = self.clone(); - let close = callback!(move || { - unsafe { - SHUTDOWN_ATTEMPTS += 1; - if SHUTDOWN_ATTEMPTS >= 3 { - nw_sys::app::quit(); - } - } - - let this = this.clone(); - spawn(async move { - this.cli.shutdown().await.unwrap_or_else(|err| log_error!("Error during shutdown: `{err}`")); - }); - }); - - self.window.on("close", close.as_ref()); - self.callbacks.retain(close)?; - - Ok(()) - } - - async fn main(self: &Arc) -> Result<()> { - log_info!("-> register window handlers"); - self.register_window_handlers()?; - log_info!("-> register ipc handlers"); - self.register_ipc_handlers()?; - log_info!("-> register cli handlers"); - self.register_cli_handlers()?; - log_info!("-> register local cli handlers"); - crate::modules::register_cli_handlers(&self.cli)?; - - // cli starts notification->term trace pipe task - log_info!("-> cli start"); - self.cli.start().await?; - - log_info!("-> signal terminal ready"); - self.core.terminal_ready().await?; - - log_info!("-> greeting"); - let kos_current_version = env!("CARGO_PKG_VERSION").to_string(); - let kos_last_version = self.settings.get::(TerminalSettings::Greeting).unwrap_or_default(); - - if kos_last_version != kos_current_version { - let greeting = r" -Hello Kaspian! - -If you have any questions, please join us on discord at https://discord.gg/kaspa - -If you are a first-time user, you can type 'guide' or 'help' to get started. - -Please note, this is an alpha software release of the Kaspa-OS; expect some bugs! - -"; - - self.cli.term().writeln(greeting.crlf()); - self.settings.set(TerminalSettings::Greeting, &kos_current_version).await?; - } - let framework_version = self.cli.version(); - let version = if framework_version == kos_current_version { - kos_current_version - } else { - format!("{} Rust Core v{}", kos_current_version, framework_version) - }; - let banner = format!("Kaspa OS v{} (type 'help' for list of commands)", version); - self.cli.term().writeln(banner); - - log_info!("-> cli run ..."); - // terminal blocks async execution, delivering commands to the cli - self.cli.run().await?; - - log_info!("-> cli stop"); - // stop notification->term trace pipe task - self.cli.stop().await?; - - log_info!("-> core shutdown"); - self.core.shutdown().await?; - - log_info!("-> terminal close"); - self.window.close_impl(true); - - Ok(()) - } -} - -#[wasm_bindgen] -pub async fn init_application() -> Result<()> { - kaspa_core::log::set_log_level(LevelFilter::Info); - workflow_log::set_colors_enabled(true); - - let terminal = Terminal::try_new().await?; - terminal.main().await?; - - Ok(()) -} diff --git a/metrics/core/src/lib.rs b/metrics/core/src/lib.rs index 2b4438d45..4a3ca2a0f 100644 --- a/metrics/core/src/lib.rs +++ b/metrics/core/src/lib.rs @@ -41,7 +41,7 @@ impl Default for Metrics { } impl Metrics { - pub fn set_rpc(&self, rpc: Option>) { + pub fn bind_rpc(&self, rpc: Option>) { *self.rpc.lock().unwrap() = rpc; } @@ -81,23 +81,23 @@ impl Metrics { }, _ = interval.next().fuse() => { - let last_metrics_data = current_metrics_data; - current_metrics_data = MetricsData::new(unixtime_as_millis_f64()); + 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); - } + 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()); + 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(); - } + 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(); } + } } } } diff --git a/notify/src/address/tracker.rs b/notify/src/address/tracker.rs index a3dc4eb01..a627408e4 100644 --- a/notify/src/address/tracker.rs +++ b/notify/src/address/tracker.rs @@ -3,7 +3,7 @@ use indexmap::{map::Entry, IndexMap}; use itertools::Itertools; use kaspa_addresses::{Address, Prefix}; use kaspa_consensus_core::tx::ScriptPublicKey; -use kaspa_core::{debug, info, trace}; +use kaspa_core::{debug, trace}; use kaspa_txscript::{extract_script_pub_key_address, pay_to_address_script}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::{ @@ -276,7 +276,7 @@ impl Inner { Self::MAX_ADDRESS_UPPER_BOUND ); let max_addresses = max_addresses.unwrap_or(Self::DEFAULT_MAX_ADDRESSES); - info!("Memory configuration: UTXO changed events wil be tracked for at most {} addresses", max_addresses); + debug!("Memory configuration: UTXO changed events wil be tracked for at most {} addresses", max_addresses); let script_pub_keys = IndexMap::with_capacity(capacity); debug!("Creating an address tracker with a capacity of {}", script_pub_keys.capacity()); diff --git a/notify/src/broadcaster.rs b/notify/src/broadcaster.rs index fde070ec8..18d2ef55a 100644 --- a/notify/src/broadcaster.rs +++ b/notify/src/broadcaster.rs @@ -69,7 +69,7 @@ where if let Some(ref encoding) = found_encoding { self.0.get_mut(subscription).unwrap().remove(encoding); if self.0.get(subscription).unwrap().is_empty() { - self.0.remove(subscription); + self.0.swap_remove(subscription); } } } diff --git a/notify/src/error.rs b/notify/src/error.rs index d6021a0bc..4ec239fac 100644 --- a/notify/src/error.rs +++ b/notify/src/error.rs @@ -23,6 +23,9 @@ pub enum Error { #[error("event type disabled")] EventTypeDisabled, + #[error("Invalid event type: {0}")] + InvalidEventType(String), + #[error(transparent)] AddressError(#[from] crate::address::error::Error), } diff --git a/notify/src/events.rs b/notify/src/events.rs index 2ccc91483..e27f51bfa 100644 --- a/notify/src/events.rs +++ b/notify/src/events.rs @@ -1,5 +1,8 @@ use super::scope::Scope; +use crate::error::Error; +use serde::{Deserialize, Serialize}; use std::ops::{Index, IndexMut}; +use std::str::FromStr; use workflow_core::enums::usize_try_from; macro_rules! event_type_enum { @@ -32,26 +35,46 @@ macro_rules! event_type_enum { } event_type_enum! { -/// Event type classifying subscriptions (see [`Scope`]) and notifications (see [`Notification`]) -/// -/// Note: This enum is central to the notification system. For supporting a new notification type, it is advised to -/// start by adding a new variant here. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum EventType { - BlockAdded = 0, - VirtualChainChanged, - FinalityConflict, - FinalityConflictResolved, - UtxosChanged, - SinkBlueScoreChanged, - VirtualDaaScoreChanged, - PruningPointUtxoSetOverride, - NewBlockTemplate, -} + /// Event type classifying subscriptions (see [`Scope`]) and notifications (see [`Notification`]) + /// + /// Note: This enum is central to the notification system. For supporting a new notification type, it is advised to + /// start by adding a new variant here. + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] + #[serde(rename_all = "kebab-case")] + pub enum EventType { + BlockAdded = 0, + VirtualChainChanged, + FinalityConflict, + FinalityConflictResolved, + UtxosChanged, + SinkBlueScoreChanged, + VirtualDaaScoreChanged, + PruningPointUtxoSetOverride, + NewBlockTemplate, + } } pub const EVENT_COUNT: usize = 9; +impl FromStr for EventType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "block-added" => Ok(EventType::BlockAdded), + "virtual-chain-changed" => Ok(EventType::VirtualChainChanged), + "finality-conflict" => Ok(EventType::FinalityConflict), + "finality-conflict-resolved" => Ok(EventType::FinalityConflictResolved), + "utxos-changed" => Ok(EventType::UtxosChanged), + "sink-blue-score-changed" => Ok(EventType::SinkBlueScoreChanged), + "virtual-daa-score-changed" => Ok(EventType::VirtualDaaScoreChanged), + "pruning-point-utxo-set-override" => Ok(EventType::PruningPointUtxoSetOverride), + "new-block-template" => Ok(EventType::NewBlockTemplate), + _ => Err(Error::InvalidEventType(s.to_string())), + } + } +} + /// Generic array with [`EventType`] strongly-typed index #[derive(Default, Clone, Copy, Debug)] pub struct EventArray([T; EVENT_COUNT]); diff --git a/protocol/flows/src/flowcontext/orphans.rs b/protocol/flows/src/flowcontext/orphans.rs index efbf18299..75c223caa 100644 --- a/protocol/flows/src/flowcontext/orphans.rs +++ b/protocol/flows/src/flowcontext/orphans.rs @@ -84,7 +84,7 @@ impl OrphanBlocksPool { FindRootsOutput::Roots(roots, orphan_ancestors) => (roots, orphan_ancestors), FindRootsOutput::NoRoots(orphan_ancestors) => { let blocks: Vec<_> = - orphan_ancestors.into_iter().map(|h| self.orphans.remove(&h).expect("orphan ancestor").block).collect(); + orphan_ancestors.into_iter().map(|h| self.orphans.swap_remove(&h).expect("orphan ancestor").block).collect(); return Some(OrphanOutput::NoRoots(consensus.validate_and_insert_block_batch(blocks))); } }; @@ -185,7 +185,7 @@ impl OrphanBlocksPool { consensus: &ConsensusProxy, root: Hash, ) -> (Vec, Vec, Vec) { - let root_entry = self.orphans.remove(&root); // Try removing the root just in case it was previously an orphan + let root_entry = self.orphans.swap_remove(&root); // Try removing the root just in case it was previously an orphan let mut process_queue = ProcessQueue::from(root_entry.map(|e| e.children).unwrap_or_else(|| self.iterate_child_orphans(root).collect())); let mut processing = HashMap::new(); @@ -199,7 +199,7 @@ impl OrphanBlocksPool { } } if processable { - let orphan_block = entry.remove(); + let orphan_block = entry.swap_remove(); let BlockValidationFutures { block_task, virtual_state_task } = consensus.validate_and_insert_block(orphan_block.block.clone()); processing.insert(orphan_hash, (orphan_block.block, block_task, virtual_state_task)); diff --git a/protocol/p2p/src/convert/error.rs b/protocol/p2p/src/convert/error.rs index 15783cb4d..14b2cec1a 100644 --- a/protocol/p2p/src/convert/error.rs +++ b/protocol/p2p/src/convert/error.rs @@ -1,3 +1,4 @@ +use kaspa_consensus_core::subnets::SubnetworkConversionError; use thiserror::Error; #[derive(Clone, Debug, Error)] @@ -20,9 +21,12 @@ pub enum ConversionError { #[error("Integer parsing error: {0}")] IntCastingError(#[from] std::num::TryFromIntError), - #[error("{0}")] + #[error(transparent)] AddressParsingError(#[from] std::net::AddrParseError), - #[error("{0}")] + #[error(transparent)] IdentityError(#[from] uuid::Error), + + #[error(transparent)] + SubnetParsingError(#[from] SubnetworkConversionError), } diff --git a/rothschild/src/main.rs b/rothschild/src/main.rs index f5bfe80ad..bbabca71b 100644 --- a/rothschild/src/main.rs +++ b/rothschild/src/main.rs @@ -17,7 +17,7 @@ use kaspa_rpc_core::{api::rpc::RpcApi, notify::mode::NotificationMode}; use kaspa_txscript::pay_to_address_script; use parking_lot::Mutex; use rayon::prelude::*; -use secp256k1::{rand::thread_rng, KeyPair}; +use secp256k1::{rand::thread_rng, Keypair}; use tokio::time::{interval, MissedTickBehavior}; const DEFAULT_SEND_AMOUNT: u64 = 10 * SOMPI_PER_KASPA; @@ -135,7 +135,7 @@ async fn main() { let schnorr_key = if let Some(private_key_hex) = args.private_key { let mut private_key_bytes = [0u8; 32]; faster_hex::hex_decode(private_key_hex.as_bytes(), &mut private_key_bytes).unwrap(); - secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, &private_key_bytes).unwrap() + secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &private_key_bytes).unwrap() } else { let (sk, pk) = &secp256k1::generate_keypair(&mut thread_rng()); let kaspa_addr = Address::new(ADDRESS_PREFIX, ADDRESS_VERSION, &pk.x_only_public_key().0.serialize()); @@ -365,7 +365,7 @@ async fn maybe_send_tx( kaspa_addr: Address, utxos: &mut [(TransactionOutpoint, UtxoEntry)], pending: &mut HashMap, - schnorr_key: KeyPair, + schnorr_key: Keypair, stats: Arc>, maximize_inputs: bool, next_available_utxo_index: &mut usize, @@ -446,7 +446,7 @@ fn estimated_mass(num_utxos: usize, num_outs: u64) -> u64 { } fn generate_tx( - schnorr_key: KeyPair, + schnorr_key: Keypair, utxos: &[(TransactionOutpoint, UtxoEntry)], send_amount: u64, num_outs: u64, diff --git a/rpc/core/Cargo.toml b/rpc/core/Cargo.toml index 76fb13a73..cfa4895f2 100644 --- a/rpc/core/Cargo.toml +++ b/rpc/core/Cargo.toml @@ -9,9 +9,16 @@ include.workspace = true license.workspace = true repository.workspace = true +[features] +wasm32-sdk = [ + "kaspa-consensus-client/wasm32-sdk", + "kaspa-consensus-wasm/wasm32-sdk" +] + [dependencies] kaspa-addresses.workspace = true kaspa-consensus-core.workspace = true +kaspa-consensus-client.workspace = true kaspa-consensus-notify.workspace = true kaspa-consensus-wasm.workspace = true kaspa-core.workspace = true @@ -22,13 +29,17 @@ kaspa-mining-errors.workspace = true kaspa-notify.workspace = true kaspa-txscript.workspace = true kaspa-utils.workspace = true +kaspa-rpc-macros.workspace = true async-channel.workspace = true async-trait.workspace = true borsh.workspace = true +cfg-if.workspace = true derive_more.workspace = true downcast.workspace = true faster-hex.workspace = true +hex.workspace = true +js-sys.workspace = true log.workspace = true paste.workspace = true serde-wasm-bindgen.workspace = true diff --git a/rpc/core/src/api/ctl.rs b/rpc/core/src/api/ctl.rs index 0cef8ecf4..49241e7d9 100644 --- a/rpc/core/src/api/ctl.rs +++ b/rpc/core/src/api/ctl.rs @@ -6,10 +6,10 @@ use workflow_core::channel::Multiplexer; #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum RpcState { /// RpcApi channel open (connected) - Opened, + Connected, /// RpcApi channel close (disconnected) #[default] - Closed, + Disconnected, } #[derive(Default)] @@ -36,8 +36,12 @@ impl RpcCtl { Self { inner: Arc::new(Inner::default()) } } - pub fn with_descriptor(descriptor: Str) -> Self { - Self { inner: Arc::new(Inner { descriptor: Mutex::new(Some(descriptor.to_string())), ..Inner::default() }) } + pub fn with_descriptor(descriptor: Option) -> Self { + if let Some(descriptor) = descriptor { + Self { inner: Arc::new(Inner { descriptor: Mutex::new(Some(descriptor.to_string())), ..Inner::default() }) } + } else { + Self::default() + } } /// Obtain internal multiplexer (MPMC channel for [`RpcState`] operations) @@ -46,7 +50,7 @@ impl RpcCtl { } pub fn is_connected(&self) -> bool { - *self.inner.state.lock().unwrap() == RpcState::Opened + *self.inner.state.lock().unwrap() == RpcState::Connected } pub fn state(&self) -> RpcState { @@ -55,26 +59,26 @@ impl RpcCtl { /// Signal open to all listeners (async) pub async fn signal_open(&self) -> RpcResult<()> { - *self.inner.state.lock().unwrap() = RpcState::Opened; - Ok(self.inner.multiplexer.broadcast(RpcState::Opened).await?) + *self.inner.state.lock().unwrap() = RpcState::Connected; + Ok(self.inner.multiplexer.broadcast(RpcState::Connected).await?) } /// Signal close to all listeners (async) pub async fn signal_close(&self) -> RpcResult<()> { - *self.inner.state.lock().unwrap() = RpcState::Closed; - Ok(self.inner.multiplexer.broadcast(RpcState::Closed).await?) + *self.inner.state.lock().unwrap() = RpcState::Disconnected; + Ok(self.inner.multiplexer.broadcast(RpcState::Disconnected).await?) } /// Try signal open to all listeners (sync) pub fn try_signal_open(&self) -> RpcResult<()> { - *self.inner.state.lock().unwrap() = RpcState::Opened; - Ok(self.inner.multiplexer.try_broadcast(RpcState::Opened)?) + *self.inner.state.lock().unwrap() = RpcState::Connected; + Ok(self.inner.multiplexer.try_broadcast(RpcState::Connected)?) } /// Try signal close to all listeners (sync) pub fn try_signal_close(&self) -> RpcResult<()> { - *self.inner.state.lock().unwrap() = RpcState::Closed; - Ok(self.inner.multiplexer.try_broadcast(RpcState::Closed)?) + *self.inner.state.lock().unwrap() = RpcState::Disconnected; + Ok(self.inner.multiplexer.try_broadcast(RpcState::Disconnected)?) } /// Set the connection descriptor (URL, peer address, etc.) diff --git a/rpc/core/src/api/notifications.rs b/rpc/core/src/api/notifications.rs index 977313d14..6449f25c0 100644 --- a/rpc/core/src/api/notifications.rs +++ b/rpc/core/src/api/notifications.rs @@ -11,9 +11,9 @@ use kaspa_notify::{ }, }; use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::to_value; use std::sync::Arc; use wasm_bindgen::JsValue; +use workflow_wasm::serde::to_value; full_featured! { #[derive(Clone, Debug, Display, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] diff --git a/rpc/core/src/error.rs b/rpc/core/src/error.rs index 21525db63..59f6b910e 100644 --- a/rpc/core/src/error.rs +++ b/rpc/core/src/error.rs @@ -1,4 +1,4 @@ -use kaspa_consensus_core::tx::TransactionId; +use kaspa_consensus_core::{subnets::SubnetworkConversionError, tx::TransactionId}; use kaspa_utils::networking::IpAddress; use std::{net::AddrParseError, num::TryFromIntError}; use thiserror::Error; @@ -115,6 +115,18 @@ pub enum RpcError { #[error("transaction query must either not filter transactions or include orphans")] InconsistentMempoolTxQuery, + + #[error(transparent)] + SubnetParsingError(#[from] SubnetworkConversionError), + + #[error(transparent)] + WasmError(#[from] workflow_wasm::error::Error), + + #[error("{0}")] + SerdeWasmBindgen(String), + + #[error(transparent)] + ConsensusClient(#[from] kaspa_consensus_client::error::Error), } impl From for RpcError { @@ -135,4 +147,10 @@ impl From> for RpcError { } } +impl From for RpcError { + fn from(value: serde_wasm_bindgen::Error) -> Self { + RpcError::SerdeWasmBindgen(value.to_string()) + } +} + pub type RpcResult = std::result::Result; diff --git a/rpc/core/src/model/block.rs b/rpc/core/src/model/block.rs index ca9af18d6..c4c501afb 100644 --- a/rpc/core/src/model/block.rs +++ b/rpc/core/src/model/block.rs @@ -1,8 +1,7 @@ +use crate::prelude::{RpcHash, RpcHeader, RpcTransaction}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use crate::prelude::{RpcHash, RpcHeader, RpcTransaction}; - #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct RpcBlock { @@ -25,3 +24,41 @@ pub struct RpcBlockVerboseData { pub merge_set_reds_hashes: Vec, pub is_chain_block: bool, } + +cfg_if::cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(typescript_custom_section)] + const TS_BLOCK: &'static str = r#" + /** + * Interface defining the structure of a block. + * + * @category Consensus + */ + export interface IBlock { + header: IHeader; + transactions: ITransaction[]; + verboseData?: IBlockVerboseData; + } + + /** + * Interface defining the structure of a block verbose data. + * + * @category Node RPC + */ + export interface IBlockVerboseData { + hash: HexString; + difficulty: number; + selectedParentHash: HexString; + transactionIds: HexString[]; + isHeaderOnly: boolean; + blueScore: number; + childrenHashes: HexString[]; + mergeSetBluesHashes: HexString[]; + mergeSetRedsHashes: HexString[]; + isChainBlock: boolean; + } + "#; + } +} diff --git a/rpc/core/src/model/mempool.rs b/rpc/core/src/model/mempool.rs index d53973dd9..bd08b745a 100644 --- a/rpc/core/src/model/mempool.rs +++ b/rpc/core/src/model/mempool.rs @@ -28,3 +28,23 @@ impl RpcMempoolEntryByAddress { Self { address, sending, receiving } } } + +cfg_if::cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(typescript_custom_section)] + const TS_MEMPOOL_ENTRY: &'static str = r#" + /** + * Mempool entry. + * + * @category Node RPC + */ + export interface IMempoolEntry { + fee : bigint; + transaction : ITransaction; + isOrphan : boolean; + } + "#; + } +} diff --git a/rpc/core/src/model/message.rs b/rpc/core/src/model/message.rs index 8538558e5..7366bf3cc 100644 --- a/rpc/core/src/model/message.rs +++ b/rpc/core/src/model/message.rs @@ -52,7 +52,8 @@ impl Display for SubmitBlockRejectReason { } #[derive(Eq, PartialEq, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "lowercase")] +#[serde(tag = "type", content = "reason")] pub enum SubmitBlockReport { Success, Reject(SubmitBlockRejectReason), @@ -78,6 +79,7 @@ pub struct SubmitBlockResponse { pub struct GetBlockTemplateRequest { /// Which kaspa address should the coinbase block reward transaction pay into pub pay_address: RpcAddress, + // TODO: replace with hex serialization pub extra_data: RpcExtraData, } impl GetBlockTemplateRequest { diff --git a/rpc/core/src/model/tx.rs b/rpc/core/src/model/tx.rs index 1b5585d3f..bb13f797d 100644 --- a/rpc/core/src/model/tx.rs +++ b/rpc/core/src/model/tx.rs @@ -22,6 +22,7 @@ pub type RpcTransactionOutpoint = TransactionOutpoint; #[serde(rename_all = "camelCase")] pub struct RpcTransactionInput { pub previous_outpoint: RpcTransactionOutpoint, + #[serde(with = "hex::serde")] pub signature_script: Vec, pub sequence: u64, pub sig_op_count: u8, @@ -90,6 +91,7 @@ pub struct RpcTransaction { pub lock_time: u64, pub subnetwork_id: RpcSubnetworkId, pub gas: u64, + #[serde(with = "hex::serde")] pub payload: Vec, pub mass: u64, pub verbose_data: Option, diff --git a/rpc/core/src/wasm/convert.rs b/rpc/core/src/wasm/convert.rs new file mode 100644 index 000000000..0c33cf0ec --- /dev/null +++ b/rpc/core/src/wasm/convert.rs @@ -0,0 +1,77 @@ +use crate::model::*; +use kaspa_consensus_client::*; +use kaspa_consensus_core::tx as cctx; +use std::sync::Arc; + +impl From for UtxoEntry { + fn from(entry: RpcUtxosByAddressesEntry) -> UtxoEntry { + let RpcUtxosByAddressesEntry { address, outpoint, utxo_entry } = entry; + let cctx::UtxoEntry { amount, script_public_key, block_daa_score, is_coinbase } = utxo_entry; + UtxoEntry { address, outpoint: outpoint.into(), amount, script_public_key, block_daa_score, is_coinbase } + } +} + +impl From for UtxoEntryReference { + fn from(entry: RpcUtxosByAddressesEntry) -> Self { + Self { utxo: Arc::new(entry.into()) } + } +} + +impl From<&RpcUtxosByAddressesEntry> for UtxoEntryReference { + fn from(entry: &RpcUtxosByAddressesEntry) -> Self { + Self { utxo: Arc::new(entry.clone().into()) } + } +} + +cfg_if::cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + + impl From for RpcTransactionInput { + fn from(tx_input: TransactionInput) -> Self { + let inner = tx_input.inner(); + RpcTransactionInput { + previous_outpoint: inner.previous_outpoint.clone().into(), + signature_script: inner.signature_script.clone(), + sequence: inner.sequence, + sig_op_count: inner.sig_op_count, + verbose_data: None, + } + } + } + + impl From for RpcTransactionOutput { + fn from(output: TransactionOutput) -> Self { + let inner = output.inner(); + RpcTransactionOutput { value: inner.value, script_public_key: inner.script_public_key.clone(), verbose_data: None } + } + } + + impl From for RpcTransaction { + fn from(tx: Transaction) -> Self { + RpcTransaction::from(&tx) + } + } + + impl From<&Transaction> for RpcTransaction { + fn from(tx: &Transaction) -> Self { + let inner = tx.inner(); + let inputs: Vec = + inner.inputs.clone().into_iter().map(|input| input.into()).collect::>(); + let outputs: Vec = + inner.outputs.clone().into_iter().map(|output| output.into()).collect::>(); + + RpcTransaction { + version: inner.version, + inputs, + outputs, + lock_time: inner.lock_time, + subnetwork_id: inner.subnetwork_id.clone(), + gas: inner.gas, + payload: inner.payload.clone(), + mass: 0, // TODO: apply mass to all external APIs including wasm + verbose_data: None, + } + } + } + } +} diff --git a/rpc/core/src/wasm/message.rs b/rpc/core/src/wasm/message.rs new file mode 100644 index 000000000..56af183db --- /dev/null +++ b/rpc/core/src/wasm/message.rs @@ -0,0 +1,1385 @@ +#![allow(non_snake_case)] + +use crate::error::RpcError as Error; +use crate::error::RpcResult as Result; +use crate::model::*; +use kaspa_addresses::Address; +use kaspa_addresses::AddressOrStringArrayT; +use kaspa_consensus_client::Transaction; +use kaspa_consensus_client::UtxoEntryReference; +use kaspa_rpc_macros::declare_typescript_wasm_interface as declare; +pub use serde_wasm_bindgen::from_value; +use wasm_bindgen::prelude::*; +use workflow_wasm::convert::*; +use workflow_wasm::extensions::*; +use workflow_wasm::serde::to_value; + +macro_rules! try_from { + ($name:ident : $from_type:ty, $to_type:ty, $body:block) => { + impl TryFrom<$from_type> for $to_type { + type Error = Error; + fn try_from($name: $from_type) -> Result { + $body + } + } + }; +} + +// --- + +#[wasm_bindgen(typescript_custom_section)] +const TS_ACCEPTED_TRANSACTION_IDS: &'static str = r#" + /** + * Accepted transaction IDs. + * + * @category Node RPC + */ + export interface IAcceptedTransactionIds { + acceptingBlockHash : HexString; + acceptedTransactionIds : HexString[]; + } +"#; + +// --- + +declare! { + IPingRequest, + r#" + /** + * @category Node RPC + */ + export interface IPingRequest { + message?: string; + } + "#, +} + +try_from! ( args: IPingRequest, PingRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IPingResponse, + r#" + /** + * @category Node RPC + */ + export interface IPingResponse { + message?: string; + } + "#, +} + +try_from! ( args: PingResponse, IPingResponse, { + Ok(to_value(&args)?.into()) +}); + +declare! { + IGetBlockCountRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetBlockCountRequest { } + "#, +} + +try_from! ( args: IGetBlockCountRequest, GetBlockCountRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetBlockCountResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetBlockCountResponse { + headerCount : bigint; + blockCount : bigint; + } + "#, +} + +try_from! ( args: GetBlockCountResponse, IGetBlockCountResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetBlockDagInfoRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetBlockDagInfoRequest { } + "#, +} + +try_from! ( args: IGetBlockDagInfoRequest, GetBlockDagInfoRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetBlockDagInfoResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetBlockDagInfoResponse { + network: string; + blockCount: bigint; + headerCount: bigint; + tipHashes: HexString[]; + difficulty: number; + pastMedianTime: bigint; + virtualParentHashes: HexString[]; + pruningPointHash: HexString; + virtualDaaScore: bigint; + sink: HexString; + } + "#, +} + +try_from! ( args: GetBlockDagInfoResponse, IGetBlockDagInfoResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetCoinSupplyRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetCoinSupplyRequest { } + "#, +} + +try_from! ( args: IGetCoinSupplyRequest, GetCoinSupplyRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetCoinSupplyResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetCoinSupplyResponse { + maxSompi: bigint; + circulatingSompi: bigint; + } + "#, +} + +try_from! ( args: GetCoinSupplyResponse, IGetCoinSupplyResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetConnectedPeerInfoRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetConnectedPeerInfoRequest { } + "#, +} + +try_from! ( args: IGetConnectedPeerInfoRequest, GetConnectedPeerInfoRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetConnectedPeerInfoResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetConnectedPeerInfoResponse { + [key: string]: any + } + "#, +} + +try_from! ( args: GetConnectedPeerInfoResponse, IGetConnectedPeerInfoResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetInfoRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetInfoRequest { } + "#, +} + +try_from! ( args: IGetInfoRequest, GetInfoRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetInfoResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetInfoResponse { + p2pId : string; + mempoolSize : bigint; + serverVersion : string; + isUtxoIndexed : boolean; + isSynced : boolean; + /** GRPC ONLY */ + hasNotifyCommand : boolean; + /** GRPC ONLY */ + hasMessageId : boolean; + } + "#, +} + +try_from! ( args: GetInfoResponse, IGetInfoResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetPeerAddressesRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetPeerAddressesRequest { } + "#, +} + +try_from! ( args: IGetPeerAddressesRequest, GetPeerAddressesRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetPeerAddressesResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetPeerAddressesResponse { + [key: string]: any + } + "#, +} + +try_from! ( args: GetPeerAddressesResponse, IGetPeerAddressesResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetMetricsRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetMetricsRequest { } + "#, +} + +try_from! ( args: IGetMetricsRequest, GetMetricsRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetMetricsResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetMetricsResponse { + [key: string]: any + } + "#, +} + +try_from! ( args: GetMetricsResponse, IGetMetricsResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetSinkRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetSinkRequest { } + "#, +} + +try_from! ( args: IGetSinkRequest, GetSinkRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetSinkResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetSinkResponse { + sink : HexString; + } + "#, +} + +try_from! ( args: GetSinkResponse, IGetSinkResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetSinkBlueScoreRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetSinkBlueScoreRequest { } + "#, +} + +try_from! ( args: IGetSinkBlueScoreRequest, GetSinkBlueScoreRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetSinkBlueScoreResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetSinkBlueScoreResponse { + blueScore : bigint; + } + "#, +} + +try_from! ( args: GetSinkBlueScoreResponse, IGetSinkBlueScoreResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IShutdownRequest, + r#" + /** + * @category Node RPC + */ + export interface IShutdownRequest { } + "#, +} + +try_from! ( args: IShutdownRequest, ShutdownRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IShutdownResponse, + r#" + /** + * @category Node RPC + */ + export interface IShutdownResponse { } + "#, +} + +try_from! ( args: ShutdownResponse, IShutdownResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetServerInfoRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetServerInfoRequest { } + "#, +} + +try_from! ( args: IGetServerInfoRequest, GetServerInfoRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetServerInfoResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetServerInfoResponse { + rpcApiVersion : number[]; + serverVersion : string; + networkId : string; + hasUtxoIndex : boolean; + isSynced : boolean; + virtualDaaScore : bigint; + } + "#, +} + +try_from! ( args: GetServerInfoResponse, IGetServerInfoResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetSyncStatusRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetSyncStatusRequest { } + "#, +} + +try_from! ( args: IGetSyncStatusRequest, GetSyncStatusRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetSyncStatusResponse, + r#" + /** + * @category Node RPC + */ + export interface IGetSyncStatusResponse { + isSynced : boolean; + } + "#, +} + +try_from! ( args: GetSyncStatusResponse, IGetSyncStatusResponse, { + Ok(to_value(&args)?.into()) +}); + +/* + Interfaces for methods with arguments +*/ + +declare! { + IAddPeerRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IAddPeerRequest { + peerAddress : INetworkAddress; + isPermanent : boolean; + } + "#, +} + +try_from! ( args: IAddPeerRequest, AddPeerRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IAddPeerResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IAddPeerResponse { } + "#, +} + +try_from! ( args: AddPeerResponse, IAddPeerResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- +declare! { + IBanRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IBanRequest { + /** + * IPv4 or IPv6 address to ban. + */ + ip : string; + } + "#, +} + +try_from! ( args: IBanRequest, BanRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IBanResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IBanResponse { } + "#, +} + +try_from! ( args: BanResponse, IBanResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IEstimateNetworkHashesPerSecondRequest, + r#" + /** + * @category Node RPC + */ + export interface IEstimateNetworkHashesPerSecondRequest { + windowSize : number; + startHash? : HexString; + } + "#, +} + +try_from! ( args: IEstimateNetworkHashesPerSecondRequest, EstimateNetworkHashesPerSecondRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IEstimateNetworkHashesPerSecondResponse, + r#" + /** + * @category Node RPC + */ + export interface IEstimateNetworkHashesPerSecondResponse { + networkHashesPerSecond : bigint; + } + "#, +} + +try_from! ( args: EstimateNetworkHashesPerSecondResponse, IEstimateNetworkHashesPerSecondResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetBalanceByAddressRequest, + r#" + /** + * @category Node RPC + */ + export interface IGetBalanceByAddressRequest { + address : Address | string; + } + "#, +} + +try_from! ( args: IGetBalanceByAddressRequest, GetBalanceByAddressRequest, { + let js_value = JsValue::from(args); + let request = if let Ok(address) = Address::try_owned_from(js_value.clone()) { + GetBalanceByAddressRequest { address } + } else { + // TODO - evaluate Object property + from_value::(js_value)? + }; + Ok(request) +}); + +declare! { + IGetBalanceByAddressResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBalanceByAddressResponse { + balance : bigint; + } + "#, +} + +try_from! ( args: GetBalanceByAddressResponse, IGetBalanceByAddressResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetBalancesByAddressesRequest, + "IGetBalancesByAddressesRequest | Address[] | string[]", + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBalancesByAddressesRequest { + addresses : Address[] | string[]; + } + "#, +} + +try_from! ( args: IGetBalancesByAddressesRequest, GetBalancesByAddressesRequest, { + let js_value = JsValue::from(args); + let request = if let Ok(addresses) = Vec::

::try_from(AddressOrStringArrayT::from(js_value.clone())) { + GetBalancesByAddressesRequest { addresses } + } else { + from_value::(js_value)? + }; + Ok(request) +}); + +declare! { + IGetBalancesByAddressesResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IBalancesByAddressesEntry { + address : Address; + balance : bigint; + } + /** + * + * + * @category Node RPC + */ + export interface IGetBalancesByAddressesResponse { + entries : IBalancesByAddressesEntry[]; + } + "#, +} + +try_from! ( args: GetBalancesByAddressesResponse, IGetBalancesByAddressesResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetBlockRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBlockRequest { + hash : HexString; + includeTransactions : boolean; + } + "#, +} + +try_from! ( args: IGetBlockRequest, GetBlockRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetBlockResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBlockResponse { + block : IBlock; + } + "#, +} + +try_from! ( args: GetBlockResponse, IGetBlockResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetBlocksRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBlocksRequest { + lowHash? : HexString; + includeBlocks : boolean; + includeTransactions : boolean; + } + "#, +} + +try_from! ( args: IGetBlocksRequest, GetBlocksRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetBlocksResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBlocksResponse { + blockHashes : HexString[]; + blocks : IBlock[]; + } + "#, +} + +try_from! ( args: GetBlocksResponse, IGetBlocksResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetBlockTemplateRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBlockTemplateRequest { + payAddress : Address | string; + /** + * `extraData` can contain a user-supplied plain text or a byte array represented by `Uint8array`. + */ + extraData? : string | Uint8Array; + } + "#, +} + +try_from! ( args: IGetBlockTemplateRequest, GetBlockTemplateRequest, { + let pay_address = args.get_cast::
("payAddress")?.into_owned(); + let extra_data = if let Some(extra_data) = args.try_get_value("extraData")? { + if let Some(text) = extra_data.as_string() { + text.into_bytes() + } else { + extra_data.try_as_vec_u8()? + } + } else { + Default::default() + }; + Ok(GetBlockTemplateRequest { + pay_address, + extra_data, + }) +}); + +declare! { + IGetBlockTemplateResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetBlockTemplateResponse { + block : IBlock; + } + "#, +} + +try_from! ( args: GetBlockTemplateResponse, IGetBlockTemplateResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetDaaScoreTimestampEstimateRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetDaaScoreTimestampEstimateRequest { + daaScores : bigint[]; + } + "#, +} + +try_from! ( args: IGetDaaScoreTimestampEstimateRequest, GetDaaScoreTimestampEstimateRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetDaaScoreTimestampEstimateResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetDaaScoreTimestampEstimateResponse { + timestamps : bigint[]; + } + "#, +} + +try_from! ( args: GetDaaScoreTimestampEstimateResponse, IGetDaaScoreTimestampEstimateResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetCurrentNetworkRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetCurrentNetworkRequest { } + "#, +} + +try_from! ( args: IGetCurrentNetworkRequest, GetCurrentNetworkRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetCurrentNetworkResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetCurrentNetworkResponse { + network : string; + } + "#, +} + +try_from! ( args: GetCurrentNetworkResponse, IGetCurrentNetworkResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetHeadersRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetHeadersRequest { + startHash : HexString; + limit : bigint; + isAscending : boolean; + } + "#, +} + +try_from! ( args: IGetHeadersRequest, GetHeadersRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetHeadersResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetHeadersResponse { + headers : IHeader[]; + } + "#, +} + +try_from! ( args: GetHeadersResponse, IGetHeadersResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetMempoolEntriesRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetMempoolEntriesRequest { + includeOrphanPool? : boolean; + filterTransactionPool? : boolean; + } + "#, +} + +try_from! ( args: IGetMempoolEntriesRequest, GetMempoolEntriesRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetMempoolEntriesResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetMempoolEntriesResponse { + mempoolEntries : IMempoolEntry[]; + } + "#, +} + +try_from! ( args: GetMempoolEntriesResponse, IGetMempoolEntriesResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetMempoolEntriesByAddressesRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetMempoolEntriesByAddressesRequest { + addresses : Address[] | string[]; + includeOrphanPool? : boolean; + filterTransactionPool? : boolean; + } + "#, +} + +try_from! ( args: IGetMempoolEntriesByAddressesRequest, GetMempoolEntriesByAddressesRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetMempoolEntriesByAddressesResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetMempoolEntriesByAddressesResponse { + entries : IMempoolEntry[]; + } + "#, +} + +try_from! ( args: GetMempoolEntriesByAddressesResponse, IGetMempoolEntriesByAddressesResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetMempoolEntryRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetMempoolEntryRequest { + transactionId : HexString; + includeOrphanPool? : boolean; + filterTransactionPool? : boolean; + } + "#, +} + +try_from! ( args: IGetMempoolEntryRequest, GetMempoolEntryRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetMempoolEntryResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetMempoolEntryResponse { + mempoolEntry : IMempoolEntry; + } + "#, +} + +try_from! ( args: GetMempoolEntryResponse, IGetMempoolEntryResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetSubnetworkRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetSubnetworkRequest { + subnetworkId : HexString; + } + "#, +} + +try_from! ( args: IGetSubnetworkRequest, GetSubnetworkRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetSubnetworkResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetSubnetworkResponse { + gasLimit : bigint; + } + "#, +} + +try_from! ( args: GetSubnetworkResponse, IGetSubnetworkResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IGetUtxosByAddressesRequest, + "IGetUtxosByAddressesRequest | Address[] | string[]", + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetUtxosByAddressesRequest { + addresses : Address[] | string[] + } + "#, +} + +try_from! ( args: IGetUtxosByAddressesRequest, GetUtxosByAddressesRequest, { + let js_value = JsValue::from(args); + let request = if let Ok(addresses) = Vec::
::try_from(AddressOrStringArrayT::from(js_value.clone())) { + GetUtxosByAddressesRequest { addresses } + } else { + from_value::(js_value)? + }; + Ok(request) +}); + +declare! { + IGetUtxosByAddressesResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetUtxosByAddressesResponse { + entries : IUtxoEntry[]; + } + "#, +} + +try_from! ( args: GetUtxosByAddressesResponse, IGetUtxosByAddressesResponse, { + let GetUtxosByAddressesResponse { entries } = args; + let entries = entries.into_iter().map(UtxoEntryReference::from).collect::>(); + let entries = js_sys::Array::from_iter(entries.into_iter().map(JsValue::from)); + let response = IGetUtxosByAddressesResponse::default(); + response.set("entries", entries.as_ref())?; + Ok(response) +}); + +// --- + +declare! { + IGetVirtualChainFromBlockRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetVirtualChainFromBlockRequest { + startHash : HexString; + includeAcceptedTransactionIds: boolean; + } + "#, +} + +try_from! ( args: IGetVirtualChainFromBlockRequest, GetVirtualChainFromBlockRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IGetVirtualChainFromBlockResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IGetVirtualChainFromBlockResponse { + removedChainBlockHashes : HexString[]; + addedChainBlockHashes : HexString[]; + acceptedTransactionIds : IAcceptedTransactionIds[]; + } + "#, +} + +try_from! ( args: GetVirtualChainFromBlockResponse, IGetVirtualChainFromBlockResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IResolveFinalityConflictRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IResolveFinalityConflictRequest { + finalityBlockHash: HexString; + } + "#, +} + +try_from! ( args: IResolveFinalityConflictRequest, ResolveFinalityConflictRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IResolveFinalityConflictResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IResolveFinalityConflictResponse { } + "#, +} + +try_from! ( args: ResolveFinalityConflictResponse, IResolveFinalityConflictResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + ISubmitBlockRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface ISubmitBlockRequest { + block : IBlock; + allowNonDAABlocks: boolean; + } + "#, +} + +try_from! ( args: ISubmitBlockRequest, SubmitBlockRequest, { + Ok(from_value(args.into())?) +}); + +#[wasm_bindgen(typescript_custom_section)] +const TS_SUBMIT_BLOCK_REPORT: &'static str = r#" + /** + * + * @category Node RPC + */ + export enum SubmitBlockRejectReason { + /** + * The block is invalid. + */ + BlockInvalid = "BlockInvalid", + /** + * The node is not synced. + */ + IsInIBD = "IsInIBD", + /** + * Route is full. + */ + RouteIsFull = "RouteIsFull", + } + + /** + * + * @category Node RPC + */ + export interface ISubmitBlockReport { + type : "success" | "reject"; + reason? : SubmitBlockRejectReason; + } +"#; + +declare! { + ISubmitBlockResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface ISubmitBlockResponse { + report : ISubmitBlockReport; + } + "#, +} + +try_from! ( args: SubmitBlockResponse, ISubmitBlockResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + ISubmitTransactionRequest, + // "ISubmitTransactionRequest | Transaction", + r#" + /** + * Submit transaction to the node. + * + * @category Node RPC + */ + export interface ISubmitTransactionRequest { + transaction : Transaction, + allowOrphan? : boolean + } + "#, +} + +try_from! ( args: ISubmitTransactionRequest, SubmitTransactionRequest, { + let (transaction, allow_orphan) = if let Some(transaction) = args.try_get_value("transaction")? { + let allow_orphan = args.try_get_bool("allowOrphan")?.unwrap_or(false); + (transaction, allow_orphan) + } else { + (args.into(), false) + }; + + let request = if let Ok(transaction) = Transaction::try_owned_from(&transaction) { + SubmitTransactionRequest { + transaction : transaction.into(), + allow_orphan, + } + } else { + from_value(transaction)? + }; + Ok(request) +}); + +declare! { + ISubmitTransactionResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface ISubmitTransactionResponse { + transactionId : HexString; + } + "#, +} + +try_from! ( args: SubmitTransactionResponse, ISubmitTransactionResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IUnbanRequest, + r#" + /** + * + * + * @category Node RPC + */ + export interface IUnbanRequest { + /** + * IPv4 or IPv6 address to unban. + */ + ip : string; + } + "#, +} + +try_from! ( args: IUnbanRequest, UnbanRequest, { + Ok(from_value(args.into())?) +}); + +declare! { + IUnbanResponse, + r#" + /** + * + * + * @category Node RPC + */ + export interface IUnbanResponse { } + "#, +} + +try_from! ( args: UnbanResponse, IUnbanResponse, { + Ok(to_value(&args)?.into()) +}); diff --git a/rpc/core/src/wasm/mod.rs b/rpc/core/src/wasm/mod.rs index 0c1d6fadf..6552baa42 100644 --- a/rpc/core/src/wasm/mod.rs +++ b/rpc/core/src/wasm/mod.rs @@ -1,82 +1,7 @@ -use crate::model::*; -use kaspa_consensus_core::tx; -use kaspa_consensus_wasm::*; -use std::sync::Arc; +pub mod convert; -impl From for UtxoEntry { - fn from(entry: RpcUtxosByAddressesEntry) -> UtxoEntry { - UtxoEntry { address: entry.address, outpoint: entry.outpoint.into(), entry: entry.utxo_entry } - } -} - -impl From for UtxoEntryReference { - fn from(entry: RpcUtxosByAddressesEntry) -> Self { - Self { utxo: Arc::new(entry.into()) } - } -} - -impl From for RpcTransactionInput { - fn from(tx_input: TransactionInput) -> Self { - let inner = tx_input.inner(); - RpcTransactionInput { - previous_outpoint: inner.previous_outpoint.clone().into(), - signature_script: inner.signature_script.clone(), - sequence: inner.sequence, - sig_op_count: inner.sig_op_count, - verbose_data: None, - } - } -} - -impl From for RpcTransactionOutput { - fn from(output: TransactionOutput) -> Self { - let inner = output.inner(); - RpcTransactionOutput { value: inner.value, script_public_key: inner.script_public_key.clone(), verbose_data: None } - } -} - -impl From for RpcTransaction { - fn from(tx: Transaction) -> Self { - RpcTransaction::from(&tx) - } -} - -impl From<&Transaction> for RpcTransaction { - fn from(tx: &Transaction) -> Self { - let inner = tx.inner(); - let inputs: Vec = - inner.inputs.clone().into_iter().map(|input| input.into()).collect::>(); - let outputs: Vec = - inner.outputs.clone().into_iter().map(|output| output.into()).collect::>(); - - RpcTransaction { - version: inner.version, - inputs, - outputs, - lock_time: inner.lock_time, - subnetwork_id: inner.subnetwork_id.clone(), - gas: inner.gas, - payload: inner.payload.clone(), - mass: 0, // TODO: apply mass to all external APIs including wasm - verbose_data: None, - } - } -} - -impl From for RpcTransaction { - fn from(mtx: SignableTransaction) -> Self { - let tx = tx::SignableTransaction::from(mtx).tx; - - RpcTransaction { - version: tx.version, - inputs: RpcTransactionInput::from_transaction_inputs(tx.inputs), - outputs: RpcTransactionOutput::from_transaction_outputs(tx.outputs), - lock_time: tx.lock_time, - subnetwork_id: tx.subnetwork_id, - gas: tx.gas, - payload: tx.payload, - mass: 0, // TODO: apply mass to all external APIs including wasm - verbose_data: None, - } +cfg_if::cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + pub mod message; } } diff --git a/rpc/macros/src/handler.rs b/rpc/macros/src/handler.rs index 4731a6791..728ce82f0 100644 --- a/rpc/macros/src/handler.rs +++ b/rpc/macros/src/handler.rs @@ -1,8 +1,9 @@ use convert_case::{Case, Casing}; use proc_macro2::{Ident, Span}; use quote::ToTokens; -use syn::{Error, Expr, ExprArray, Result}; +use syn::{Attribute, Error, Expr, ExprArray, Result}; +#[derive(Debug)] pub struct Handler { pub name: String, pub fn_call: Ident, @@ -11,11 +12,16 @@ pub struct Handler { pub fn_camel: Ident, pub request_type: Ident, pub response_type: Ident, + pub typename: Ident, + pub ts_request_type: Ident, + pub ts_response_type: Ident, + pub ts_custom_section_ident: Ident, // gPRC fields pub is_subscription: bool, pub response_message_type: Ident, pub fallback_request_type: Ident, + pub docs: Vec, } impl Handler { @@ -24,13 +30,22 @@ impl Handler { } pub fn new_with_args(handler: &Expr, fn_suffix: Option<&str>) -> Handler { - let name = handler.to_token_stream().to_string(); + let (name, docs) = match handler { + syn::Expr::Path(expr_path) => (expr_path.path.to_token_stream().to_string(), expr_path.attrs.clone()), + _ => (handler.to_token_stream().to_string(), vec![]), + }; + //let name = handler.to_token_stream().to_string(); 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()); + let typename = Ident::new(&name.to_string(), Span::call_site()); + let ts_request_type = Ident::new(&format!("I{name}Request"), Span::call_site()); + let ts_response_type = Ident::new(&format!("I{name}Response"), Span::call_site()); + let ts_custom_section_ident = Ident::new(&format!("TS_{}", name.to_uppercase()), Span::call_site()); + // gPRC fields let fallback_name = name.replace("StopNotifying", "Notify"); let is_subscription = fallback_name.starts_with("Notify"); @@ -44,9 +59,14 @@ impl Handler { fn_camel, request_type, response_type, + typename, + ts_request_type, + ts_response_type, + ts_custom_section_ident, is_subscription, response_message_type, fallback_request_type, + docs, } } } diff --git a/rpc/macros/src/lib.rs b/rpc/macros/src/lib.rs index df54d793a..1c205c26e 100644 --- a/rpc/macros/src/lib.rs +++ b/rpc/macros/src/lib.rs @@ -10,6 +10,12 @@ pub fn build_wrpc_client_interface(input: TokenStream) -> TokenStream { wrpc::client::build_wrpc_client_interface(input) } +#[proc_macro] +#[proc_macro_error] +pub fn declare_typescript_wasm_interface(input: TokenStream) -> TokenStream { + wrpc::wasm::declare_typescript_wasm_interface(input) +} + #[proc_macro] #[proc_macro_error] pub fn build_wrpc_server_interface(input: TokenStream) -> TokenStream { diff --git a/rpc/macros/src/wrpc/wasm.rs b/rpc/macros/src/wrpc/wasm.rs index 89802ad87..011822019 100644 --- a/rpc/macros/src/wrpc/wasm.rs +++ b/rpc/macros/src/wrpc/wasm.rs @@ -1,6 +1,6 @@ use crate::handler::*; use convert_case::{Case, Casing}; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, Literal, Span, TokenStream}; use quote::{quote, ToTokens}; use regex::Regex; use std::convert::Into; @@ -8,7 +8,7 @@ use syn::{ parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, - Error, Expr, ExprArray, Result, Token, + Error, Expr, ExprArray, ExprLit, Lit, Result, Token, }; #[derive(Debug)] @@ -42,37 +42,50 @@ impl ToTokens for RpcHandlers { 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); + let Handler { + fn_call, fn_camel, fn_no_suffix, ts_request_type, ts_response_type, request_type, response_type, docs, .. + } = Handler::new(handler); - targets_no_args.push(quote! { + // / @param {object} value - an object containing { message: String, privateKey: String|PrivateKey } + // / @returns {String} the signature, in hex string format + let links = format! {"@see {{@link {ts_request_type}}}, {{@link {ts_response_type}}}"}; + let throws = "@throws `string` on an RPC error or a server-side error."; + targets_no_args.push(quote! { + #(#docs)* + #[doc=#links] + #[doc=#throws] #[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)?; + pub async fn #fn_no_suffix(&self, request : Option<#ts_request_type>) -> Result<#ts_response_type> { + let request: #request_type = request.unwrap_or_default().try_into()?; // log_info!("request: {:#?}",request); - let result: RpcResult<#response_type> = self.client.#fn_call(request).await; + let result: RpcResult<#response_type> = self.inner.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()) + Ok(response.try_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); + let Handler { + fn_call, fn_camel, fn_no_suffix, ts_request_type, ts_response_type, request_type, response_type, docs, .. + } = Handler::new(handler); + let links = format! {"@see {{@link {ts_request_type}}}, {{@link {ts_response_type}}}"}; + let throws = "@throws `string` on an RPC error, a server-side error or when supplying incorrect arguments."; targets_with_args.push(quote! { - + #(#docs)* + #[doc=#links] + #[doc=#throws] #[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; + pub async fn #fn_no_suffix(&self, request: #ts_request_type) -> Result<#ts_response_type> { + let request: #request_type = request.try_into()?; + let result: RpcResult<#response_type> = self.inner.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()) + Ok(response.try_into()?) } }); @@ -125,7 +138,14 @@ impl ToTokens for RpcSubscriptions { 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 (name, docs) = match handler { + syn::Expr::Path(expr_path) => (expr_path.path.to_token_stream().to_string(), &expr_path.attrs), + _ => { + continue; + } + }; + + let name = format!("Notify{}", name.as_str()); let regex = Regex::new(r"^Notify").unwrap(); let blank = regex.replace(&name, ""); let subscribe = regex.replace(&name, "Subscribe"); @@ -138,16 +158,24 @@ impl ToTokens for RpcSubscriptions { let fn_unsubscribe_camel = Ident::new(&unsubscribe.to_case(Case::Camel), Span::call_site()); targets.push(quote! { - + #(#docs)* #[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?; + if let Some(listener_id) = self.listener_id() { + self.inner.client.start_notify(listener_id, Scope::#scope(#sub_scope {})).await?; + } else { + workflow_log::log_error!("subscribe on a closed connection"); + } 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?; + if let Some(listener_id) = self.listener_id() { + self.inner.client.stop_notify(listener_id, Scope::#scope(#sub_scope {})).await?; + } else { + workflow_log::log_error!("unsubscribe on a closed connection"); + } Ok(()) } @@ -170,3 +198,140 @@ pub fn build_wrpc_wasm_bindgen_subscriptions(input: proc_macro::TokenStream) -> // println!("MACRO: {}", ts.to_string()); ts.into() } + +// ##################################################################### + +#[derive(Debug)] +struct TsInterface { + handler: Handler, + alias: Literal, + // declaration: Expr, + declaration: String, +} + +impl Parse for TsInterface { + fn parse(input: ParseStream) -> Result { + let parsed = Punctuated::::parse_terminated(input).unwrap(); + + if parsed.len() == 2 { + let mut iter = parsed.iter(); + let handler = Handler::new(iter.next().unwrap()); + let alias = Literal::string(&handler.name); + let declaration = extract_literal(&iter.next().unwrap().clone())?; + Ok(TsInterface { handler, alias, declaration }) + } else if parsed.len() == 3 { + let mut iter = parsed.iter(); + let handler = Handler::new(iter.next().unwrap()); + let alias = match iter.next().unwrap().clone() { + Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => Literal::string(&lit_str.value()), + _ => return Err(Error::new_spanned(parsed, "type spec must be a string literal".to_string())), + }; + let declaration = extract_literal(&iter.next().unwrap().clone())?; + Ok(TsInterface { handler, alias, declaration }) + } else { + return Err(Error::new_spanned( + parsed, + "usage: declare_wasm_interface!(typescript_type, [alias], typescript declaration)".to_string(), + )); + } + } +} + +impl ToTokens for TsInterface { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { handler, alias, declaration } = self; + let Handler { name, typename, ts_custom_section_ident, .. } = handler; + + let declaration = if name.ends_with("Request") { + let method = (&name.trim_end_matches("Request")[1..]).to_case(Case::Camel); + insert_typedoc( + declaration, + &format!( + r#" + Argument interface for the {{@link RpcClient.{method}}} RPC method. + "# + ), + ) + } else if name.ends_with("Response") { + let method = (&name.trim_end_matches("Response")[1..]).to_case(Case::Camel); + insert_typedoc( + declaration, + &format!( + r#" + Return interface for the {{@link RpcClient.{method}}} RPC method. + "# + ), + ) + } else { + declaration.to_owned() + }; + // println!("declaration: {}", declaration); + + quote! { + + #[wasm_bindgen(typescript_custom_section)] + const #ts_custom_section_ident: &'static str = #declaration; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = js_sys::Object, typescript_type = #alias)] + #[derive(Default)] + pub type #typename; + } + + + } + .to_tokens(tokens); + } +} + +pub fn declare_typescript_wasm_interface(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let declaration = parse_macro_input!(input as TsInterface); + let ts = declaration.to_token_stream(); + // println!("MACRO: {}", ts.to_string()); + ts.into() +} + +fn extract_literal(expr: &Expr) -> Result { + match expr { + Expr::Lit(expr_lit) => { + if let Lit::Str(lit_str) = &expr_lit.lit { + Ok(lit_str.value()) + } else { + Err(Error::new_spanned(expr, "argument must be a string literal".to_string())) + } + } + _ => Err(Error::new_spanned(expr, "argument must be a string literal".to_string())), + } +} + +fn insert_typedoc(text: &str, insertion: &str) -> String { + if let Some(mut index) = text.find("/**") { + index += 3; + let insertion = insertion + .split('\n') + .filter_map(|line| (!line.trim().is_empty()).then_some(format!("\n\t* {}", line.trim()))) + .collect::(); + let mut result = String::with_capacity(text.len() + insertion.len()); + result.push_str(&text[..index]); + result.push_str(&insertion); + result.push_str(&text[index..]); + + let lines = result + .split('\n') + .map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with("/**") || trimmed.starts_with('*') { + trimmed + } else { + line + } + }) + .collect::>() + .join("\n"); + + lines + } else { + text.to_string() + } +} diff --git a/rpc/wrpc/client/Cargo.toml b/rpc/wrpc/client/Cargo.toml index 5261c2aa5..199232ff0 100644 --- a/rpc/wrpc/client/Cargo.toml +++ b/rpc/wrpc/client/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true repository.workspace = true [features] -no-unsafe-eval = ["workflow-core/no-unsafe-eval","workflow-rpc/no-unsafe-eval"] +wasm32-sdk = ["kaspa-consensus-wasm/wasm32-sdk","kaspa-rpc-core/wasm32-sdk","workflow-rpc/wasm32-sdk"] default = [] [lib] @@ -20,6 +20,7 @@ crate-type = ["cdylib", "lib"] async-std.workspace = true async-trait.workspace = true borsh.workspace = true +cfg-if.workspace = true futures.workspace = true js-sys.workspace = true kaspa-addresses.workspace = true @@ -29,15 +30,18 @@ kaspa-notify.workspace = true kaspa-rpc-core.workspace = true kaspa-rpc-macros.workspace = true paste.workspace = true +rand.workspace = true regex.workspace = true serde_json.workspace = true serde-wasm-bindgen.workspace = true serde.workspace = true +toml.workspace = true thiserror.workspace = true wasm-bindgen-futures.workspace = true wasm-bindgen.workspace = true workflow-core.workspace = true workflow-dom.workspace = true +workflow-http.workspace = true workflow-log.workspace = true workflow-rpc.workspace = true workflow-wasm.workspace = true \ No newline at end of file diff --git a/rpc/wrpc/client/Resolvers.toml b/rpc/wrpc/client/Resolvers.toml new file mode 100644 index 000000000..295a352d2 --- /dev/null +++ b/rpc/wrpc/client/Resolvers.toml @@ -0,0 +1,11 @@ +[[resolver]] +url = "https://beacon.kaspa-ng.org" +enable = true + +[[resolver]] +url = "https://beacon.kaspa-ng.io" +enable = true + +[[resolver]] +url = "http://127.0.0.1:8888" +enable = false diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index 573d2a500..4e9fffc0c 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -1,6 +1,6 @@ -use crate::error::Error; use crate::imports::*; use crate::parse::parse_host; +use crate::{error::Error, node::NodeDescriptor}; use kaspa_consensus_core::network::NetworkType; use kaspa_notify::{ listener::ListenerLifespan, @@ -15,41 +15,42 @@ use std::fmt::Debug; use workflow_core::{channel::Multiplexer, runtime as application_runtime}; use workflow_dom::utils::window; use workflow_rpc::client::Ctl as WrpcCtl; -pub use workflow_rpc::client::{ConnectOptions, ConnectResult, ConnectStrategy, WebSocketConfig}; - -// /// [`NotificationMode`] controls notification delivery process -// #[wasm_bindgen] -// #[derive(Clone, Copy, Debug)] -// pub enum NotificationMode { -// /// Local notifier is used for notification processing. -// /// -// /// Multiple listeners can register and subscribe independently. -// MultiListeners, -// /// No notifier is present, notifications are relayed -// /// directly through the internal channel to a single listener. -// Direct, -// } +pub use workflow_rpc::client::{ + ConnectOptions, ConnectResult, ConnectStrategy, Resolver as RpcResolver, ResolverResult, WebSocketConfig, WebSocketError, +}; + +type RpcClientNotifier = Arc>; -#[derive(Clone)] struct Inner { rpc_client: Arc>, - notification_channel: Channel, + notification_relay_channel: Channel, + notification_intake_channel: Mutex>, + notifier: Arc>>, encoding: Encoding, wrpc_ctl_multiplexer: Multiplexer, rpc_ctl: RpcCtl, background_services_running: Arc, service_ctl: DuplexChannel<()>, + connect_guard: AsyncMutex<()>, + disconnect_guard: AsyncMutex<()>, + // --- + default_url: Mutex>, + current_url: Mutex>, + resolver: Mutex>, + network_id: Mutex>, + node_descriptor: Mutex>>, } impl Inner { - pub fn new(encoding: Encoding, url: &str) -> Result { + pub fn new(encoding: Encoding, url: Option<&str>, resolver: Option, network_id: Option) -> Result { // log_trace!("Kaspa wRPC::{encoding} connecting to: {url}"); let rpc_ctl = RpcCtl::with_descriptor(url); let wrpc_ctl_multiplexer = Multiplexer::::new(); - let options = RpcClientOptions { url, ctl_multiplexer: Some(wrpc_ctl_multiplexer.clone()), ..RpcClientOptions::default() }; + let options = RpcClientOptions::new().with_ctl_multiplexer(wrpc_ctl_multiplexer.clone()); - let notification_channel = Channel::unbounded(); + let notification_relay_channel = Channel::unbounded(); + let notification_intake_channel = Mutex::new(Channel::unbounded()); // The `Interface` struct can be used to register for server-side // notifications. All notification methods have to be created at @@ -69,7 +70,7 @@ impl Inner { ] .into_iter() .for_each(|notification_op| { - let notification_sender_ = notification_channel.sender.clone(); + let notification_sender_ = notification_relay_channel.sender.clone(); interface.notification( notification_op, workflow_rpc::client::Notification::new(move |notification: kaspa_rpc_core::Notification| { @@ -81,7 +82,7 @@ impl Inner { // log_info!("notification: posting to channel: {notification:?}"); notification_sender.send(notification).await?; } else { - log_warning!("WARNING: Kaspa RPC notification is not consumed by user: {:?}", notification); + log_warn!("WARNING: Kaspa RPC notification is not consumed by user: {:?}", notification); } Ok(()) }) @@ -89,32 +90,33 @@ impl Inner { ); }); - let ws_config = WebSocketConfig { - max_message_size: Some(1024 * 1024 * 1024), // 1Gb message size limit on native platforms - max_frame_size: None, - accept_unmasked_frames: false, - ..Default::default() - }; - - let rpc = Arc::new(RpcClient::new_with_encoding(encoding, interface.into(), options, Some(ws_config))?); + let rpc = Arc::new(RpcClient::new_with_encoding(encoding, interface.into(), options, None)?); let client = Self { rpc_client: rpc, - notification_channel, + notification_relay_channel, + notification_intake_channel, + notifier: Default::default(), encoding, wrpc_ctl_multiplexer, rpc_ctl, service_ctl: DuplexChannel::unbounded(), background_services_running: Arc::new(AtomicBool::new(false)), + connect_guard: async_std::sync::Mutex::new(()), + disconnect_guard: async_std::sync::Mutex::new(()), + // --- + default_url: Mutex::new(url.map(|s| s.to_string())), + current_url: Mutex::new(None), + resolver: Mutex::new(resolver), + network_id: Mutex::new(network_id), + node_descriptor: Mutex::new(None), }; Ok(client) } - pub fn notification_channel_receiver(&self) -> Receiver { - self.notification_channel.receiver.clone() - } - - pub fn shutdown_notification_channel(&self) -> bool { - self.notification_channel.receiver.close() + pub fn reset_notification_intake_channel(&self) { + let mut intake = self.notification_intake_channel.lock().unwrap(); + intake.sender.close(); + *intake = Channel::unbounded(); } /// Start sending notifications of some type to the client. @@ -129,13 +131,65 @@ impl Inner { self.rpc_client.call(RpcApiOps::Unsubscribe, scope).await.map_err(|err| err.to_string())?; Ok(()) } + + fn default_url(&self) -> Option { + self.default_url.lock().unwrap().clone() + } + + fn set_default_url(&self, url: Option<&str>) { + *self.default_url.lock().unwrap() = url.map(String::from); + } + + fn current_url(&self) -> Option { + self.current_url.lock().unwrap().clone() + } + + fn set_current_url(&self, url: Option<&str>) { + *self.current_url.lock().unwrap() = url.map(String::from); + } + + fn resolver(&self) -> Option { + self.resolver.lock().unwrap().clone() + } + + fn network_id(&self) -> Option { + *self.network_id.lock().unwrap() + } + + fn build_notifier(self: &Arc, subscription_context: Option) -> Result { + let receiver = self.notification_intake_channel.lock().unwrap().receiver.clone(); + + let enabled_events = EVENT_TYPE_ARRAY[..].into(); + let converter = Arc::new(RpcCoreConverter::new()); + let collector = Arc::new(RpcCoreCollector::new(WRPC_CLIENT, receiver, converter)); + let subscriber = Arc::new(Subscriber::new(WRPC_CLIENT, enabled_events, self.clone(), 0)); + let policies = MutationPolicies::new(UtxosChangedMutationPolicy::AddressSet); + let notifier = Arc::new(Notifier::new( + WRPC_CLIENT, + enabled_events, + vec![collector], + vec![subscriber], + subscription_context.unwrap_or_default(), + 3, + policies, + )); + + // let receiver = self.notification_intake_channel.lock().unwrap().receiver.clone(); + // let enabled_events = EVENT_TYPE_ARRAY[..].into(); + // let converter = Arc::new(RpcCoreConverter::new()); + // let collector = Arc::new(RpcCoreCollector::new(WRPC_CLIENT, receiver, converter)); + // let subscriber = Arc::new(Subscriber::new(WRPC_CLIENT, enabled_events, self.clone(), 0)); + // let notifier = Arc::new(Notifier::new(WRPC_CLIENT, enabled_events, vec![collector], vec![subscriber], 3)); + *self.notifier.lock().unwrap() = Some(notifier.clone()); + Ok(notifier) + } } impl Debug for Inner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("KaspaRpcClient") .field("rpc", &"rpc") - .field("notification_channel", &self.notification_channel) + // .field("notification_channel", &self.notification_channel) .field("encoding", &self.encoding) .finish() } @@ -156,6 +210,27 @@ impl SubscriptionManager for Inner { } } +#[async_trait] +impl RpcResolver for Inner { + async fn resolve_url(&self) -> ResolverResult { + let url = if let Some(url) = self.default_url() { + url + } else if let Some(resolver) = self.resolver().as_ref() { + let network_id = self.network_id().expect("Resolver requires network id in RPC client configuration"); + let node = resolver.get_node(self.encoding, network_id).await.map_err(WebSocketError::custom)?; + let url = node.url.clone(); + self.node_descriptor.lock().unwrap().replace(Arc::new(node)); + url + } else { + panic!("RpcClient resolver configuration error (expecting Some(Resolver))") + }; + + self.rpc_ctl.set_descriptor(Some(url.clone())); + self.set_current_url(Some(&url)); + Ok(url) + } +} + const WRPC_CLIENT: &str = "wrpc-client"; /// [`KaspaRpcClient`] allows connection to the Kaspa wRPC Server via @@ -170,61 +245,118 @@ const WRPC_CLIENT: &str = "wrpc-client"; #[derive(Clone)] pub struct KaspaRpcClient { inner: Arc, - notifier: Option>>, - notification_mode: NotificationMode, +} + +impl Debug for KaspaRpcClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KaspaRpcClient").field("url", &self.url()).field("connected", &self.is_connected()).finish() + } } impl KaspaRpcClient { /// Create a new `KaspaRpcClient` with the given Encoding and URL - // FIXME - pub fn new(encoding: Encoding, url: &str, subscription_context: Option) -> Result { - Self::new_with_args(encoding, NotificationMode::Direct, url, subscription_context) + pub fn new( + encoding: Encoding, + url: Option<&str>, + resolver: Option, + network_id: Option, + subscription_context: Option, + ) -> Result { + Self::new_with_args(encoding, url, resolver, network_id, subscription_context) + // FIXME + // pub fn new(encoding: Encoding, url: &str, ) -> Result { + // Self::new_with_args(encoding, NotificationMode::Direct, url, subscription_context) } /// Extended constructor that accepts [`NotificationMode`] argument. pub fn new_with_args( encoding: Encoding, - notification_mode: NotificationMode, - url: &str, + url: Option<&str>, + resolver: Option, + network_id: Option, subscription_context: Option, ) -> Result { - let inner = Arc::new(Inner::new(encoding, url)?); - let notifier = if matches!(notification_mode, NotificationMode::MultiListeners) { - let enabled_events = EVENT_TYPE_ARRAY[..].into(); - let converter = Arc::new(RpcCoreConverter::new()); - let collector = Arc::new(RpcCoreCollector::new(WRPC_CLIENT, inner.notification_channel_receiver(), converter)); - let subscriber = Arc::new(Subscriber::new(WRPC_CLIENT, enabled_events, inner.clone(), 0)); - let policies = MutationPolicies::new(UtxosChangedMutationPolicy::AddressSet); - Some(Arc::new(Notifier::new( - WRPC_CLIENT, - enabled_events, - vec![collector], - vec![subscriber], - subscription_context.unwrap_or_default(), - 3, - policies, - ))) - } else { - None - }; - - let client = KaspaRpcClient { inner, notifier, notification_mode }; + let inner = Arc::new(Inner::new(encoding, url, resolver, network_id)?); + inner.build_notifier(subscription_context)?; + let client = KaspaRpcClient { inner }; + // notification_mode: NotificationMode, + // url: &str, + // subscription_context: Option, + // ) -> Result { + // let inner = Arc::new(Inner::new(encoding, url)?); + // let notifier = if matches!(notification_mode, NotificationMode::MultiListeners) { + // let enabled_events = EVENT_TYPE_ARRAY[..].into(); + // let converter = Arc::new(RpcCoreConverter::new()); + // let collector = Arc::new(RpcCoreCollector::new(WRPC_CLIENT, inner.notification_channel_receiver(), converter)); + // let subscriber = Arc::new(Subscriber::new(WRPC_CLIENT, enabled_events, inner.clone(), 0)); + // let policies = MutationPolicies::new(UtxosChangedMutationPolicy::AddressSet); + // Some(Arc::new(Notifier::new( + // WRPC_CLIENT, + // enabled_events, + // vec![collector], + // vec![subscriber], + // subscription_context.unwrap_or_default(), + // 3, + // policies, + // ))) + // } else { + // None + // }; + + // let client = KaspaRpcClient { inner, notifier, notification_mode }; Ok(client) } - pub fn url(&self) -> String { - self.inner.rpc_client.url() + async fn start_notifier(&self) -> Result<()> { + let notifier = self.inner.build_notifier(None)?; + notifier.start(); + Ok(()) + } + + async fn stop_notifier(&self) -> Result<()> { + self.inner.reset_notification_intake_channel(); + self.notifier().join().await?; + Ok(()) + } + + fn notifier(&self) -> RpcClientNotifier { + self.inner.notifier.lock().unwrap().clone().expect("Rpc client is not correctly initialized") } - pub fn set_url(&self, url: &str) -> Result<()> { - self.inner.rpc_ctl.set_descriptor(Some(url.to_string())); - self.inner.rpc_client.set_url(url)?; + pub fn url(&self) -> Option { + self.inner.current_url() + } + + pub fn set_url(&self, url: Option<&str>) -> Result<()> { + self.inner.set_default_url(url); Ok(()) } - pub fn is_open(&self) -> bool { - self.inner.rpc_client.is_open() + pub fn is_connected(&self) -> bool { + self.inner.rpc_client.is_connected() + } + + pub fn encoding(&self) -> Encoding { + self.inner.encoding + } + + pub fn resolver(&self) -> Option { + self.inner.resolver() + } + + pub fn set_resolver(&self, resolver: Resolver) -> Result<()> { + self.inner.resolver.lock().unwrap().replace(resolver); + Ok(()) + } + + pub fn set_network_id(&self, network_id: &NetworkId) -> Result<()> { + self.inner.network_id.lock().unwrap().replace(*network_id); + Ok(()) + } + + pub fn node_descriptor(&self) -> Option> { + self.inner.node_descriptor.lock().unwrap().clone() } pub fn rpc_client(&self) -> &Arc> { @@ -239,36 +371,25 @@ impl KaspaRpcClient { &self.inner.rpc_ctl } - /// Starts RPC services. + /// Start background RPC services. pub async fn start(&self) -> Result<()> { if !self.inner.background_services_running.load(Ordering::SeqCst) { - match &self.notification_mode { - NotificationMode::MultiListeners => { - self.notifier.clone().unwrap().start(); - } - NotificationMode::Direct => {} - } - + self.inner.background_services_running.store(true, Ordering::SeqCst); + self.start_notifier().await?; self.start_rpc_ctl_service().await?; } + Ok(()) } - /// Stops background services. + /// Stop background RPC services. pub async fn stop(&self) -> Result<()> { if self.inner.background_services_running.load(Ordering::SeqCst) { - match &self.notification_mode { - NotificationMode::MultiListeners => { - self.inner.shutdown_notification_channel(); - self.notifier.as_ref().unwrap().join().await?; - } - NotificationMode::Direct => { - // self.notification_ctl.signal(()).await?; - } - } - self.stop_rpc_ctl_service().await?; + self.stop_notifier().await?; + self.inner.background_services_running.store(false, Ordering::SeqCst); } + Ok(()) } @@ -276,25 +397,58 @@ impl KaspaRpcClient { /// to the wRPC server. If the supplied `block` call is `true` /// this function will block until the first successful /// connection. - pub async fn connect(&self, options: ConnectOptions) -> ConnectResult { - if let Some(url) = options.url.as_ref() { - self.inner.rpc_ctl.set_descriptor(Some(url.clone())); + /// + /// This method starts background RPC services if they are not running and + /// attempts to connect to the RPC endpoint. + pub async fn connect(&self, options: Option) -> ConnectResult { + let _guard = self.inner.connect_guard.lock().await; + + let mut options = options.unwrap_or_default(); + let strategy = options.strategy; + + if let Some(url) = options.url.take() { + self.set_url(Some(&url))?; } + + // 1Gb message and frame size limits (on native and NodeJs platforms) + let ws_config = WebSocketConfig { + max_message_size: Some(1024 * 1024 * 1024), + max_frame_size: Some(1024 * 1024 * 1024), + accept_unmasked_frames: false, + resolver: Some(self.inner.clone()), + ..Default::default() + }; + self.start().await?; - Ok(self.inner.rpc_client.connect(options).await?) + self.inner.rpc_client.configure(ws_config); + match self.inner.rpc_client.connect(options).await { + Ok(v) => Ok(v), + Err(err) => { + if strategy == ConnectStrategy::Fallback { + let _guard = self.inner.disconnect_guard.lock().await; + self.inner.rpc_client.shutdown().await?; + self.stop().await?; + } + Err(err.into()) + } + } } + /// This method stops background RPC services and disconnects + /// from the RPC endpoint. pub async fn disconnect(&self) -> Result<()> { + let _guard = self.inner.disconnect_guard.lock().await; + self.inner.rpc_client.shutdown().await?; self.stop().await?; Ok(()) } - /// Stop and shutdown RPC disconnecting existing connections - /// and stopping reconnection process. - pub async fn shutdown(&self) -> Result<()> { - Ok(self.inner.rpc_client.shutdown().await?) - } + // Stop and shutdown RPC disconnecting existing connections + // and stopping reconnection process. + // pub async fn shutdown(&self) -> Result<()> { + // Ok(self.inner.rpc_client.shutdown().await?) + // } /// A helper function that is not `async`, allowing connection /// process to be initiated from non-async contexts. @@ -307,15 +461,7 @@ impl KaspaRpcClient { } pub fn notification_channel_receiver(&self) -> Receiver { - self.inner.notification_channel.receiver.clone() - } - - pub fn encoding(&self) -> Encoding { - self.inner.encoding - } - - pub fn notification_mode(&self) -> NotificationMode { - self.notification_mode + self.inner.notification_intake_channel.lock().unwrap().receiver.clone() } pub fn ctl(&self) -> &RpcCtl { @@ -337,7 +483,7 @@ impl KaspaRpcClient { } let location = window().location(); let protocol = - location.protocol().map_err(|_| Error::AddressError("Unable to obtain window location protocol".to_string()))?; + location.protocol().map_err(|_| Error::UrlError("Unable to obtain window location protocol".to_string()))?; if protocol == "http:" || protocol == "chrome-extension:" { Ok("ws") } else if protocol == "https:" { @@ -372,19 +518,30 @@ impl KaspaRpcClient { async fn start_rpc_ctl_service(&self) -> Result<()> { let inner = self.inner.clone(); let wrpc_ctl_channel = inner.wrpc_ctl_multiplexer.channel(); + let notification_relay_channel = inner.notification_relay_channel.clone(); spawn(async move { loop { select! { _ = inner.service_ctl.request.receiver.recv().fuse() => { break; }, + msg = notification_relay_channel.receiver.recv().fuse() => { + if let Ok(msg) = msg { + // inner.rpc_ctl.notify(msg).await.expect("(KaspaRpcClient) rpc_ctl.notify() error"); + if let Err(err) = inner.notification_intake_channel.lock().unwrap().sender.try_send(msg) { + log_error!("notification_intake_channel.sender.try_send() error: {err}"); + } + } else { + log_error!("notification_relay_channel receiver error"); + } + } msg = wrpc_ctl_channel.receiver.recv().fuse() => { if let Ok(msg) = msg { match msg { - WrpcCtl::Open => { + WrpcCtl::Connect => { inner.rpc_ctl.signal_open().await.expect("(KaspaRpcClient) rpc_ctl.signal_open() error"); } - WrpcCtl::Close => { + WrpcCtl::Disconnect => { inner.rpc_ctl.signal_close().await.expect("(KaspaRpcClient) rpc_ctl.signal_close() error"); } } @@ -404,6 +561,13 @@ impl KaspaRpcClient { self.inner.service_ctl.signal(()).await?; Ok(()) } + + /// Triggers a disconnection on the underlying WebSocket. + /// This is intended for debug purposes only. + /// Can be used to test application reconnection logic. + pub fn trigger_abort(&self) -> Result<()> { + Ok(self.inner.rpc_client.trigger_abort()?) + } } #[async_trait] @@ -462,50 +626,32 @@ impl RpcApi for KaspaRpcClient { /// Register a new listener and returns an id and a channel receiver. fn register_new_listener(&self, connection: ChannelConnection) -> ListenerId { - match self.notification_mode { - NotificationMode::MultiListeners => { - self.notifier.as_ref().unwrap().register_new_listener(connection, ListenerLifespan::Dynamic) - } - NotificationMode::Direct => ListenerId::default(), - } + self.notifier().register_new_listener(connection, ListenerLifespan::Dynamic) + // match self.notification_mode { + // NotificationMode::MultiListeners => { + // self.notifier.as_ref().unwrap().register_new_listener(connection, ListenerLifespan::Dynamic) + // } + // NotificationMode::Direct => ListenerId::default(), + // } } /// Unregister an existing listener. /// /// Stop all notifications for this listener and drop its channel. async fn unregister_listener(&self, id: ListenerId) -> RpcResult<()> { - match self.notification_mode { - NotificationMode::MultiListeners => { - self.notifier.as_ref().unwrap().unregister_listener(id)?; - } - NotificationMode::Direct => {} - } + self.notifier().unregister_listener(id)?; Ok(()) } /// Start sending notifications of some type to a listener. async fn start_notify(&self, id: ListenerId, scope: Scope) -> RpcResult<()> { - match self.notification_mode { - NotificationMode::MultiListeners => { - self.notifier.clone().unwrap().try_start_notify(id, scope)?; - } - NotificationMode::Direct => { - self.inner.start_notify_to_client(scope).await?; - } - } + self.notifier().try_start_notify(id, scope)?; Ok(()) } /// Stop sending notifications of some type to a listener. async fn stop_notify(&self, id: ListenerId, scope: Scope) -> RpcResult<()> { - match self.notification_mode { - NotificationMode::MultiListeners => { - self.notifier.clone().unwrap().try_stop_notify(id, scope)?; - } - NotificationMode::Direct => { - self.inner.stop_notify_to_client(scope).await?; - } - } + self.notifier().try_stop_notify(id, scope)?; Ok(()) } } diff --git a/rpc/wrpc/client/src/error.rs b/rpc/wrpc/client/src/error.rs index cbdeeddd5..781455ddd 100644 --- a/rpc/wrpc/client/src/error.rs +++ b/rpc/wrpc/client/src/error.rs @@ -3,8 +3,10 @@ use wasm_bindgen::JsError; use wasm_bindgen::JsValue; use workflow_core::channel::ChannelError; use workflow_core::sendable::*; +use workflow_http::error::Error as HttpError; use workflow_rpc::client::error::Error as RpcError; use workflow_rpc::client::error::WebSocketError; +use workflow_wasm::error::Error as WasmError; use workflow_wasm::printable::*; #[derive(Debug, Error)] @@ -13,7 +15,7 @@ pub enum Error { Custom(String), #[error("wRPC address error -> {0}")] - AddressError(String), + UrlError(String), #[error("wRPC -> {0}")] RpcError(#[from] RpcError), @@ -30,7 +32,7 @@ pub enum Error { #[error("Channel -> {0}")] ChannelError(String), - #[error("Serde WASM bindgen ser/deser error: {0}")] + #[error("Serde WASM bindgen serialization or deserialization error: {0}")] SerdeWasmBindgen(Sendable), #[error("{0}")] @@ -44,6 +46,21 @@ pub enum Error { #[error(transparent)] ConsensusWasm(#[from] kaspa_consensus_wasm::error::Error), + + #[error(transparent)] + HttpError(#[from] HttpError), + + #[error(transparent)] + WasmError(#[from] WasmError), + + #[error(transparent)] + AddressError(#[from] kaspa_addresses::AddressError), + + #[error(transparent)] + TomlError(#[from] toml::de::Error), + + #[error(transparent)] + NetworkId(#[from] kaspa_consensus_core::network::NetworkIdError), } impl Error { diff --git a/rpc/wrpc/client/src/imports.rs b/rpc/wrpc/client/src/imports.rs index 8fa31702b..b95128bb7 100644 --- a/rpc/wrpc/client/src/imports.rs +++ b/rpc/wrpc/client/src/imports.rs @@ -1,8 +1,14 @@ +#![allow(unused_imports)] + pub use crate::client::*; +pub use crate::resolver::Resolver; pub use crate::result::Result; +pub use async_std::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; pub use async_trait::async_trait; +pub use cfg_if::cfg_if; pub use futures::*; pub use js_sys::Function; +pub use kaspa_consensus_core::network::{NetworkId, NetworkIdT}; pub use kaspa_notify::{ error::{Error as NotifyError, Result as NotifyResult}, events::EVENT_TYPE_ARRAY, @@ -18,6 +24,7 @@ pub use kaspa_rpc_core::{ notify::{connection::ChannelConnection, mode::NotificationMode}, prelude::*, }; +pub use serde::{Deserialize, Serialize}; pub use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, diff --git a/rpc/wrpc/client/src/lib.rs b/rpc/wrpc/client/src/lib.rs index 3a6fcf356..b3f26c425 100644 --- a/rpc/wrpc/client/src/lib.rs +++ b/rpc/wrpc/client/src/lib.rs @@ -2,6 +2,8 @@ pub mod client; pub mod error; mod imports; pub mod result; -pub mod wasm; -pub use imports::{KaspaRpcClient, WrpcEncoding}; +pub use imports::{KaspaRpcClient, Resolver, WrpcEncoding}; +pub mod node; pub mod parse; +pub mod prelude; +pub mod resolver; diff --git a/rpc/wrpc/client/src/node.rs b/rpc/wrpc/client/src/node.rs new file mode 100644 index 000000000..4afb0f1b7 --- /dev/null +++ b/rpc/wrpc/client/src/node.rs @@ -0,0 +1,44 @@ +use crate::imports::*; + +/// +/// Data structure representing a Node connection endpoint +/// as provided by the {@link Resolver}. +/// +/// @category Node RPC +/// +#[derive(Clone, Debug, Serialize, Deserialize)] +#[wasm_bindgen(inspectable)] +pub struct NodeDescriptor { + /// The unique identifier of the node. + #[wasm_bindgen(getter_with_clone)] + pub id: String, + /// The URL of the node WebSocket (wRPC URL). + #[wasm_bindgen(getter_with_clone)] + pub url: String, + /// Optional name of the node provider. + #[wasm_bindgen(getter_with_clone)] + pub provider_name: Option, + /// Optional site URL of the node provider. + #[wasm_bindgen(getter_with_clone)] + pub provider_url: Option, +} + +impl Eq for NodeDescriptor {} + +impl PartialEq for NodeDescriptor { + fn eq(&self, other: &Self) -> bool { + self.url == other.url + } +} + +impl std::fmt::Display for NodeDescriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.url) + } +} + +impl NodeDescriptor { + pub fn url(&self) -> String { + self.url.clone() + } +} diff --git a/rpc/wrpc/client/src/prelude.rs b/rpc/wrpc/client/src/prelude.rs new file mode 100644 index 000000000..6a410b723 --- /dev/null +++ b/rpc/wrpc/client/src/prelude.rs @@ -0,0 +1,7 @@ +pub use crate::client::{ConnectOptions, ConnectStrategy}; +pub use crate::{KaspaRpcClient, Resolver, WrpcEncoding}; +pub use kaspa_consensus_core::network::{NetworkId, NetworkType}; +pub use kaspa_notify::{connection::ChannelType, listener::ListenerId, scope::*}; +pub use kaspa_rpc_core::notify::{connection::ChannelConnection, mode::NotificationMode}; +pub use kaspa_rpc_core::{api::ctl::RpcState, Notification}; +pub use kaspa_rpc_core::{api::rpc::RpcApi, *}; diff --git a/rpc/wrpc/client/src/resolver.rs b/rpc/wrpc/client/src/resolver.rs new file mode 100644 index 000000000..4fd159b40 --- /dev/null +++ b/rpc/wrpc/client/src/resolver.rs @@ -0,0 +1,120 @@ +use crate::error::Error; +use crate::imports::*; +use crate::node::NodeDescriptor; +pub use futures::future::join_all; +use rand::seq::SliceRandom; +use rand::thread_rng; +use workflow_http::get_json; + +const DEFAULT_VERSION: usize = 1; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolverRecord { + pub url: String, + pub enable: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ResolverConfig { + resolver: Vec, +} + +fn try_parse_resolvers(toml: &str) -> Result>> { + Ok(toml::from_str::(toml)? + .resolver + .into_iter() + .filter_map(|resolver| resolver.enable.unwrap_or(true).then_some(Arc::new(resolver.url))) + .collect::>()) +} + +#[derive(Debug)] +struct Inner { + pub urls: Vec>, +} + +impl Inner { + pub fn new(urls: Vec>) -> Self { + Self { urls } + } +} + +/// +/// Resolver is a client for obtaining public Kaspa wRPC endpoints. +/// +#[derive(Debug, Clone)] +pub struct Resolver { + inner: Arc, +} + +impl Default for Resolver { + fn default() -> Self { + let toml = include_str!("../Resolvers.toml"); + let urls = try_parse_resolvers(toml).expect("TOML: Unable to parse RPC Resolver list"); + Self { inner: Arc::new(Inner::new(urls)) } + } +} + +impl Resolver { + pub fn new(urls: Vec>) -> Self { + if urls.is_empty() { + panic!("Resolver: Empty URL list supplied to the constructor."); + } + + Self { inner: Arc::new(Inner::new(urls)) } + } + + pub fn urls(&self) -> Vec> { + self.inner.urls.clone() + } + + async fn fetch_node_info(&self, url: &str, encoding: Encoding, network_id: NetworkId) -> Result { + let url = format!("{}/v{}/wrpc/{}/{}", url, DEFAULT_VERSION, encoding, network_id); + let node = + get_json::(&url).await.map_err(|error| Error::custom(format!("Unable to connect to {url}: {error}")))?; + Ok(node) + } + + pub async fn fetch(&self, encoding: Encoding, network_id: NetworkId) -> Result { + let mut urls = self.inner.urls.clone(); + urls.shuffle(&mut thread_rng()); + + let mut errors = Vec::default(); + for url in urls { + match self.fetch_node_info(&url, encoding, network_id).await { + Ok(node) => return Ok(node), + Err(error) => errors.push(error), + } + } + Err(Error::Custom(format!("Failed to connect: {:?}", errors))) + } + + pub async fn fetch_all(&self, encoding: Encoding, network_id: NetworkId) -> Result> { + let futures = self.inner.urls.iter().map(|url| self.fetch_node_info(url, encoding, network_id)).collect::>(); + let mut errors = Vec::default(); + let result = join_all(futures) + .await + .into_iter() + .filter_map(|result| match result { + Ok(node) => Some(node), + Err(error) => { + errors.push(format!("{:?}", error)); + None + } + }) + .collect::>(); + if result.is_empty() { + Err(Error::Custom(format!("Failed to connect: {:?}", errors))) + } else { + Ok(result) + } + } + + pub async fn get_node(&self, encoding: Encoding, network_id: NetworkId) -> Result { + self.fetch(encoding, network_id).await + } + + pub async fn get_url(&self, encoding: Encoding, network_id: NetworkId) -> Result { + let nodes = self.fetch(encoding, network_id).await?; + Ok(nodes.url.clone()) + } +} diff --git a/rpc/wrpc/client/src/wasm.rs b/rpc/wrpc/client/src/wasm.rs deleted file mode 100644 index 9718ee5a4..000000000 --- a/rpc/wrpc/client/src/wasm.rs +++ /dev/null @@ -1,383 +0,0 @@ -use crate::error::Error; -use crate::imports::*; -use crate::result::Result; -use js_sys::Array; -use kaspa_addresses::{Address, AddressList}; -use kaspa_consensus_core::network::{wasm::Network, NetworkType}; -use kaspa_consensus_wasm::{SignableTransaction, Transaction}; -use kaspa_notify::notification::Notification as NotificationT; -pub use kaspa_rpc_macros::{build_wrpc_wasm_bindgen_interface, build_wrpc_wasm_bindgen_subscriptions}; -pub use serde_wasm_bindgen::from_value; -pub use workflow_wasm::serde::to_value; - -struct NotificationSink(Function); -unsafe impl Send for NotificationSink {} -impl From for Function { - fn from(f: NotificationSink) -> Self { - f.0 - } -} - -pub struct Inner { - notification_task: AtomicBool, - notification_ctl: DuplexChannel, - notification_callback: Arc>>, -} - -/// Kaspa RPC client -#[wasm_bindgen(inspectable)] -#[derive(Clone)] -pub struct RpcClient { - #[wasm_bindgen(skip)] - pub client: Arc, - pub(crate) inner: Arc, -} - -#[wasm_bindgen] -impl RpcClient { - /// Create a new RPC client with [`Encoding`] and a `url`. - #[wasm_bindgen(constructor)] - pub fn new(url: &str, encoding: Encoding, network_type: Option) -> Result { - let url = if let Some(network_type) = network_type { Self::parse_url(url, encoding, network_type)? } else { url.to_string() }; - - let rpc_client = RpcClient { - client: Arc::new(KaspaRpcClient::new(encoding, url.as_str(), None).unwrap_or_else(|err| panic!("{err}"))), - inner: Arc::new(Inner { - notification_task: AtomicBool::new(false), - notification_ctl: DuplexChannel::oneshot(), - notification_callback: Arc::new(Mutex::new(None)), - }), - }; - - Ok(rpc_client) - } - - #[wasm_bindgen(getter)] - pub fn url(&self) -> String { - self.client.url() - } - - #[wasm_bindgen(getter, js_name = "open")] - pub fn is_open(&self) -> bool { - self.client.is_open() - } - - /// Connect to the Kaspa RPC server. This function starts a background - /// task that connects and reconnects to the server if 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()?; - - self.start_notification_task()?; - self.client.connect(options).await?; - Ok(()) - } - - /// Disconnect from the Kaspa RPC server. - pub async fn disconnect(&self) -> Result<()> { - self.clear_notification_callback(); - self.stop_notification_task().await?; - self.client.shutdown().await?; - Ok(()) - } - - async fn stop_notification_task(&self) -> Result<()> { - if self.inner.notification_task.load(Ordering::SeqCst) { - self.inner.notification_task.store(false, Ordering::SeqCst); - self.inner.notification_ctl.signal(()).await.map_err(|err| JsError::new(&err.to_string()))?; - } - Ok(()) - } - - fn clear_notification_callback(&self) { - *self.inner.notification_callback.lock().unwrap() = None; - } - - /// Register a notification callback. - pub async fn notify(&self, callback: JsValue) -> Result<()> { - if callback.is_function() { - let fn_callback: Function = callback.into(); - self.inner.notification_callback.lock().unwrap().replace(NotificationSink(fn_callback)); - } else { - self.stop_notification_task().await?; - self.clear_notification_callback(); - } - Ok(()) - } -} - -impl RpcClient { - pub fn new_with_rpc_client(client: Arc) -> RpcClient { - RpcClient { - client, - inner: Arc::new(Inner { - notification_task: AtomicBool::new(false), - notification_ctl: DuplexChannel::oneshot(), - notification_callback: Arc::new(Mutex::new(None)), - }), - } - } - - pub fn client(&self) -> &Arc { - &self.client - } - - /// Notification task receives notifications and executes them on the - /// user-supplied callback function. - fn start_notification_task(&self) -> Result<()> { - let ctl_receiver = self.inner.notification_ctl.request.receiver.clone(); - let ctl_sender = self.inner.notification_ctl.response.sender.clone(); - let notification_receiver = self.client.notification_channel_receiver(); - let notification_callback = self.inner.notification_callback.clone(); - - spawn(async move { - loop { - select! { - _ = ctl_receiver.recv().fuse() => { - break; - }, - msg = notification_receiver.recv().fuse() => { - // log_info!("notification: {:?}",msg); - if let Ok(notification) = &msg { - if let Some(callback) = notification_callback.lock().unwrap().as_ref() { - let op: RpcApiOps = notification.event_type().into(); - let op_value = to_value(&op).map_err(|err|{ - log_error!("Notification handler - unable to convert notification op: {}",err.to_string()); - }).ok(); - let op_payload = notification.to_value().map_err(|err| { - log_error!("Notification handler - unable to convert notification payload: {}",err.to_string()); - }).ok(); - if op_value.is_none() || op_payload.is_none() { - continue; - } - if let Err(err) = callback.0.call2(&JsValue::undefined(), &op_value.unwrap(), &op_payload.unwrap()) { - log_error!("Error while executing notification callback: {:?}",err); - } - } - } - } - } - } - - ctl_sender.send(()).await.ok(); - }); - - Ok(()) - } -} - -#[wasm_bindgen] -impl RpcClient { - #[wasm_bindgen(js_name = "defaultPort")] - pub fn default_port(encoding: WrpcEncoding, network: Network) -> Result { - let network_type = NetworkType::try_from(network)?; - match encoding { - WrpcEncoding::Borsh => Ok(network_type.default_borsh_rpc_port()), - WrpcEncoding::SerdeJson => Ok(network_type.default_json_rpc_port()), - } - } - - /// Constructs an WebSocket RPC URL given the partial URL or an IP, RPC encoding - /// and a network type. - /// - /// # Arguments - /// - /// * `url` - Partial URL or an IP address - /// * `encoding` - RPC encoding - /// * `network_type` - Network type - /// - #[wasm_bindgen(js_name = parseUrl)] - pub fn parse_url(url: &str, encoding: Encoding, network: Network) -> Result { - let url_ = KaspaRpcClient::parse_url(url.to_string(), encoding, network.try_into()?)?; - Ok(url_) - } -} - -#[wasm_bindgen] -impl RpcClient { - /// Subscription to DAA Score - #[wasm_bindgen(js_name = subscribeDaaScore)] - pub async fn subscribe_daa_score(&self) -> Result<()> { - self.client.start_notify(ListenerId::default(), Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; - Ok(()) - } - - /// Unsubscribe from DAA Score - #[wasm_bindgen(js_name = unsubscribeDaaScore)] - pub async fn unsubscribe_daa_score(&self) -> Result<()> { - self.client.stop_notify(ListenerId::default(), Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; - Ok(()) - } - - /// Subscription to UTXOs Changed notifications - #[wasm_bindgen(js_name = subscribeUtxosChanged)] - pub async fn subscribe_utxos_changed(&self, addresses: &JsValue) -> Result<()> { - let addresses = Array::from(addresses) - .to_vec() - .into_iter() - .map(|jsv| from_value(jsv).map_err(|err| JsError::new(&err.to_string()))) - .collect::, JsError>>()?; - self.client.start_notify(ListenerId::default(), UtxosChangedScope::new(addresses).into()).await?; - Ok(()) - } - - /// Unsubscribe from DAA Score (test) - #[wasm_bindgen(js_name = unsubscribeUtxosChanged)] - pub async fn unsubscribe_utxos_changed(&self, addresses: &JsValue) -> Result<()> { - let addresses = Array::from(addresses) - .to_vec() - .into_iter() - .map(|jsv| from_value(jsv).map_err(|err| JsError::new(&err.to_string()))) - .collect::, JsError>>()?; - self.client.stop_notify(ListenerId::default(), UtxosChangedScope::new(addresses).into()).await?; - Ok(()) - } - - // scope variant with field functions - - #[wasm_bindgen(js_name = subscribeVirtualChainChanged)] - pub async fn subscribe_virtual_chain_changed(&self, include_accepted_transaction_ids: bool) -> Result<()> { - self.client - .start_notify( - ListenerId::default(), - Scope::VirtualChainChanged(VirtualChainChangedScope { include_accepted_transaction_ids }), - ) - .await?; - Ok(()) - } - #[wasm_bindgen(js_name = unsubscribeVirtualChainChanged)] - pub async fn unsubscribe_virtual_chain_changed(&self, include_accepted_transaction_ids: bool) -> Result<()> { - self.client - .stop_notify( - ListenerId::default(), - Scope::VirtualChainChanged(VirtualChainChangedScope { include_accepted_transaction_ids }), - ) - .await?; - Ok(()) - } - - // #[wasm_bindgen(js_name = subscribeUtxosChanged)] - // pub async fn subscribe_utxos_changed(&self, addresses: Vec
) -> JsResult<()> { - // self.client.start_notify(ListenerId::default(), Scope::UtxosChanged(UtxosChangedScope { addresses })).await?; - // Ok(()) - // } - // #[wasm_bindgen(js_name = unsubscribeUtxosChanged)] - // pub async fn unsubscribe_utxos_changed(&self, addresses: Vec
) -> JsResult<()> { - // self.client.stop_notify(ListenerId::default(), Scope::UtxosChanged(UtxosChangedScope { addresses })).await?; - // Ok(()) - // } -} - -// Build subscribe functions -build_wrpc_wasm_bindgen_subscriptions!([ - BlockAdded, - //VirtualChainChanged, // can't used this here due to non-C-style enum variant - FinalityConflict, - FinalityConflictResolved, - //UtxosChanged, // can't used this here due to non-C-style enum variant - SinkBlueScoreChanged, - VirtualDaaScoreChanged, - PruningPointUtxoSetOverride, - NewBlockTemplate, -]); - -// Build RPC method invocation functions. This macro -// takes two lists. First list is for functions that -// do not have arguments and the second one is for -// functions that have single arguments (request). - -build_wrpc_wasm_bindgen_interface!( - [ - // functions with no arguments - GetBlockCount, - GetBlockDagInfo, - GetCoinSupply, - GetConnectedPeerInfo, - GetInfo, - GetPeerAddresses, - GetMetrics, - GetSink, - GetSinkBlueScore, - Ping, - Shutdown, - GetServerInfo, - GetSyncStatus, - ], - [ - // functions with `request` argument - AddPeer, - Ban, - EstimateNetworkHashesPerSecond, - GetBalanceByAddress, - GetBalancesByAddresses, - GetBlock, - GetBlocks, - GetBlockTemplate, - GetDaaScoreTimestampEstimate, - GetCurrentNetwork, - GetHeaders, - GetMempoolEntries, - GetMempoolEntriesByAddresses, - GetMempoolEntry, - GetSubnetwork, - // GetUtxosByAddresses, - GetVirtualChainFromBlock, - ResolveFinalityConflict, - SubmitBlock, - // SubmitTransaction, - Unban, - ] -); - -#[wasm_bindgen] -impl RpcClient { - #[wasm_bindgen(js_name = submitTransaction)] - pub async fn js_submit_transaction(&self, js_value: JsValue, allow_orphan: Option) -> Result { - let transaction = if let Ok(signable) = SignableTransaction::try_from(&js_value) { - Transaction::from(signable) - } else if let Ok(transaction) = Transaction::try_from(js_value) { - transaction - } else { - return Err(Error::custom("invalid transaction data")); - }; - - let transaction = RpcTransaction::from(transaction); - - let request = SubmitTransactionRequest { transaction, allow_orphan: allow_orphan.unwrap_or(false) }; - - // log_info!("submit_transaction req: {:?}", request); - let response = self.submit_transaction(request).await.map_err(|err| wasm_bindgen::JsError::new(&err.to_string()))?; - to_value(&response).map_err(|err| err.into()) - } - - /// This call accepts an `Array` of `Address` or an Array of address strings. - #[wasm_bindgen(js_name = getUtxosByAddresses)] - pub async fn get_utxos_by_addresses(&self, request: JsValue) -> Result { - let request = if let Ok(addresses) = AddressList::try_from(&request) { - GetUtxosByAddressesRequest { addresses: addresses.into() } - } else { - from_value::(request)? - }; - - let result: RpcResult = self.client.get_utxos_by_addresses_call(request).await; - let response: GetUtxosByAddressesResponse = result.map_err(|err| wasm_bindgen::JsError::new(&err.to_string()))?; - to_value(&response.entries).map_err(|err| err.into()) - } - - #[wasm_bindgen(js_name = getUtxosByAddressesCall)] - pub async fn get_utxos_by_addresses_call(&self, request: JsValue) -> Result { - let request = from_value::(request)?; - let result: RpcResult = self.client.get_utxos_by_addresses_call(request).await; - let response: GetUtxosByAddressesResponse = result.map_err(|err| wasm_bindgen::JsError::new(&err.to_string()))?; - to_value(&response).map_err(|err| err.into()) - } -} - -impl RpcClient { - pub async fn submit_transaction(&self, request: SubmitTransactionRequest) -> Result { - let result: RpcResult = self.client.submit_transaction_call(request).await; - let response: SubmitTransactionResponse = result?; - Ok(response) - } -} diff --git a/rpc/wrpc/examples/subscriber/Cargo.toml b/rpc/wrpc/examples/subscriber/Cargo.toml new file mode 100644 index 000000000..1e1b8bdba --- /dev/null +++ b/rpc/wrpc/examples/subscriber/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "kaspa-wrpc-example-subscriber" +description = "Kaspa wRPC subscription example" +publish = false +rust-version.workspace = true +version.workspace = true +edition.workspace = true +authors.workspace = true +include.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +futures.workspace = true +kaspa-consensus-core.workspace = true +kaspa-rpc-core.workspace = true +kaspa-wrpc-client.workspace = true +kaspa-notify.workspace = true +tokio.workspace = true +workflow-core.workspace = true +workflow-log.workspace = true +ctrlc.workspace = true diff --git a/rpc/wrpc/examples/subscriber/src/main.rs b/rpc/wrpc/examples/subscriber/src/main.rs new file mode 100644 index 000000000..00aa128ba --- /dev/null +++ b/rpc/wrpc/examples/subscriber/src/main.rs @@ -0,0 +1,289 @@ +// Basic example of a Kaspa wRPC client that connects to a node +// and subscribes to notifications. This example demonstrates +// how to handle RPC connection events, perform subscriptions, +// handle subscription notifications etc. + +pub use futures::{select, select_biased, FutureExt, Stream, StreamExt, TryStreamExt}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +// We use workflow-rs primitives for async task and channel management +// as they function uniformly in tokio as well as WASM32 runtimes. +use workflow_core::channel::{oneshot, Channel, DuplexChannel}; +use workflow_core::task::spawn; +use workflow_log::prelude::*; + +// Kaspa RPC primitives +use kaspa_wrpc_client::prelude::*; +// reuse wRPC Result type for convenience +use kaspa_wrpc_client::result::Result; + +struct Inner { + // task control duplex channel - a pair of channels where sender + // is used to signal an async task termination request and receiver + // is used to signal task termination completion. + task_ctl: DuplexChannel<()>, + // Kaspa wRPC client instance + client: Arc, + // our own view on the connection state + is_connected: AtomicBool, + // channel supplied to the notification subsystem + // to receive the node notifications we subscribe to + notification_channel: Channel, + // listener id used to manage notification scopes + // we can have multiple IDs for different scopes + // paired with multiple notification channels + listener_id: Mutex>, +} + +// Example primitive that manages an RPC connection and +// runs its own event task to handle RPC connection +// events and node notifications we subscribe to. +#[derive(Clone)] +pub struct Listener { + inner: Arc, +} + +impl Listener { + pub fn try_new(network_id: NetworkId, url: Option) -> Result { + // if not url is supplied we use the default resolver to + // obtain the public node rpc endpoint + let (resolver, url) = if let Some(url) = url { (None, Some(url)) } else { (Some(Resolver::default()), None) }; + + // Create a basic Kaspa RPC client instance using Borsh encoding. + let client = Arc::new(KaspaRpcClient::new_with_args(WrpcEncoding::Borsh, url.as_deref(), resolver, Some(network_id), None)?); + + let inner = Inner { + task_ctl: DuplexChannel::oneshot(), + client, + is_connected: AtomicBool::new(false), + notification_channel: Channel::unbounded(), + listener_id: Mutex::new(None), + }; + + Ok(Self { inner: Arc::new(inner) }) + } + + // Helper fn to check if we are currently connected + // to the node. This only represents our own view of + // the connection state (i.e. if in a different setup + // our event task is shutdown, the RPC client may remain + // connected. + fn is_connected(&self) -> bool { + self.inner.is_connected.load(Ordering::SeqCst) + } + + // Start the listener + async fn start(&self) -> Result<()> { + // we do not block the async connect() function + // as we handle the connection state in the event task + let options = ConnectOptions { block_async_connect: false, ..Default::default() }; + + // start the event processing task + self.start_event_task().await?; + + // start the RPC connection... + // this will initiate an RPC connection + // background task that will continuously + // try to connect to the given URL or query + // a URL from the resolver if one is provided. + self.client().connect(Some(options)).await?; + + Ok(()) + } + + // Stop the listener + async fn stop(&self) -> Result<()> { + // Disconnect the RPC client + self.client().disconnect().await?; + // make sure to stop the event task after + // the RPC client is disconnected to receive + // and handle disconnection events. + self.stop_event_task().await?; + Ok(()) + } + + pub fn client(&self) -> &Arc { + &self.inner.client + } + + async fn register_notification_listeners(&self) -> Result<()> { + // IMPORTANT: notification scopes are managed by the node + // for the lifetime of the RPC connection, as such they + // are "lost" if we disconnect. For that reason we must + // re-register all notification scopes when we connect. + + let listener_id = self.client().rpc_api().register_new_listener(ChannelConnection::new( + "wrpc-example-subscriber", + self.inner.notification_channel.sender.clone(), + ChannelType::Persistent, + )); + *self.inner.listener_id.lock().unwrap() = Some(listener_id); + self.client().rpc_api().start_notify(listener_id, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; + Ok(()) + } + + async fn unregister_notification_listener(&self) -> Result<()> { + let listener_id = self.inner.listener_id.lock().unwrap().take(); + if let Some(id) = listener_id { + // We do not need to unregister previously registered + // notifications as we are unregistering the entire listener. + + // If we do want to unregister individual notifications we can do: + // `self.client().rpc_api().stop_notify(listener_id, Scope:: ... ).await?;` + // for each previously registered notification scope. + + self.client().rpc_api().unregister_listener(id).await?; + } + Ok(()) + } + + // generic notification handler fn called by the event task + async fn handle_notification(&self, notification: Notification) -> Result<()> { + log_info!("Notification: {notification:?}"); + Ok(()) + } + + // generic connection handler fn called by the event task + async fn handle_connect(&self) -> Result<()> { + println!("Connected to {:?}", self.client().url()); + + // make an RPC method call to the node... + let server_info = self.client().get_server_info().await?; + log_info!("Server info: {server_info:?}"); + + // now that we have successfully connected we + // can register for notifications + self.register_notification_listeners().await?; + + // store internal state indicating that we are currently connected + self.inner.is_connected.store(true, Ordering::SeqCst); + Ok(()) + } + + // generic disconnection handler fn called by the event task + async fn handle_disconnect(&self) -> Result<()> { + println!("Disconnected from {:?}", self.client().url()); + + // Unregister notifications + self.unregister_notification_listener().await?; + + // store internal state indicating that we are currently disconnected + self.inner.is_connected.store(false, Ordering::SeqCst); + Ok(()) + } + + async fn start_event_task(&self) -> Result<()> { + // clone self for the async task + let listener = self.clone(); + + // clone the "rpc control channel" that posts notifications + // when the RPC channel is connected or disconnected + let rpc_ctl_channel = self.client().rpc_ctl().multiplexer().channel(); + + // clone our sender and receiver channels for task control + // these are obtained from the `DuplexChannel` - a pair of + // channels where sender acts as a trigger signaling termination + // and the receiver is used to signal termination completion. + // (this is a common pattern used for channel lifetime management + // in the rusty kaspa framework) + let task_ctl_receiver = self.inner.task_ctl.request.receiver.clone(); + let task_ctl_sender = self.inner.task_ctl.response.sender.clone(); + + // clone notification event channel that we provide to the RPC client + // notification subsystem to receive notifications from the node. + let notification_receiver = self.inner.notification_channel.receiver.clone(); + + spawn(async move { + loop { + select_biased! { + msg = rpc_ctl_channel.receiver.recv().fuse() => { + match msg { + Ok(msg) => { + + // handle RPC channel connection and disconnection events + match msg { + RpcState::Connected => { + if let Err(err) = listener.handle_connect().await { + log_error!("Error in connect handler: {err}"); + } + }, + RpcState::Disconnected => { + if let Err(err) = listener.handle_disconnect().await { + log_error!("Error in disconnect handler: {err}"); + } + } + } + } + Err(err) => { + // this will never occur if the RpcClient is owned and + // properly managed. This can only occur if RpcClient is + // deleted while this task is still running. + log_error!("RPC CTL channel error: {err}"); + panic!("Unexpected: RPC CTL channel closed, halting..."); + } + } + } + notification = notification_receiver.recv().fuse() => { + match notification { + Ok(notification) => { + if let Err(err) = listener.handle_notification(notification).await { + log_error!("Error while handling notification: {err}"); + } + } + Err(err) => { + panic!("RPC notification channel error: {err}"); + } + } + }, + + // we use select_biased to drain rpc_ctl + // and notifications before shutting down + // as such task_ctl is last in the poll order + _ = task_ctl_receiver.recv().fuse() => { + break; + }, + + } + } + + log_info!("Event task existing..."); + + // handle our own power down on the rpc channel that remains connected + if listener.is_connected() { + listener.handle_disconnect().await.unwrap_or_else(|err| log_error!("{err}")); + } + + // post task termination event + task_ctl_sender.send(()).await.unwrap(); + }); + Ok(()) + } + + async fn stop_event_task(&self) -> Result<()> { + self.inner.task_ctl.signal(()).await.expect("stop_event_task() signal error"); + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let listener = Listener::try_new(NetworkId::new(NetworkType::Mainnet), None)?; + + let (shutdown_sender, shutdown_receiver) = oneshot::<()>(); + + ctrlc::set_handler(move || { + log_info!("^SIGTERM - shutting down..."); + shutdown_sender.try_send(()).expect("Error sending shutdown signal..."); + }) + .expect("Unable to set the Ctrl+C signal handler"); + + listener.start().await?; + + // block until the shutdown signal is received + shutdown_receiver.recv().await.expect("Error waiting for shutdown signal..."); + + listener.stop().await?; + + Ok(()) +} diff --git a/rpc/wrpc/resolver/Cargo.toml b/rpc/wrpc/resolver/Cargo.toml new file mode 100644 index 000000000..cb28d82bf --- /dev/null +++ b/rpc/wrpc/resolver/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "kaspa-resolver" +description = "Kaspa wRPC endpoint resolver and monitor" +version.workspace = true +edition.workspace = true +authors.workspace = true +include.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] + + +ahash.workspace = true +cfg-if.workspace = true +clap.workspace = true +convert_case.workspace = true +futures.workspace = true +kaspa-consensus-core.workspace = true +kaspa-rpc-core.workspace = true +kaspa-utils.workspace = true +kaspa-wrpc-client.workspace = true +serde_json.workspace = true +serde.workspace = true +thiserror.workspace = true +tokio.workspace = true +toml.workspace = true +workflow-core.workspace = true +workflow-http.workspace = true +workflow-log.workspace = true +xxhash-rust = { workspace = true } + +# these are temporarily localized to prevent +# conflicts with other workspace dependencies +# as tower is used in gRPC-related crates. +axum = "0.7.4" +console = "0.15.8" +mime = "0.3.16" +tower = { version = "0.4.13", features = ["buffer","limit"] } +tower-http = { version = "0.5.1", features = ["cors"] } +tracing-subscriber = "0.3.18" diff --git a/rpc/wrpc/resolver/src/args.rs b/rpc/wrpc/resolver/src/args.rs new file mode 100644 index 000000000..7a526b99b --- /dev/null +++ b/rpc/wrpc/resolver/src/args.rs @@ -0,0 +1,54 @@ +pub use clap::Parser; +use std::str::FromStr; + +#[derive(Default, Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct Args { + /// HTTP server port + #[arg(long, default_value = "127.0.0.1:8888")] + pub listen: String, + + /// Optional rate limit in the form `:`, where `requests` is the number of requests allowed per specified number of `seconds` + #[arg(long = "rate-limit", value_name = "REQUESTS:SECONDS")] + pub rate_limit: Option, + + /// Verbose mode + #[arg(short, long, default_value = "false")] + pub verbose: bool, + + /// Show node data on each election + #[arg(short, long, default_value = "false")] + pub election: bool, + + /// Enable resolver status access via `/status` + #[arg(long, default_value = "false")] + pub status: bool, +} + +#[derive(Clone, Debug)] +pub struct RateLimit { + pub requests: u64, + pub period: u64, +} + +impl FromStr for RateLimit { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts = s.split_once(':'); + let (requests, period) = match parts { + None | Some(("", _)) | Some((_, "")) => { + return Err("invalid rate limit, must be `:`".to_string()); + } + Some(x) => x, + }; + let requests = requests + .parse() + .map_err(|_| format!("Unable to parse number of requests, the value must be an integer, supplied: {:?}", requests))?; + let period = period.parse().map_err(|_| { + format!("Unable to parse period, the value must be an integer specifying number of seconds, supplied: {:?}", period) + })?; + + Ok(RateLimit { requests, period }) + } +} diff --git a/rpc/wrpc/resolver/src/connection.rs b/rpc/wrpc/resolver/src/connection.rs new file mode 100644 index 000000000..75577719f --- /dev/null +++ b/rpc/wrpc/resolver/src/connection.rs @@ -0,0 +1,262 @@ +use crate::imports::*; + +const BIAS_SCALE: u64 = 1_000_000; + +#[derive(Debug, Clone)] +pub struct Descriptor { + pub connection: Arc, + pub json: String, +} + +impl From<&Arc> for Descriptor { + fn from(connection: &Arc) -> Self { + Self { connection: connection.clone(), json: serde_json::to_string(&Output::from(connection)).unwrap() } + } +} + +impl fmt::Display for Connection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: [{:>3}] {}", self.node.id_string, self.clients(), self.node.address) + } +} + +#[derive(Debug)] +pub struct Connection { + pub node: Arc, + bias: u64, + descriptor: RwLock>, + sender: Sender, + client: KaspaRpcClient, + shutdown_ctl: DuplexChannel<()>, + is_connected: Arc, + is_synced: Arc, + is_online: Arc, + clients: Arc, + args: Arc, +} + +impl Connection { + pub fn try_new(node: Arc, sender: Sender, args: &Arc) -> Result { + let client = KaspaRpcClient::new(node.encoding, Some(&node.address), None, None, None)?; + let descriptor = RwLock::default(); + let shutdown_ctl = DuplexChannel::oneshot(); + let is_connected = Arc::new(AtomicBool::new(false)); + let is_synced = Arc::new(AtomicBool::new(true)); + let is_online = Arc::new(AtomicBool::new(false)); + let clients = Arc::new(AtomicU64::new(0)); + let bias = (node.bias.unwrap_or(1.0) * BIAS_SCALE as f64) as u64; + let args = args.clone(); + Ok(Self { node, descriptor, sender, client, shutdown_ctl, is_connected, is_synced, is_online, clients, bias, args }) + } + + pub fn verbose(&self) -> bool { + self.args.verbose + } + + pub fn score(&self) -> u64 { + self.clients.load(Ordering::Relaxed) * self.bias / BIAS_SCALE + } + + pub fn connected(&self) -> bool { + self.is_connected.load(Ordering::Relaxed) + } + + pub fn online(&self) -> bool { + self.is_online.load(Ordering::Relaxed) + } + + pub fn is_synced(&self) -> bool { + self.is_synced.load(Ordering::Relaxed) + } + + pub fn clients(&self) -> u64 { + self.clients.load(Ordering::Relaxed) + } + + pub fn status(&self) -> &'static str { + if self.connected() { + if self.is_synced() { + "online" + } else { + "syncing" + } + } else { + "offline" + } + } + + pub fn descriptor(&self) -> Option { + self.descriptor.read().unwrap().clone() + } + + async fn connect(&self) -> Result<()> { + let options = ConnectOptions { block_async_connect: false, strategy: ConnectStrategy::Retry, ..Default::default() }; + + self.client.connect(Some(options)).await?; + Ok(()) + } + + async fn task(self: Arc) -> Result<()> { + self.connect().await?; + let rpc_ctl_channel = self.client.rpc_ctl().multiplexer().channel(); + let shutdown_ctl_receiver = self.shutdown_ctl.request.receiver.clone(); + let shutdown_ctl_sender = self.shutdown_ctl.response.sender.clone(); + + let interval = workflow_core::task::interval(Duration::from_secs(5)); + pin_mut!(interval); + + loop { + select! { + _ = interval.next().fuse() => { + if self.is_connected.load(Ordering::Relaxed) { + let previous = self.is_online.load(Ordering::Relaxed); + let online = self.update_metrics().await.is_ok(); + self.is_online.store(online, Ordering::Relaxed); + if online != previous { + if self.verbose() { + log_error!("Offline","{}", self.node.address); + } + self.update(online).await?; + } + } + } + + msg = rpc_ctl_channel.receiver.recv().fuse() => { + match msg { + Ok(msg) => { + + // handle wRPC channel connection and disconnection events + match msg { + RpcState::Connected => { + log_success!("Connected","{}",self.node.address); + self.is_connected.store(true, Ordering::Relaxed); + if self.update_metrics().await.is_ok() { + self.is_online.store(true, Ordering::Relaxed); + self.update(true).await?; + } else { + self.is_online.store(false, Ordering::Relaxed); + } + }, + RpcState::Disconnected => { + self.is_connected.store(false, Ordering::Relaxed); + self.is_online.store(false, Ordering::Relaxed); + self.update(false).await?; + log_error!("Disconnected","{}",self.node.address); + } + } + } + Err(err) => { + println!("Monitor: error while receiving rpc_ctl_channel message: {err}"); + break; + } + } + } + + _ = shutdown_ctl_receiver.recv().fuse() => { + break; + }, + + } + } + + shutdown_ctl_sender.send(()).await.unwrap(); + + Ok(()) + } + + pub fn start(self: &Arc) -> Result<()> { + let this = self.clone(); + spawn(async move { + if let Err(error) = this.task().await { + println!("NodeConnection task error: {:?}", error); + } + }); + + Ok(()) + } + + pub async fn stop(self: &Arc) -> Result<()> { + self.shutdown_ctl.signal(()).await.expect("NodeConnection shutdown signal error"); + Ok(()) + } + + async fn update_metrics(self: &Arc) -> Result { + match self.client.get_sync_status().await { + Ok(is_synced) => { + let previous_sync = self.is_synced.load(Ordering::Relaxed); + self.is_synced.store(is_synced, Ordering::Relaxed); + + if is_synced { + match self.client.get_metrics(false, true, false, false).await { + Ok(metrics) => { + if let Some(connection_metrics) = metrics.connection_metrics { + // update + let previous = self.clients.load(Ordering::Relaxed); + let clients = + connection_metrics.borsh_live_connections as u64 + connection_metrics.json_live_connections as u64; + self.clients.store(clients, Ordering::Relaxed); + if clients != previous { + if self.verbose() { + log_success!("Clients", "{self}"); + } + Ok(true) + } else { + Ok(false) + } + } else { + log_error!("Metrics", "{self} - failure"); + Err(Error::ConnectionMetrics) + } + } + Err(err) => { + log_error!("Metrics", "{self}"); + log_error!("RPC", "{err}"); + Err(Error::Metrics) + } + } + } else { + if is_synced != previous_sync { + log_error!("Syncing", "{self}"); + } + Err(Error::Sync) + } + } + Err(err) => { + log_error!("RPC", "{self}"); + log_error!("RPC", "{err}"); + Err(Error::Status) + } + } + } + + pub async fn update(self: &Arc, online: bool) -> Result<()> { + *self.descriptor.write().unwrap() = online.then_some(self.into()); + self.sender.try_send(self.node.params())?; + Ok(()) + } +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Output<'a> { + pub id: &'a str, + pub url: &'a str, + + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_name: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_url: Option<&'a str>, +} + +impl<'a> From<&'a Arc> for Output<'a> { + fn from(connection: &'a Arc) -> Self { + let id = connection.node.id_string.as_str(); + let url = connection.node.address.as_str(); + let provider_name = connection.node.provider.as_ref().map(|provider| provider.name.as_str()); + let provider_url = connection.node.provider.as_ref().map(|provider| provider.url.as_str()); + + // let provider_name = connection.node.provider.as_deref(); + // let provider_url = connection.node.link.as_deref(); + Self { id, url, provider_name, provider_url } + } +} diff --git a/rpc/wrpc/resolver/src/error.rs b/rpc/wrpc/resolver/src/error.rs new file mode 100644 index 000000000..4f390c3ce --- /dev/null +++ b/rpc/wrpc/resolver/src/error.rs @@ -0,0 +1,53 @@ +use kaspa_wrpc_client::error::Error as RpcError; +use thiserror::Error; +use toml::de::Error as TomlError; + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Custom(String), + + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), + + #[error("TOML error: {0}")] + Toml(#[from] TomlError), + + #[error("IO Error: {0}")] + Io(#[from] std::io::Error), + + #[error(transparent)] + Serde(#[from] serde_json::Error), + + #[error("Connection Metrics")] + ConnectionMetrics, + #[error("Metrics")] + Metrics, + #[error("Sync")] + Sync, + #[error("Status")] + Status, + + #[error("Channel send error")] + ChannelSend, + #[error("Channel try send error")] + TryChannelSend, +} + +impl Error { + pub fn custom(msg: T) -> Self { + Error::Custom(msg.to_string()) + } +} + +impl From> for Error { + fn from(_: workflow_core::channel::SendError) -> Self { + Error::ChannelSend + } +} + +impl From> for Error { + fn from(_: workflow_core::channel::TrySendError) -> Self { + Error::TryChannelSend + } +} diff --git a/rpc/wrpc/resolver/src/imports.rs b/rpc/wrpc/resolver/src/imports.rs new file mode 100644 index 000000000..29c86a481 --- /dev/null +++ b/rpc/wrpc/resolver/src/imports.rs @@ -0,0 +1,28 @@ +pub use crate::args::Args; +pub use crate::error::Error; +pub use crate::log::*; +pub use crate::node::Node; +pub use crate::params::{PathParams, QueryParams}; +pub use crate::result::Result; +pub use crate::transport::Transport; +pub use ahash::AHashMap; +pub use cfg_if::cfg_if; +pub use futures::{pin_mut, select, FutureExt, StreamExt}; +pub use kaspa_consensus_core::network::NetworkId; +pub use kaspa_rpc_core::api::ctl::RpcState; +pub use kaspa_rpc_core::api::rpc::RpcApi; +pub use kaspa_utils::hashmap::GroupExtension; +pub use kaspa_wrpc_client::{ + client::{ConnectOptions, ConnectStrategy}, + KaspaRpcClient, WrpcEncoding, +}; +pub use serde::{de::DeserializeOwned, Deserialize, Serialize}; +pub use std::collections::HashMap; +pub use std::fmt; +pub use std::path::Path; +pub use std::sync::atomic::AtomicBool; +pub use std::sync::atomic::{AtomicU64, Ordering}; +pub use std::sync::{Arc, Mutex, OnceLock, RwLock}; +pub use std::time::Duration; +pub use workflow_core::channel::*; +pub use workflow_core::task::spawn; diff --git a/rpc/wrpc/resolver/src/log.rs b/rpc/wrpc/resolver/src/log.rs new file mode 100644 index 000000000..5f66416a0 --- /dev/null +++ b/rpc/wrpc/resolver/src/log.rs @@ -0,0 +1,44 @@ +pub mod impls { + use console::style; + use std::fmt; + + pub fn log_success(source: &str, args: &fmt::Arguments<'_>) { + println!("{:>12} {}", style(source).green().bold(), args); + } + + pub fn log_warn(source: &str, args: &fmt::Arguments<'_>) { + println!("{:>12} {}", style(source).yellow().bold(), args); + } + + pub fn log_error(source: &str, args: &fmt::Arguments<'_>) { + println!("{:>12} {}", style(source).red().bold(), args); + } +} + +#[macro_export] +macro_rules! log_success { + ($target:expr, $($t:tt)*) => ( + $crate::log::impls::log_success($target, &format_args!($($t)*)) + ) +} + +pub use log_success; + +#[macro_export] +macro_rules! log_warn { + + ($target:expr, $($t:tt)*) => ( + $crate::log::impls::log_warn($target, &format_args!($($t)*)) + ) +} + +pub use log_warn; + +#[macro_export] +macro_rules! log_error { + ($target:expr, $($t:tt)*) => ( + $crate::log::impls::log_error($target, &format_args!($($t)*)) + ) +} + +pub use log_error; diff --git a/rpc/wrpc/resolver/src/main.rs b/rpc/wrpc/resolver/src/main.rs new file mode 100644 index 000000000..f071b1287 --- /dev/null +++ b/rpc/wrpc/resolver/src/main.rs @@ -0,0 +1,41 @@ +mod args; +mod connection; +mod error; +pub mod imports; +mod log; +mod monitor; +mod node; +mod panic; +mod params; +mod result; +mod server; +mod transport; + +use args::*; +use result::Result; +use std::sync::Arc; + +#[tokio::main] +async fn main() { + if let Err(error) = run().await { + eprintln!("Error: {}", error); + std::process::exit(1); + } +} + +async fn run() -> Result<()> { + let args = Arc::new(Args::parse()); + + workflow_log::set_log_level(workflow_log::LevelFilter::Info); + panic::init_ungraceful_panic_handler(); + + println!(); + println!("Kaspa wRPC Resolver v{} starting...", env!("CARGO_PKG_VERSION")); + + monitor::init(&args); + let (listener, app) = server::server(&args).await?; + monitor::start().await?; + axum::serve(listener, app).await?; + monitor::stop().await?; + Ok(()) +} diff --git a/rpc/wrpc/resolver/src/monitor.rs b/rpc/wrpc/resolver/src/monitor.rs new file mode 100644 index 000000000..748a5148f --- /dev/null +++ b/rpc/wrpc/resolver/src/monitor.rs @@ -0,0 +1,241 @@ +use crate::connection::{Connection, Descriptor}; +use crate::imports::*; + +static MONITOR: OnceLock> = OnceLock::new(); + +pub fn init(args: &Arc) { + MONITOR.set(Arc::new(Monitor::new(args))).unwrap(); +} + +pub fn monitor() -> &'static Arc { + MONITOR.get().unwrap() +} + +pub async fn start() -> Result<()> { + monitor().start().await +} + +pub async fn stop() -> Result<()> { + monitor().stop().await +} + +/// Monitor receives updates from [Connection] monitoring tasks +/// and updates the descriptors for each [Params] based on the +/// connection store (number of connections * bias). +pub struct Monitor { + args: Arc, + connections: RwLock>>>, + descriptors: RwLock>, + channel: Channel, + shutdown_ctl: DuplexChannel<()>, +} + +impl Default for Monitor { + fn default() -> Self { + Self { + args: Arc::new(Args::default()), + connections: Default::default(), + descriptors: Default::default(), + channel: Channel::unbounded(), + shutdown_ctl: DuplexChannel::oneshot(), + } + } +} + +impl fmt::Debug for Monitor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Monitor") + .field("verbose", &self.verbose()) + .field("connections", &self.connections) + .field("descriptors", &self.descriptors) + .finish() + } +} + +impl Monitor { + pub fn new(args: &Arc) -> Self { + Self { args: args.clone(), ..Default::default() } + } + + pub fn verbose(&self) -> bool { + self.args.verbose + } + + pub fn connections(&self) -> AHashMap>> { + self.connections.read().unwrap().clone() + } + + /// Process an update to `Server.toml` removing or adding node connections accordingly. + pub async fn update_nodes(&self, nodes: Vec>) -> Result<()> { + let mut connections = self.connections(); + + for params in PathParams::iter() { + let nodes = nodes.iter().filter(|node| node.params() == params).collect::>(); + + let list = connections.entry(params).or_default(); + + let create: Vec<_> = nodes.iter().filter(|node| !list.iter().any(|connection| connection.node == ***node)).collect(); + + let remove: Vec<_> = + list.iter().filter(|connection| !nodes.iter().any(|node| connection.node == **node)).cloned().collect(); + + for node in create { + let created = Arc::new(Connection::try_new((*node).clone(), self.channel.sender.clone(), &self.args)?); + created.start()?; + list.push(created); + } + + for removed in remove { + removed.stop().await?; + list.retain(|c| c.node != removed.node); + } + } + + *self.connections.write().unwrap() = connections; + + // flush all params to the update channel to refresh selected descriptors + PathParams::iter().for_each(|param| self.channel.sender.try_send(param).unwrap()); + + Ok(()) + } + + pub async fn start(self: &Arc) -> Result<()> { + let toml = std::fs::read_to_string(Path::new("Servers.toml"))?; + let nodes = crate::node::try_parse_nodes(toml.as_str())?; + + let this = self.clone(); + spawn(async move { + if let Err(error) = this.task().await { + println!("NodeConnection task error: {:?}", error); + } + }); + + self.update_nodes(nodes).await?; + + Ok(()) + } + + pub async fn stop(&self) -> Result<()> { + self.shutdown_ctl.signal(()).await.expect("Monitor shutdown signal error"); + Ok(()) + } + + async fn task(self: Arc) -> Result<()> { + let receiver = self.channel.receiver.clone(); + let shutdown_ctl_receiver = self.shutdown_ctl.request.receiver.clone(); + let shutdown_ctl_sender = self.shutdown_ctl.response.sender.clone(); + + loop { + select! { + + msg = receiver.recv().fuse() => { + match msg { + Ok(params) => { + + // run node elections + + let mut connections = self.connections() + .get(¶ms) + .expect("Monitor: expecting existing connection params") + .clone() + .into_iter() + .filter(|connection|connection.online()) + .collect::>(); + if connections.is_empty() { + self.descriptors.write().unwrap().remove(¶ms); + } else { + connections.sort_by_key(|connection| connection.score()); + + if self.args.election { + log_success!("",""); + connections.iter().for_each(|connection| { + log_warn!("Node","{}", connection); + }); + } + + if let Some(descriptor) = connections.first().unwrap().descriptor() { + let mut descriptors = self.descriptors.write().unwrap(); + + // extra debug output & monitoring + if self.args.verbose || self.args.election { + if let Some(current) = descriptors.get(¶ms) { + if current.connection.node.id != descriptor.connection.node.id { + log_success!("Election","{}", descriptor.connection); + descriptors.insert(params,descriptor); + } else { + log_success!("Keep","{}", descriptor.connection); + } + } else { + log_success!("Default","{}", descriptor.connection); + descriptors.insert(params,descriptor); + } + } else { + descriptors.insert(params,descriptor); + } + } + + if self.args.election && self.args.verbose { + log_success!("",""); + } + } + } + Err(err) => { + println!("Monitor: error while receiving update message: {err}"); + } + } + + } + _ = shutdown_ctl_receiver.recv().fuse() => { + break; + }, + + } + } + + shutdown_ctl_sender.send(()).await.unwrap(); + + Ok(()) + } + + /// Get the status of all nodes as a JSON string (available via `/status` endpoint if enabled). + pub fn get_all_json(&self) -> String { + let connections = self.connections(); + let nodes = connections.values().flatten().map(Status::from).collect::>(); + serde_json::to_string(&nodes).unwrap() + } + + /// Get JSON string representing node information (id, url, provider, link) + pub fn get_json(&self, params: &PathParams) -> Option { + self.descriptors.read().unwrap().get(params).cloned().map(|descriptor| descriptor.json) + } +} + +#[derive(Serialize)] +pub struct Status<'a> { + pub id: &'a str, + pub url: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_name: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub provider_url: Option<&'a str>, + pub transport: Transport, + pub encoding: WrpcEncoding, + pub network: NetworkId, + pub online: bool, + pub status: &'static str, +} + +impl<'a> From<&'a Arc> for Status<'a> { + fn from(connection: &'a Arc) -> Self { + let url = connection.node.address.as_str(); + let provider_name = connection.node.provider.as_ref().map(|provider| provider.name.as_str()); + let provider_url = connection.node.provider.as_ref().map(|provider| provider.url.as_str()); + let id = connection.node.id_string.as_str(); + let transport = connection.node.transport; + let encoding = connection.node.encoding; + let network = connection.node.network; + let status = connection.status(); + let online = connection.online(); + Self { id, url, provider_name, provider_url, transport, encoding, network, status, online } + } +} diff --git a/rpc/wrpc/resolver/src/node.rs b/rpc/wrpc/resolver/src/node.rs new file mode 100644 index 000000000..d0968966c --- /dev/null +++ b/rpc/wrpc/resolver/src/node.rs @@ -0,0 +1,75 @@ +use crate::imports::*; +use xxhash_rust::xxh3::xxh3_64; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Provider { + pub name: String, + pub url: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Node { + #[serde(skip)] + pub id: u64, + #[serde(skip)] + pub id_string: String, + + pub name: Option, + pub location: Option, + pub address: String, + pub transport: Transport, + pub encoding: WrpcEncoding, + pub network: NetworkId, + pub enable: Option, + pub bias: Option, + pub version: Option, + pub provider: Option, +} + +impl Eq for Node {} + +impl PartialEq for Node { + fn eq(&self, other: &Self) -> bool { + self.address == other.address + } +} + +impl std::fmt::Display for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let title = self.name.clone().unwrap_or(self.address.to_string()); + write!(f, "{}", title) + } +} + +impl Node { + pub fn params(&self) -> PathParams { + PathParams::new(self.encoding, self.network) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NodeConfig { + #[serde(rename = "node")] + nodes: Vec, +} + +pub fn try_parse_nodes(toml: &str) -> Result>> { + let nodes: Vec> = toml::from_str::(toml)? + .nodes + .into_iter() + .filter_map(|mut node| { + let id = xxh3_64(node.address.as_bytes()); + let id_string = format!("{id:x}"); + node.id = id; + node.id_string = id_string.chars().take(8).collect(); + node.enable.unwrap_or(true).then_some(node).map(Arc::new) + }) + .collect::>(); + Ok(nodes) +} + +impl AsRef for Node { + fn as_ref(&self) -> &Node { + self + } +} diff --git a/rpc/wrpc/resolver/src/panic.rs b/rpc/wrpc/resolver/src/panic.rs new file mode 100644 index 000000000..7e6d78a1f --- /dev/null +++ b/rpc/wrpc/resolver/src/panic.rs @@ -0,0 +1,10 @@ +use std::panic; + +pub fn init_ungraceful_panic_handler() { + let default_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic_info| { + default_hook(panic_info); + println!("Exiting..."); + std::process::exit(1); + })); +} diff --git a/rpc/wrpc/resolver/src/params.rs b/rpc/wrpc/resolver/src/params.rs new file mode 100644 index 000000000..7e31b69e7 --- /dev/null +++ b/rpc/wrpc/resolver/src/params.rs @@ -0,0 +1,146 @@ +use serde::{de, Deserializer, Serializer}; + +use crate::imports::*; +use std::{fmt, str::FromStr}; +// use convert_case::{Case, Casing}; + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PathParams { + pub encoding: WrpcEncoding, + pub network: NetworkId, +} + +impl PathParams { + pub fn new(encoding: WrpcEncoding, network: NetworkId) -> Self { + Self { encoding, network } + } + + pub fn iter() -> impl Iterator { + NetworkId::iter().flat_map(move |network_id| WrpcEncoding::iter().map(move |encoding| PathParams::new(*encoding, network_id))) + } +} + +impl fmt::Display for PathParams { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.encoding.to_string().to_lowercase(), self.network) + } +} + +// --- + +#[derive(Debug, Deserialize)] +pub struct QueryParams { + // Accessible via a query string like "?access=utxo-index+tx-index+block-dag+metrics+visualizer+mining" + pub access: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum AccessType { + Transact, // UTXO and TX index, submit transaction, single mempool entry + Mempool, // Full mempool data access + BlockDag, // Access to Blocks + Network, // Network data access (peers, ban, etc.) + Metrics, // Access to Metrics + Visualizer, // Access to Visualization data feeds + Mining, // Access to submit block, GBT, etc. +} + +impl AccessType { + pub fn iter() -> impl Iterator { + [ + AccessType::Transact, + AccessType::Mempool, + AccessType::BlockDag, + AccessType::Network, + AccessType::Metrics, + AccessType::Visualizer, + AccessType::Mining, + ] + .into_iter() + } +} + +impl fmt::Display for AccessType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + AccessType::Transact => "transact", + AccessType::Mempool => "mempool", + AccessType::BlockDag => "block-dag", + AccessType::Network => "network", + AccessType::Metrics => "metrics", + AccessType::Visualizer => "visualizer", + AccessType::Mining => "mining", + }; + write!(f, "{s}") + } +} + +impl FromStr for AccessType { + type Err = String; + fn from_str(s: &str) -> std::result::Result { + match s { + "transact" => Ok(AccessType::Transact), + "mempool" => Ok(AccessType::Mempool), + "block-dag" => Ok(AccessType::BlockDag), + "network" => Ok(AccessType::Network), + "metrics" => Ok(AccessType::Metrics), + "visualizer" => Ok(AccessType::Visualizer), + "mining" => Ok(AccessType::Mining), + _ => Err(format!("Invalid access type: {}", s)), + } + } +} + +#[derive(Debug, Clone)] +pub struct AccessList { + pub access: Vec, +} + +impl std::fmt::Display for AccessList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.access.iter().map(|access| access.to_string()).collect::>().join(" ")) + } +} + +impl FromStr for AccessList { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + let access = s.split(' ').map(|s| s.parse::()).collect::, _>>()?; + Ok(AccessList { access }) + } +} + +impl Serialize for AccessList { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +struct AccessListVisitor; +impl<'de> de::Visitor<'de> for AccessListVisitor { + type Value = AccessList; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string containing list of permissions separated by a '+'") + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: de::Error, + { + AccessList::from_str(value).map_err(|err| de::Error::custom(err.to_string())) + } +} + +impl<'de> Deserialize<'de> for AccessList { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(AccessListVisitor) + } +} diff --git a/rpc/wrpc/resolver/src/result.rs b/rpc/wrpc/resolver/src/result.rs new file mode 100644 index 000000000..605dc25cf --- /dev/null +++ b/rpc/wrpc/resolver/src/result.rs @@ -0,0 +1 @@ +pub type Result = std::result::Result; diff --git a/rpc/wrpc/resolver/src/server.rs b/rpc/wrpc/resolver/src/server.rs new file mode 100644 index 000000000..3717a6ebf --- /dev/null +++ b/rpc/wrpc/resolver/src/server.rs @@ -0,0 +1,149 @@ +use crate::imports::*; +use crate::monitor::monitor; +use axum::{ + async_trait, + extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Query}, + http::{header, request::Parts, HeaderValue, StatusCode}, + response::IntoResponse, + routing::get, + // Json, + Router, +}; +use tokio::net::TcpListener; + +use axum::{error_handling::HandleErrorLayer, BoxError}; +use std::time::Duration; +use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder}; +use tower_http::cors::{Any, CorsLayer}; + +pub async fn server(args: &Args) -> Result<(TcpListener, Router)> { + // initialize tracing + tracing_subscriber::fmt::init(); + + let app = Router::new().route("/v1/wrpc/:encoding/:network", get(get_elected_node)); + + let app = if args.status { + log_warn!("Routes", "Enabling `/status` route"); + app.route("/status", get(get_status_all_nodes)) + } else { + log_success!("Routes", "Disabling `/status` route"); + app + }; + + let app = if let Some(rate_limit) = args.rate_limit.as_ref() { + log_success!("Limits", "Setting rate limit to: {} requests per {} seconds", rate_limit.requests, rate_limit.period); + app.layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|err: BoxError| async move { + (StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled error: {}", err)) + })) + .layer(BufferLayer::new(1024)) + .layer(RateLimitLayer::new(rate_limit.requests, Duration::from_secs(rate_limit.period))), + ) + } else { + log_warn!("Limits", "Rate limit is disabled"); + app + }; + + let app = app.layer(CorsLayer::new().allow_origin(Any)); + + log_success!("Server", "Listening on http://{}", args.listen.as_str()); + let listener = tokio::net::TcpListener::bind(args.listen.as_str()).await.unwrap(); + Ok((listener, app)) +} + +// respond with a JSON object containing the status of all nodes +async fn get_status_all_nodes() -> impl IntoResponse { + let json = monitor().get_all_json(); + (StatusCode::OK, [(header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()))], json).into_response() +} + +// respond with a JSON object containing the elected node +async fn get_elected_node(Query(_query): Query, Path(params): Path) -> impl IntoResponse { + // println!("params: {:?}", params); + // println!("query: {:?}", query); + + if let Some(json) = monitor().get_json(¶ms) { + ([(header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()))], json).into_response() + } else { + ( + StatusCode::NOT_FOUND, + [(header::CONTENT_TYPE, HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()))], + "NOT FOUND".to_string(), + ) + .into_response() + } +} + +// We define our own `Path` extractor that customizes the error from `axum::extract::Path` +struct Path(T); + +#[async_trait] +impl FromRequestParts for Path +where + // these trait bounds are copied from `impl FromRequest for axum::extract::path::Path` + T: DeserializeOwned + Send, + S: Send + Sync, +{ + type Rejection = (StatusCode, axum::Json); + + async fn from_request_parts(parts: &mut Parts, state: &S) -> std::result::Result { + match axum::extract::Path::::from_request_parts(parts, state).await { + Ok(value) => Ok(Self(value.0)), + Err(rejection) => { + let (status, body) = match rejection { + PathRejection::FailedToDeserializePathParams(inner) => { + let mut status = StatusCode::BAD_REQUEST; + + let kind = inner.into_kind(); + let body = match &kind { + ErrorKind::WrongNumberOfParameters { .. } => PathError { message: kind.to_string(), location: None }, + + ErrorKind::ParseErrorAtKey { key, .. } => { + PathError { message: kind.to_string(), location: Some(key.clone()) } + } + + ErrorKind::ParseErrorAtIndex { index, .. } => { + PathError { message: kind.to_string(), location: Some(index.to_string()) } + } + + ErrorKind::ParseError { .. } => PathError { message: kind.to_string(), location: None }, + + ErrorKind::InvalidUtf8InPathParam { key } => { + PathError { message: kind.to_string(), location: Some(key.clone()) } + } + + ErrorKind::UnsupportedType { .. } => { + // this error is caused by the programmer using an unsupported type + // (such as nested maps) so respond with `500` instead + status = StatusCode::INTERNAL_SERVER_ERROR; + PathError { message: kind.to_string(), location: None } + } + + ErrorKind::Message(msg) => PathError { message: msg.clone(), location: None }, + + _ => PathError { message: format!("Unhandled deserialization error: {kind}"), location: None }, + }; + + (status, body) + } + PathRejection::MissingPathParams(error) => { + (StatusCode::INTERNAL_SERVER_ERROR, PathError { message: error.to_string(), location: None }) + } + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + PathError { message: format!("Unhandled path rejection: {rejection}"), location: None }, + ), + }; + + Err((status, axum::Json(body))) + } + } + } +} + +#[derive(Serialize)] +struct PathError { + message: String, + location: Option, +} diff --git a/rpc/wrpc/resolver/src/transport.rs b/rpc/wrpc/resolver/src/transport.rs new file mode 100644 index 000000000..ccfd6dee7 --- /dev/null +++ b/rpc/wrpc/resolver/src/transport.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Transport { + Grpc, + Wrpc, +} diff --git a/rpc/wrpc/wasm/Cargo.toml b/rpc/wrpc/wasm/Cargo.toml index 424f798e0..83c78d26f 100644 --- a/rpc/wrpc/wasm/Cargo.toml +++ b/rpc/wrpc/wasm/Cargo.toml @@ -9,8 +9,37 @@ include.workspace = true license.workspace = true repository.workspace = true +[features] +wasm32-sdk = ["kaspa-wrpc-client/wasm32-sdk"] +default = [] + [lib] crate-type = ["cdylib", "lib"] [dependencies] +ahash.workspace = true +async-std.workspace = true +cfg-if.workspace = true +kaspa-addresses.workspace = true +kaspa-consensus-core.workspace = true +kaspa-consensus-client.workspace = true +kaspa-consensus-wasm.workspace = true +kaspa-notify.workspace = true kaspa-wrpc-client.workspace = true +kaspa-rpc-core.workspace = true +kaspa-rpc-macros.workspace = true +kaspa-wasm-core.workspace = true +serde_json.workspace = true +serde-wasm-bindgen.workspace = true +serde.workspace = true +workflow-log.workspace = true +workflow-rpc.workspace = true +workflow-wasm.workspace = true +wasm-bindgen.workspace = true +js-sys.workspace = true +wasm-bindgen-futures.workspace = true +workflow-core.workspace = true +futures.workspace = true + +[lints.clippy] +empty_docs = "allow" diff --git a/rpc/wrpc/wasm/build-node b/rpc/wrpc/wasm/build-node index 7c8e82dc1..b126bcacf 100755 --- a/rpc/wrpc/wasm/build-node +++ b/rpc/wrpc/wasm/build-node @@ -1 +1,3 @@ -wasm-pack build --target nodejs --out-name kaspa-rpc --out-dir nodejs/kaspa-rpc +#!/bin/bash + +wasm-pack build --target nodejs --out-name kaspa-rpc --out-dir nodejs/kaspa-rpc --features wasm32-sdk diff --git a/rpc/wrpc/wasm/build-web b/rpc/wrpc/wasm/build-web index 23f0487fb..6f65f9bad 100755 --- a/rpc/wrpc/wasm/build-web +++ b/rpc/wrpc/wasm/build-web @@ -1 +1,3 @@ -wasm-pack build --target web --out-name kaspa-rpc --out-dir web/kaspa-rpc +#!/bin/bash + +wasm-pack build --target web --out-name kaspa-rpc --out-dir web/kaspa-rpc --features wasm32-sdk diff --git a/rpc/wrpc/wasm/nodejs/index.js b/rpc/wrpc/wasm/nodejs/index.js index a94263911..41964fa3e 100644 --- a/rpc/wrpc/wasm/nodejs/index.js +++ b/rpc/wrpc/wasm/nodejs/index.js @@ -1,15 +1,18 @@ // W3C WebSocket module shim globalThis.WebSocket = require('websocket').w3cwebsocket; -let {RpcClient,Encoding,init_console_panic_hook,defer} = require('./kaspa-rpc'); -// init_console_panic_hook(); +let {RpcClient,Encoding,initConsolePanicHook,defer} = require('./kaspa-rpc'); +initConsolePanicHook(); const MAX_NOTIFICATION = 10; -let URL = "ws://127.0.0.1:17110"; -let rpc = new RpcClient(Encoding.Borsh,URL); +let url = "ws://127.0.0.1:17110"; +let rpc = new RpcClient({ + url, + encoding : Encoding.Borsh, +}); (async () => { - console.log(`# connecting to ${URL}`) + console.log(`# connecting to ${url}`) await rpc.connect(); console.log(`# connected ...`) @@ -19,8 +22,8 @@ let rpc = new RpcClient(Encoding.Borsh,URL); let finish = defer(); let seq = 0; // register notification handler - await rpc.notify(async (op, payload) => { - console.log(`#${seq} - `,"op:",op,"payload:",payload); + rpc.addEventListener(async (event) => { + console.log(`#${seq} - `,"type:",event.type,"data:",event.data); seq++; if (seq == MAX_NOTIFICATION) { // await rpc.disconnect(); @@ -31,12 +34,12 @@ let rpc = new RpcClient(Encoding.Borsh,URL); // test subscription console.log("subscribing..."); - await rpc.subscribeDaaScore(); + await rpc.subscribeVirtualDaaScoreChanged(); // wait until notifier signals completion await finish; // clear notification handler - await rpc.notify(null); + await rpc.removeAllEventListeners(); // disconnect RPC interface await rpc.disconnect(); diff --git a/rpc/wrpc/wasm/src/client.rs b/rpc/wrpc/wasm/src/client.rs new file mode 100644 index 000000000..35cabd8d8 --- /dev/null +++ b/rpc/wrpc/wasm/src/client.rs @@ -0,0 +1,1052 @@ +#![allow(non_snake_case)] + +use crate::imports::*; +use crate::Resolver; +use crate::{RpcEventCallback, RpcEventType, RpcEventTypeOrCallback}; +use js_sys::{Function, Object}; +use kaspa_addresses::{Address, AddressOrStringArrayT}; +use kaspa_consensus_client::UtxoEntryReference; +use kaspa_consensus_core::network::{NetworkType, NetworkTypeT}; +use kaspa_notify::connection::ChannelType; +use kaspa_notify::events::EventType; +use kaspa_notify::listener; +use kaspa_notify::notification::Notification as NotificationT; +use kaspa_rpc_core::api::ctl; +pub use kaspa_rpc_core::wasm::message::*; +pub use kaspa_rpc_macros::{ + build_wrpc_wasm_bindgen_interface, build_wrpc_wasm_bindgen_subscriptions, declare_typescript_wasm_interface as declare, +}; +use kaspa_wasm_core::events::{get_event_targets, Sink}; +pub use serde_wasm_bindgen::from_value; +use workflow_rpc::client::Ctl; +pub use workflow_rpc::client::IConnectOptions; +pub use workflow_rpc::encoding::Encoding as WrpcEncoding; +use workflow_wasm::callback; +use workflow_wasm::extensions::ObjectExtension; +pub use workflow_wasm::serde::to_value; + +declare! { + IRpcConfig, + r#" + /** + * RPC client configuration options + * + * @category Node RPC + */ + export interface IRpcConfig { + /** + * An instance of the {@link Resolver} class to use for an automatic public node lookup. + * If supplying a resolver, the `url` property is ignored. + */ + resolver? : Resolver, + /** + * URL for wRPC node endpoint + */ + url?: string; + /** + * RPC encoding: `borsh` or `json` (default is `borsh`) + */ + encoding?: Encoding; + /** + * Network identifier: `mainnet`, `testnet-10` etc. + * `networkId` is required when using a resolver. + */ + networkId?: NetworkId | string; + } + "#, +} + +pub struct RpcConfig { + pub resolver: Option, + pub url: Option, + pub encoding: Option, + pub network_id: Option, +} + +impl Default for RpcConfig { + fn default() -> Self { + RpcConfig { url: None, encoding: Some(Encoding::Borsh), network_id: None, resolver: None } + } +} + +impl TryFrom for RpcConfig { + type Error = Error; + fn try_from(config: IRpcConfig) -> Result { + let resolver = config.try_get::("resolver")?; + let url = config.try_get_string("url")?; + let encoding = config.try_get::("encoding")?; + let network_id = config.try_get::("networkId")?; + + if resolver.is_some() && network_id.is_none() { + return Err(Error::custom("networkId is required when using a resolver")); + } + + Ok(RpcConfig { resolver, url, encoding, network_id }) + } +} + +impl TryFrom for IRpcConfig { + type Error = Error; + fn try_from(config: RpcConfig) -> Result { + let object = IRpcConfig::default(); + object.set("resolver", &config.resolver.into())?; + object.set("url", &config.url.into())?; + object.set("encoding", &config.encoding.into())?; + object.set("networkId", &config.network_id.into())?; + Ok(object) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +enum NotificationEvent { + All, + Notification(EventType), + RpcCtl(Ctl), +} + +impl FromStr for NotificationEvent { + type Err = Error; + fn from_str(s: &str) -> Result { + if s == "*" { + Ok(NotificationEvent::All) + } else if let Ok(ctl) = Ctl::from_str(s) { + Ok(NotificationEvent::RpcCtl(ctl)) + } else if let Ok(event) = EventType::from_str(s) { + Ok(NotificationEvent::Notification(event)) + } else { + Err(Error::custom(format!("Invalid notification event type: `{}`", s))) + } + } +} + +impl TryFrom for NotificationEvent { + type Error = Error; + fn try_from(event: JsValue) -> Result { + if let Some(event) = event.as_string() { + event.parse() + } else { + Err(Error::custom(format!("Invalid notification event: `{:?}`", event))) + } + } +} + +pub struct Inner { + client: Arc, + resolver: Option, + notification_task: AtomicBool, + notification_ctl: DuplexChannel, + callbacks: Arc>>>, + listener_id: Arc>>, + notification_channel: Channel, +} + +impl Inner { + fn notification_callbacks(&self, event: NotificationEvent) -> Option> { + let notification_callbacks = self.callbacks.lock().unwrap(); + let all = notification_callbacks.get(&NotificationEvent::All).cloned(); + let target = notification_callbacks.get(&event).cloned(); + match (all, target) { + (Some(mut vec_all), Some(vec_target)) => { + vec_all.extend(vec_target); + Some(vec_all) + } + (Some(vec_all), None) => Some(vec_all), + (None, Some(vec_target)) => Some(vec_target), + (None, None) => None, + } + } +} + +/// +/// +/// Kaspa RPC client uses ([wRPC](https://github.com/workflow-rs/workflow-rs/tree/master/rpc)) +/// interface to connect directly with Kaspa Node. wRPC supports +/// two types of encodings: `borsh` (binary, default) and `json`. +/// +/// There are two ways to connect: Directly to any Kaspa Node or to a +/// community-maintained public node infrastructure using the {@link Resolver} class. +/// +/// **Connecting to a public node using a resolver** +/// +/// ```javascript +/// let rpc = new RpcClient({ +/// resolver : new Resolver(), +/// networkId : "mainnet", +/// }); +/// +/// await rpc.connect(); +/// ``` +/// +/// **Connecting to a Kaspa Node directly** +/// +/// ```javascript +/// let rpc = new RpcClient({ +/// // if port is not provided it will default +/// // to the default port for the networkId +/// url : "127.0.0.1", +/// networkId : "mainnet", +/// }); +/// ``` +/// +/// **Example usage** +/// +/// ```javascript +/// +/// // Create a new RPC client with a URL +/// let rpc = new RpcClient({ url : "wss://" }); +/// +/// // Create a new RPC client with a resolver +/// // (networkId is required when using a resolver) +/// let rpc = new RpcClient({ +/// resolver : new Resolver(), +/// networkId : "mainnet", +/// }); +/// +/// rpc.addEventListener("connect", async (event) => { +/// console.log("Connected to", rpc.url); +/// await rpc.subscribeDaaScore(); +/// }); +/// +/// rpc.addEventListener("disconnect", (event) => { +/// console.log("Disconnected from", rpc.url); +/// }); +/// +/// try { +/// await rpc.connect(); +/// } catch(err) { +/// console.log("Error connecting:", err); +/// } +/// +/// ``` +/// +/// You can register event listeners to receive notifications from the RPC client +/// using {@link RpcClient.addEventListener} and {@link RpcClient.removeEventListener} functions. +/// +/// **IMPORTANT:** If RPC is disconnected, upon reconnection you do not need +/// to re-register event listeners, but your have to re-subscribe for Kaspa node +/// notifications: +/// +/// ```typescript +/// rpc.addEventListener("connect", async (event) => { +/// console.log("Connected to", rpc.url); +/// // re-subscribe each time we connect +/// await rpc.subscribeDaaScore(); +/// // ... perform wallet address subscriptions +/// }); +/// +/// ``` +/// +/// If using NodeJS, it is important that {@link RpcClient.disconnect} is called before +/// the process exits to ensure that the WebSocket connection is properly closed. +/// Failure to do this will prevent the process from exiting. +/// +/// @category Node RPC +/// +#[wasm_bindgen(inspectable)] +#[derive(Clone, CastFromJs)] +pub struct RpcClient { + // #[wasm_bindgen(skip)] + pub(crate) inner: Arc, +} + +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + #[wasm_bindgen(typescript_custom_section)] + const TS_NOTIFY: &'static str = r#" + interface RpcClient { + /** + * @param {RpcEventCallback} callback + */ + addEventListener(callback:RpcEventCallback): void; + /** + * @param {RpcEventType} event + * @param {RpcEventCallback} [callback] + */ + addEventListener( + event: M, + callback: (eventData: RpcEventMap[M]) => void + ) + }"#; + } +} + +impl RpcClient { + pub fn new(config: Option) -> Result { + let RpcConfig { resolver, url, encoding, network_id } = config.unwrap_or_default(); + + let encoding = encoding.unwrap_or(Encoding::Borsh); + + let url = url + .map( + |url| { + if let Some(network_id) = network_id { + Self::parse_url(&url, encoding, network_id) + } else { + Ok(url.to_string()) + } + }, + ) + .transpose()?; + + let client = Arc::new( + KaspaRpcClient::new(encoding, url.as_deref(), resolver.clone().map(Into::into), network_id, None) + .unwrap_or_else(|err| panic!("{err}")), + ); + + let rpc_client = RpcClient { + inner: Arc::new(Inner { + client, + resolver, + notification_task: AtomicBool::new(false), + notification_ctl: DuplexChannel::oneshot(), + callbacks: Arc::new(Default::default()), + listener_id: Arc::new(Mutex::new(None)), + notification_channel: Channel::unbounded(), + }), + }; + + Ok(rpc_client) + } +} + +#[wasm_bindgen] +impl RpcClient { + /// + /// Create a new RPC client with optional {@link Encoding} and a `url`. + /// + /// @see {@link IRpcConfig} interface for more details. + /// + #[wasm_bindgen(constructor)] + pub fn ctor(config: Option) -> Result { + Self::new(config.map(RpcConfig::try_from).transpose()?) + } + + /// The current URL of the RPC client. + #[wasm_bindgen(getter)] + pub fn url(&self) -> Option { + self.inner.client.url() + } + + /// Current rpc resolver + #[wasm_bindgen(getter)] + pub fn resolver(&self) -> Option { + self.inner.resolver.clone() + } + + /// Set the resolver for the RPC client. + /// This setting will take effect on the next connection. + #[wasm_bindgen(js_name = setResolver)] + pub fn set_resolver(&self, resolver: Resolver) -> Result<()> { + self.inner.client.set_resolver(resolver.into())?; + Ok(()) + } + + /// Set the network id for the RPC client. + /// This setting will take effect on the next connection. + #[wasm_bindgen(js_name = setNetworkId)] + pub fn set_network_id(&self, network_id: &NetworkId) -> Result<()> { + self.inner.client.set_network_id(network_id)?; + Ok(()) + } + + /// The current connection status of the RPC client. + #[wasm_bindgen(getter, js_name = "isConnected")] + pub fn is_connected(&self) -> bool { + self.inner.client.is_connected() + } + + /// The current protocol encoding. + #[wasm_bindgen(getter, js_name = "encoding")] + pub fn encoding(&self) -> String { + self.inner.client.encoding().to_string() + } + + /// Optional: Resolver node id. + #[wasm_bindgen(getter, js_name = "nodeId")] + pub fn resolver_node_id(&self) -> Option { + self.inner.client.node_descriptor().map(|node| node.id.clone()) + } + + /// Optional: public node provider name. + #[wasm_bindgen(getter, js_name = "providerName")] + pub fn resolver_node_provider_name(&self) -> Option { + self.inner.client.node_descriptor().and_then(|node| node.provider_name.clone()) + } + + /// Optional: public node provider URL. + #[wasm_bindgen(getter, js_name = "providerUrl")] + pub fn resolver_node_provider_url(&self) -> Option { + self.inner.client.node_descriptor().and_then(|node| node.provider_url.clone()) + } + + /// Connect to the Kaspa RPC server. This function starts a background + /// task that connects and reconnects to the server if the connection + /// is terminated. Use [`disconnect()`](Self::disconnect()) to + /// terminate the connection. + /// @see {@link IConnectOptions} interface for more details. + pub async fn connect(&self, args: Option) -> Result<()> { + let options = args.map(ConnectOptions::try_from).transpose()?; + + self.start_notification_task()?; + self.inner.client.connect(options).await?; + + Ok(()) + } + + /// Disconnect from the Kaspa RPC server. + pub async fn disconnect(&self) -> Result<()> { + // disconnect the client first to receive the 'close' event + self.inner.client.disconnect().await?; + self.stop_notification_task().await?; + Ok(()) + } + + /// Start background RPC services (automatically started when invoking {@link RpcClient.connect}). + pub async fn start(&self) -> Result<()> { + self.start_notification_task()?; + self.inner.client.start().await?; + Ok(()) + } + + /// Stop background RPC services (automatically stopped when invoking {@link RpcClient.disconnect}). + pub async fn stop(&self) -> Result<()> { + self.inner.client.stop().await?; + self.stop_notification_task().await?; + Ok(()) + } + + /// Triggers a disconnection on the underlying WebSocket + /// if the WebSocket is in connected state. + /// This is intended for debug purposes only. + /// Can be used to test application reconnection logic. + #[wasm_bindgen(js_name = "triggerAbort")] + pub fn trigger_abort(&self) { + self.inner.client.trigger_abort().ok(); + } + + /// + /// Register an event listener callback. + /// + /// Registers a callback function to be executed when a specific event occurs. + /// The callback function will receive an {@link RpcEvent} object with the event `type` and `data`. + /// + /// **RPC Subscriptions vs Event Listeners** + /// + /// Subscriptions are used to receive notifications from the RPC client. + /// Event listeners are client-side application registrations that are + /// triggered when notifications are received. + /// + /// If node is disconnected, upon reconnection you do not need to re-register event listeners, + /// however, you have to re-subscribe for Kaspa node notifications. As such, it is recommended + /// to register event listeners when the RPC `open` event is received. + /// + /// ```javascript + /// rpc.addEventListener("connect", async (event) => { + /// console.log("Connected to", rpc.url); + /// await rpc.subscribeDaaScore(); + /// // ... perform wallet address subscriptions + /// }); + /// ``` + /// + /// **Multiple events and listeners** + /// + /// `addEventListener` can be used to register multiple event listeners for the same event + /// as well as the same event listener for multiple events. + /// + /// ```javascript + /// // Registering a single event listener for multiple events: + /// rpc.addEventListener(["connect", "disconnect"], (event) => { + /// console.log(event); + /// }); + /// + /// // Registering event listener for all events: + /// // (by omitting the event type) + /// rpc.addEventListener((event) => { + /// console.log(event); + /// }); + /// + /// // Registering multiple event listeners for the same event: + /// rpc.addEventListener("connect", (event) => { // first listener + /// console.log(event); + /// }); + /// rpc.addEventListener("connect", (event) => { // second listener + /// console.log(event); + /// }); + /// ``` + /// + /// **Use of context objects** + /// + /// You can also register an event with a `context` object. When the event is triggered, + /// the `handleEvent` method of the `context` object will be called while `this` value + /// will be set to the `context` object. + /// ```javascript + /// // Registering events with a context object: + /// + /// const context = { + /// someProperty: "someValue", + /// handleEvent: (event) => { + /// // the following will log "someValue" + /// console.log(this.someProperty); + /// console.log(event); + /// } + /// }; + /// rpc.addEventListener(["connect","disconnect"], context); + /// + /// ``` + /// + /// **General use examples** + /// + /// In TypeScript you can use {@link RpcEventType} enum (such as `RpcEventType.Connect`) + /// or `string` (such as "connect") to register event listeners. + /// In JavaScript you can only use `string`. + /// + /// ```typescript + /// // Example usage (TypeScript): + /// + /// rpc.addEventListener(RpcEventType.Connect, (event) => { + /// console.log("Connected to", rpc.url); + /// }); + /// + /// rpc.addEventListener(RpcEventType.VirtualDaaScoreChanged, (event) => { + /// console.log(event.type,event.data); + /// }); + /// await rpc.subscribeDaaScore(); + /// + /// rpc.addEventListener(RpcEventType.BlockAdded, (event) => { + /// console.log(event.type,event.data); + /// }); + /// await rpc.subscribeBlockAdded(); + /// + /// // Example usage (JavaScript): + /// + /// rpc.addEventListener("virtual-daa-score-changed", (event) => { + /// console.log(event.type,event.data); + /// }); + /// + /// await rpc.subscribeDaaScore(); + /// rpc.addEventListener("block-added", (event) => { + /// console.log(event.type,event.data); + /// }); + /// await rpc.subscribeBlockAdded(); + /// ``` + /// + /// @see {@link RpcEventType} for a list of supported events. + /// @see {@link RpcEventData} for the event data interface specification. + /// @see {@link RpcClient.removeEventListener}, {@link RpcClient.removeAllEventListeners} + /// + #[wasm_bindgen(js_name = "addEventListener", skip_typescript)] + pub fn add_event_listener(&self, event: RpcEventTypeOrCallback, callback: Option) -> Result<()> { + if let Ok(sink) = Sink::try_from(&event) { + let event = NotificationEvent::All; + self.inner.callbacks.lock().unwrap().entry(event).or_default().push(sink); + Ok(()) + } else if let Some(Ok(sink)) = callback.map(Sink::try_from) { + let targets: Vec = get_event_targets(event)?; + for event in targets { + self.inner.callbacks.lock().unwrap().entry(event).or_default().push(sink.clone()); + } + Ok(()) + } else { + Err(Error::custom("Invalid event listener callback")) + } + } + + /// + /// Unregister an event listener. + /// This function will remove the callback for the specified event. + /// If the `callback` is not supplied, all callbacks will be + /// removed for the specified event. + /// + /// @see {@link RpcClient.addEventListener} + #[wasm_bindgen(js_name = "removeEventListener")] + pub fn remove_event_listener(&self, event: RpcEventType, callback: Option) -> Result<()> { + let mut callbacks = self.inner.callbacks.lock().unwrap(); + if let Ok(sink) = Sink::try_from(&event) { + // remove callback from all events + for (_, handlers) in callbacks.iter_mut() { + handlers.retain(|handler| handler != &sink); + } + } else if let Some(Ok(sink)) = callback.map(Sink::try_from) { + // remove callback from specific events + let targets: Vec = get_event_targets(event)?; + for target in targets.into_iter() { + callbacks.entry(target).and_modify(|handlers| { + handlers.retain(|handler| handler != &sink); + }); + } + } else { + // remove all callbacks for the event + let targets: Vec = get_event_targets(event)?; + for event in targets { + callbacks.remove(&event); + } + } + Ok(()) + } + + /// + /// Unregister a single event listener callback from all events. + /// + /// + /// + #[wasm_bindgen(js_name = "clearEventListener")] + pub fn clear_event_listener(&self, callback: RpcEventCallback) -> Result<()> { + let sink = Sink::new(callback); + let mut notification_callbacks = self.inner.callbacks.lock().unwrap(); + for (_, handlers) in notification_callbacks.iter_mut() { + handlers.retain(|handler| handler != &sink); + } + Ok(()) + } + + /// + /// Unregister all notification callbacks for all events. + /// + #[wasm_bindgen(js_name = "removeAllEventListeners")] + pub fn remove_all_event_listeners(&self) -> Result<()> { + *self.inner.callbacks.lock().unwrap() = Default::default(); + Ok(()) + } +} + +impl RpcClient { + pub fn new_with_rpc_client(client: Arc) -> RpcClient { + let resolver = client.resolver().map(Into::into); + RpcClient { + inner: Arc::new(Inner { + client, + resolver, + notification_task: AtomicBool::new(false), + notification_ctl: DuplexChannel::oneshot(), + callbacks: Arc::new(Mutex::new(Default::default())), + listener_id: Arc::new(Mutex::new(None)), + notification_channel: Channel::unbounded(), + }), + } + } + + pub fn listener_id(&self) -> Option { + *self.inner.listener_id.lock().unwrap() + } + + pub fn client(&self) -> &Arc { + &self.inner.client + } + + async fn stop_notification_task(&self) -> Result<()> { + if self.inner.notification_task.load(Ordering::SeqCst) { + self.inner.notification_ctl.signal(()).await.map_err(|err| JsError::new(&err.to_string()))?; + self.inner.notification_task.store(false, Ordering::SeqCst); + } + Ok(()) + } + + /// Notification task receives notifications and executes them on the + /// user-supplied callback function. + fn start_notification_task(&self) -> Result<()> { + if self.inner.notification_task.load(Ordering::SeqCst) { + return Ok(()); + } + + self.inner.notification_task.store(true, Ordering::SeqCst); + + let ctl_receiver = self.inner.notification_ctl.request.receiver.clone(); + let ctl_sender = self.inner.notification_ctl.response.sender.clone(); + let notification_receiver = self.inner.notification_channel.receiver.clone(); + let ctl_multiplexer_channel = + self.inner.client.rpc_client().ctl_multiplexer().as_ref().expect("WASM32 RpcClient ctl_multiplexer is None").channel(); + let this = self.clone(); + + spawn(async move { + loop { + select_biased! { + msg = ctl_multiplexer_channel.recv().fuse() => { + if let Ok(ctl) = msg { + + match ctl { + Ctl::Connect => { + let listener_id = this.inner.client.register_new_listener(ChannelConnection::new( + "kaspa-wrpc-client-wasm", + this.inner.notification_channel.sender.clone(), + ChannelType::Persistent, + )); + *this.inner.listener_id.lock().unwrap() = Some(listener_id); + } + Ctl::Disconnect => { + let listener_id = this.inner.listener_id.lock().unwrap().take(); + if let Some(listener_id) = listener_id { + if let Err(err) = this.inner.client.unregister_listener(listener_id).await { + log_error!("Error in unregister_listener: {:?}",err); + } + } + } + } + + let event = NotificationEvent::RpcCtl(ctl); + if let Some(handlers) = this.inner.notification_callbacks(event) { + for handler in handlers.into_iter() { + let event = Object::new(); + event.set("type", &ctl.to_string().into()).ok(); + event.set("rpc", &this.clone().into()).ok(); + if let Err(err) = handler.call(&event.into()) { + log_error!("Error while executing RPC notification callback: {:?}",err); + } + } + } + } + }, + msg = notification_receiver.recv().fuse() => { + if let Ok(notification) = &msg { + match ¬ification { + kaspa_rpc_core::Notification::UtxosChanged(utxos_changed_notification) => { + + let event_type = EventType::UtxosChanged; + let notification_event = NotificationEvent::Notification(event_type); + if let Some(handlers) = this.inner.notification_callbacks(notification_event) { + + let UtxosChangedNotification { added, removed } = utxos_changed_notification; + let added = js_sys::Array::from_iter(added.iter().map(UtxoEntryReference::from).map(JsValue::from)); + let removed = js_sys::Array::from_iter(removed.iter().map(UtxoEntryReference::from).map(JsValue::from)); + let notification = Object::new(); + notification.set("added", &added).unwrap(); + notification.set("removed", &removed).unwrap(); + + for handler in handlers.into_iter() { + let event = Object::new(); + let event_type_value = to_value(&event_type).unwrap(); + event.set("type", &event_type_value).expect("setting event type"); + event.set("data", ¬ification).expect("setting event data"); + if let Err(err) = handler.call(&event.into()) { + log_error!("Error while executing RPC notification callback: {:?}",err); + } + } + } + }, + _ => { + let event_type = notification.event_type(); + let notification_event = NotificationEvent::Notification(event_type); + if let Some(handlers) = this.inner.notification_callbacks(notification_event) { + for handler in handlers.into_iter() { + let event = Object::new(); + let event_type_value = to_value(&event_type).unwrap(); + event.set("type", &event_type_value).expect("setting event type"); + event.set("data", ¬ification.to_value().unwrap()).expect("setting event data"); + if let Err(err) = handler.call(&event.into()) { + log_error!("Error while executing RPC notification callback: {:?}",err); + } + } + } + } + } + } + } + _ = ctl_receiver.recv().fuse() => { + break; + }, + + } + } + + if let Some(listener_id) = this.listener_id() { + this.inner.listener_id.lock().unwrap().take(); + if let Err(err) = this.inner.client.unregister_listener(listener_id).await { + log_error!("Error in unregister_listener: {:?}", err); + } + } + + ctl_sender.send(()).await.ok(); + }); + + Ok(()) + } +} + +#[wasm_bindgen] +impl RpcClient { + #[wasm_bindgen(js_name = "defaultPort")] + pub fn default_port(encoding: WrpcEncoding, network: &NetworkTypeT) -> Result { + let network_type = NetworkType::try_from(network)?; + match encoding { + WrpcEncoding::Borsh => Ok(network_type.default_borsh_rpc_port()), + WrpcEncoding::SerdeJson => Ok(network_type.default_json_rpc_port()), + } + } + + /// Constructs an WebSocket RPC URL given the partial URL or an IP, RPC encoding + /// and a network type. + /// + /// # Arguments + /// + /// * `url` - Partial URL or an IP address + /// * `encoding` - RPC encoding + /// * `network_type` - Network type + /// + #[wasm_bindgen(js_name = parseUrl)] + pub fn parse_url(url: &str, encoding: Encoding, network: NetworkId) -> Result { + let url_ = KaspaRpcClient::parse_url(url.to_string(), encoding, network.into())?; + Ok(url_) + } +} + +#[wasm_bindgen] +impl RpcClient { + /// Manage subscription for a virtual DAA score changed notification event. + /// Virtual DAA score changed notification event is produced when the virtual + /// Difficulty Adjustment Algorithm (DAA) score changes in the Kaspa BlockDAG. + #[wasm_bindgen(js_name = subscribeVirtualDaaScoreChanged)] + pub async fn subscribe_daa_score(&self) -> Result<()> { + if let Some(listener_id) = self.listener_id() { + self.inner.client.stop_notify(listener_id, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; + } else { + log_error!("RPC unsubscribe on a closed connection"); + } + Ok(()) + } + + /// Manage subscription for a virtual DAA score changed notification event. + /// Virtual DAA score changed notification event is produced when the virtual + /// Difficulty Adjustment Algorithm (DAA) score changes in the Kaspa BlockDAG. + #[wasm_bindgen(js_name = unsubscribeVirtualDaaScoreChanged)] + pub async fn unsubscribe_daa_score(&self) -> Result<()> { + if let Some(listener_id) = self.listener_id() { + self.inner.client.stop_notify(listener_id, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; + } else { + log_error!("RPC unsubscribe on a closed connection"); + } + Ok(()) + } + + /// Subscribe for a UTXOs changed notification event. + /// UTXOs changed notification event is produced when the set + /// of unspent transaction outputs (UTXOs) changes in the + /// Kaspa BlockDAG. The event notification will be scoped to the + /// provided list of addresses. + #[wasm_bindgen(js_name = subscribeUtxosChanged)] + pub async fn subscribe_utxos_changed(&self, addresses: AddressOrStringArrayT) -> Result<()> { + if let Some(listener_id) = self.listener_id() { + let addresses: Vec
= addresses.try_into()?; + self.inner.client.start_notify(listener_id, Scope::UtxosChanged(UtxosChangedScope { addresses })).await?; + } else { + log_error!("RPC subscribe on a closed connection"); + } + + Ok(()) + } + + /// Unsubscribe from UTXOs changed notification event + /// for a specific set of addresses. + #[wasm_bindgen(js_name = unsubscribeUtxosChanged)] + pub async fn unsubscribe_utxos_changed(&self, addresses: AddressOrStringArrayT) -> Result<()> { + if let Some(listener_id) = self.listener_id() { + let addresses: Vec
= addresses.try_into()?; + self.inner.client.stop_notify(listener_id, Scope::UtxosChanged(UtxosChangedScope { addresses })).await?; + } else { + log_error!("RPC unsubscribe on a closed connection"); + } + Ok(()) + } + + // TODO: scope variant with field functions + + /// Manage subscription for a virtual chain changed notification event. + /// Virtual chain changed notification event is produced when the virtual + /// chain changes in the Kaspa BlockDAG. + #[wasm_bindgen(js_name = subscribeVirtualChainChanged)] + pub async fn subscribe_virtual_chain_changed(&self, include_accepted_transaction_ids: bool) -> Result<()> { + if let Some(listener_id) = self.listener_id() { + self.inner + .client + .start_notify(listener_id, Scope::VirtualChainChanged(VirtualChainChangedScope { include_accepted_transaction_ids })) + .await?; + } else { + log_error!("RPC subscribe on a closed connection"); + } + Ok(()) + } + + /// Manage subscription for a virtual chain changed notification event. + /// Virtual chain changed notification event is produced when the virtual + /// chain changes in the Kaspa BlockDAG. + #[wasm_bindgen(js_name = unsubscribeVirtualChainChanged)] + pub async fn unsubscribe_virtual_chain_changed(&self, include_accepted_transaction_ids: bool) -> Result<()> { + if let Some(listener_id) = self.listener_id() { + self.inner + .client + .stop_notify(listener_id, Scope::VirtualChainChanged(VirtualChainChangedScope { include_accepted_transaction_ids })) + .await?; + } else { + log_error!("RPC unsubscribe on a closed connection"); + } + Ok(()) + } +} + +// Build subscribe functions +build_wrpc_wasm_bindgen_subscriptions!([ + // Manually implemented subscriptions (above) + // - VirtualChainChanged, // can't used this here due to non-C-style enum variant + // - UtxosChanged, // can't used this here due to non-C-style enum variant + // - VirtualDaaScoreChanged, + /// Manage subscription for a block added notification event. + /// Block added notification event is produced when a new + /// block is added to the Kaspa BlockDAG. + BlockAdded, + /// Manage subscription for a finality conflict notification event. + /// Finality conflict notification event is produced when a finality + /// conflict occurs in the Kaspa BlockDAG. + FinalityConflict, + // TODO provide better description + /// Manage subscription for a finality conflict resolved notification event. + /// Finality conflict resolved notification event is produced when a finality + /// conflict in the Kaspa BlockDAG is resolved. + FinalityConflictResolved, + /// Manage subscription for a sink blue score changed notification event. + /// Sink blue score changed notification event is produced when the blue + /// score of the sink block changes in the Kaspa BlockDAG. + SinkBlueScoreChanged, + /// Manage subscription for a pruning point UTXO set override notification event. + /// Pruning point UTXO set override notification event is produced when the + /// UTXO set override for the pruning point changes in the Kaspa BlockDAG. + PruningPointUtxoSetOverride, + /// Manage subscription for a new block template notification event. + /// New block template notification event is produced when a new block + /// template is generated for mining in the Kaspa BlockDAG. + NewBlockTemplate, +]); + +// Build RPC method invocation functions. This macro +// takes two lists. First list is for functions that +// do not have arguments and the second one is for +// functions that have a single argument (request). + +build_wrpc_wasm_bindgen_interface!( + [ + // functions with optional arguments + // they are specified as Option + // which map as `request? : IXxxRequest` in typescript + /// Retrieves the current number of blocks in the Kaspa BlockDAG. + /// This is not a block count, not a "block height" and can not be + /// used for transaction validation. + /// Returned information: Current block count. + GetBlockCount, + /// Provides information about the Directed Acyclic Graph (DAG) + /// structure of the Kaspa BlockDAG. + /// Returned information: Number of blocks in the DAG, + /// number of tips in the DAG, hash of the selected parent block, + /// difficulty of the selected parent block, selected parent block + /// blue score, selected parent block time. + GetBlockDagInfo, + /// Returns the total current coin supply of Kaspa network. + /// Returned information: Total coin supply. + GetCoinSupply, + /// Retrieves information about the peers connected to the Kaspa node. + /// Returned information: Peer ID, IP address and port, connection + /// status, protocol version. + GetConnectedPeerInfo, + /// Retrieves general information about the Kaspa node. + /// Returned information: Version of the Kaspa node, protocol + /// version, network identifier. + /// This call is primarily used by gRPC clients. + /// For wRPC clients, use {@link RpcClient.getServerInfo}. + GetInfo, + /// Provides a list of addresses of known peers in the Kaspa + /// network that the node can potentially connect to. + /// Returned information: List of peer addresses. + GetPeerAddresses, + /// Retrieves various metrics and statistics related to the + /// performance and status of the Kaspa node. + /// Returned information: Memory usage, CPU usage, network activity. + GetMetrics, + /// Retrieves the current sink block, which is the block with + /// the highest cumulative difficulty in the Kaspa BlockDAG. + /// Returned information: Sink block hash, sink block height. + GetSink, + /// Returns the blue score of the current sink block, indicating + /// the total amount of work that has been done on the main chain + /// leading up to that block. + /// Returned information: Blue score of the sink block. + GetSinkBlueScore, + /// Tests the connection and responsiveness of a Kaspa node. + /// Returned information: None. + Ping, + /// Gracefully shuts down the Kaspa node. + /// Returned information: None. + Shutdown, + /// Retrieves information about the Kaspa server. + /// Returned information: Version of the Kaspa server, protocol + /// version, network identifier. + GetServerInfo, + /// Obtains basic information about the synchronization status of the Kaspa node. + /// Returned information: Syncing status. + GetSyncStatus, + ], + [ + // functions with `request` argument + /// Adds a peer to the Kaspa node's list of known peers. + /// Returned information: None. + AddPeer, + /// Bans a peer from connecting to the Kaspa node for a specified duration. + /// Returned information: None. + Ban, + /// Estimates the network's current hash rate in hashes per second. + /// Returned information: Estimated network hashes per second. + EstimateNetworkHashesPerSecond, + /// Retrieves the balance of a specific address in the Kaspa BlockDAG. + /// Returned information: Balance of the address. + GetBalanceByAddress, + /// Retrieves balances for multiple addresses in the Kaspa BlockDAG. + /// Returned information: Balances of the addresses. + GetBalancesByAddresses, + /// Retrieves a specific block from the Kaspa BlockDAG. + /// Returned information: Block information. + GetBlock, + /// Retrieves multiple blocks from the Kaspa BlockDAG. + /// Returned information: List of block information. + GetBlocks, + /// Generates a new block template for mining. + /// Returned information: Block template information. + GetBlockTemplate, + /// Retrieves the estimated DAA (Difficulty Adjustment Algorithm) + /// score timestamp estimate. + /// Returned information: DAA score timestamp estimate. + GetDaaScoreTimestampEstimate, + /// Retrieves the current network configuration. + /// Returned information: Current network configuration. + GetCurrentNetwork, + /// Retrieves block headers from the Kaspa BlockDAG. + /// Returned information: List of block headers. + GetHeaders, + /// Retrieves mempool entries from the Kaspa node's mempool. + /// Returned information: List of mempool entries. + GetMempoolEntries, + /// Retrieves mempool entries associated with specific addresses. + /// Returned information: List of mempool entries. + GetMempoolEntriesByAddresses, + /// Retrieves a specific mempool entry by transaction ID. + /// Returned information: Mempool entry information. + GetMempoolEntry, + /// Retrieves information about a subnetwork in the Kaspa BlockDAG. + /// Returned information: Subnetwork information. + GetSubnetwork, + /// Retrieves unspent transaction outputs (UTXOs) associated with + /// specific addresses. + /// Returned information: List of UTXOs. + GetUtxosByAddresses, + /// Retrieves the virtual chain corresponding to a specified block hash. + /// Returned information: Virtual chain information. + GetVirtualChainFromBlock, + /// Resolves a finality conflict in the Kaspa BlockDAG. + /// Returned information: None. + ResolveFinalityConflict, + /// Submits a block to the Kaspa network. + /// Returned information: None. + SubmitBlock, + /// Submits a transaction to the Kaspa network. + /// Returned information: None. + SubmitTransaction, + /// Unbans a previously banned peer, allowing it to connect + /// to the Kaspa node again. + /// Returned information: None. + Unban, + ] +); diff --git a/rpc/wrpc/wasm/src/imports.rs b/rpc/wrpc/wasm/src/imports.rs new file mode 100644 index 000000000..19065859f --- /dev/null +++ b/rpc/wrpc/wasm/src/imports.rs @@ -0,0 +1,40 @@ +#![allow(unused_imports)] + +pub use ahash::AHashMap; +pub use async_std::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; +pub use cfg_if::cfg_if; +pub use futures::*; +pub use js_sys::Function; +pub use kaspa_consensus_core::network::{NetworkId, NetworkIdError, NetworkIdT}; +pub use kaspa_notify::{ + error::{Error as NotifyError, Result as NotifyResult}, + events::EVENT_TYPE_ARRAY, + listener::ListenerId, + notifier::{Notifier, Notify}, + scope::*, + subscriber::{Subscriber, SubscriptionManager}, +}; +pub use kaspa_rpc_core::{ + api::ops::RpcApiOps, + api::rpc::RpcApi, + error::RpcResult, + notify::{connection::ChannelConnection, mode::NotificationMode}, + prelude::*, +}; +pub use kaspa_wrpc_client::client::*; +pub use kaspa_wrpc_client::error::Error; +pub use kaspa_wrpc_client::result::Result; +pub use serde::{Deserialize, Serialize}; +pub use std::str::FromStr; +pub use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, +}; +pub use wasm_bindgen::prelude::*; +pub use workflow_core::{ + channel::{Channel, DuplexChannel, Receiver}, + task::spawn, +}; +pub use workflow_log::*; +pub use workflow_rpc::client::prelude::{Encoding as WrpcEncoding, *}; +pub use workflow_wasm::prelude::*; diff --git a/rpc/wrpc/wasm/src/lib.rs b/rpc/wrpc/wasm/src/lib.rs index d40b5794b..e80b3baac 100644 --- a/rpc/wrpc/wasm/src/lib.rs +++ b/rpc/wrpc/wasm/src/lib.rs @@ -1,2 +1,16 @@ -#[allow(unused_imports)] -use kaspa_wrpc_client::wasm::*; +#![allow(unused_imports)] + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + mod imports; + pub mod client; + pub use client::*; + pub mod resolver; + pub use resolver::*; + pub mod notify; + pub use notify::*; + } + +} diff --git a/rpc/wrpc/wasm/src/notify.rs b/rpc/wrpc/wasm/src/notify.rs new file mode 100644 index 000000000..23781e314 --- /dev/null +++ b/rpc/wrpc/wasm/src/notify.rs @@ -0,0 +1,239 @@ +use crate::imports::*; +use kaspa_rpc_macros::declare_typescript_wasm_interface as declare; + +#[wasm_bindgen(typescript_custom_section)] +const TS_HEADER: &'static str = r#" + +/** + * RPC notification events. + * + * @see {RpcClient.addEventListener}, {RpcClient.removeEventListener} + */ +export enum RpcEventType { + Connect = "connect", + Disconnect = "disconnect", + BlockAdded = "block-added", + VirtualChainChanged = "virtual-chain-changed", + FinalityConflict = "finality-conflict", + FinalityConflictResolved = "finality-conflict-resolved", + UtxosChanged = "utxos-changed", + SinkBlueScoreChanged = "sink-blue-score-changed", + VirtualDaaScoreChanged = "virtual-daa-score-changed", + PruningPointUtxoSetOverride = "pruning-point-utxo-set-override", + NewBlockTemplate = "new-block-template", +} + +/** + * RPC notification data payload. + * + * @category Node RPC + */ +export type RpcEventData = IBlockAdded + | IVirtualChainChanged + | IFinalityConflict + | IFinalityConflictResolved + | IUtxosChanged + | ISinkBlueScoreChanged + | IVirtualDaaScoreChanged + | IPruningPointUtxoSetOverride + | INewBlockTemplate; + +/** + * RPC notification event data map. + * + * @category Node RPC + */ +export type RpcEventMap = { + "connect" : undefined, + "disconnect" : undefined, + "block-added" : IBlockAdded, + "virtual-chain-changed" : IVirtualChainChanged, + "finality-conflict" : IFinalityConflict, + "finality-conflict-resolved" : IFinalityConflictResolved, + "utxos-changed" : IUtxosChanged, + "sink-blue-score-changed" : ISinkBlueScoreChanged, + "virtual-daa-score-changed" : IVirtualDaaScoreChanged, + "pruning-point-utxo-set-override" : IPruningPointUtxoSetOverride, + "new-block-template" : INewBlockTemplate, +} + +/** + * RPC notification event. + * + * @category Node RPC + */ +export type RpcEvent = { + [K in keyof RpcEventMap]: { event: K, data: RpcEventMap[K] } +}[keyof RpcEventMap]; + +/** + * RPC notification callback type. + * + * This type is used to define the callback function that is called when an RPC notification is received. + * + * @see {@link RpcClient.subscribeVirtualDaaScoreChanged}, + * {@link RpcClient.subscribeUtxosChanged}, + * {@link RpcClient.subscribeVirtualChainChanged}, + * {@link RpcClient.subscribeBlockAdded}, + * {@link RpcClient.subscribeFinalityConflict}, + * {@link RpcClient.subscribeFinalityConflictResolved}, + * {@link RpcClient.subscribeSinkBlueScoreChanged}, + * {@link RpcClient.subscribePruningPointUtxoSetOverride}, + * {@link RpcClient.subscribeNewBlockTemplate}, + * + * @category Node RPC + */ +export type RpcEventCallback = (event: RpcEvent) => void; + +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Function, typescript_type = "RpcEventCallback")] + pub type RpcEventCallback; + + #[wasm_bindgen(extends = js_sys::Function, typescript_type = "RpcEventType | string")] + #[derive(Debug)] + pub type RpcEventType; + + #[wasm_bindgen(typescript_type = "RpcEventType | string | RpcEventCallback")] + #[derive(Debug)] + pub type RpcEventTypeOrCallback; +} + +declare! { + IBlockAdded, + r#" + /** + * Block added notification event is produced when a new + * block is added to the Kaspa BlockDAG. + * + * @category Node RPC + */ + export interface IBlockAdded { + [key: string]: any; + } + "#, +} + +declare! { + IVirtualChainChanged, + r#" + /** + * Virtual chain changed notification event is produced when the virtual + * chain changes in the Kaspa BlockDAG. + * + * @category Node RPC + */ + export interface IVirtualChainChanged { + [key: string]: any; + } + "#, +} + +declare! { + IFinalityConflict, + r#" + /** + * Finality conflict notification event is produced when a finality + * conflict occurs in the Kaspa BlockDAG. + * + * @category Node RPC + */ + export interface IFinalityConflict { + [key: string]: any; + } + "#, +} + +declare! { + IFinalityConflictResolved, + r#" + /** + * Finality conflict resolved notification event is produced when a finality + * conflict in the Kaspa BlockDAG is resolved. + * + * @category Node RPC + */ + export interface IFinalityConflictResolved { + [key: string]: any; + } + "#, +} + +declare! { + IUtxosChanged, + r#" + /** + * UTXOs changed notification event is produced when the set + * of unspent transaction outputs (UTXOs) changes in the + * Kaspa BlockDAG. The event notification is scoped to the + * monitored list of addresses specified during the subscription. + * + * @category Node RPC + */ + export interface IUtxosChanged { + [key: string]: any; + } + "#, +} + +declare! { + ISinkBlueScoreChanged, + r#" + /** + * Sink blue score changed notification event is produced when the blue + * score of the sink block changes in the Kaspa BlockDAG. + * + * @category Node RPC + */ + export interface ISinkBlueScoreChanged { + [key: string]: any; + } + "#, +} + +declare! { + IVirtualDaaScoreChanged, + r#" + /** + * Virtual DAA score changed notification event is produced when the virtual + * Difficulty Adjustment Algorithm (DAA) score changes in the Kaspa BlockDAG. + * + * @category Node RPC + */ + export interface IVirtualDaaScoreChanged { + [key: string]: any; + } + "#, +} + +declare! { + IPruningPointUtxoSetOverride, + r#" + /** + * Pruning point UTXO set override notification event is produced when the + * UTXO set override for the pruning point changes in the Kaspa BlockDAG. + * + * @category Node RPC + */ + export interface IPruningPointUtxoSetOverride { + [key: string]: any; + } + "#, +} + +declare! { + INewBlockTemplate, + r#" + /** + * New block template notification event is produced when a new block + * template is generated for mining in the Kaspa BlockDAG. + * + * @category Node RPC + */ + export interface INewBlockTemplate { + [key: string]: any; + } + "#, +} diff --git a/rpc/wrpc/wasm/src/resolver.rs b/rpc/wrpc/wasm/src/resolver.rs new file mode 100644 index 000000000..ee4b5d883 --- /dev/null +++ b/rpc/wrpc/wasm/src/resolver.rs @@ -0,0 +1,208 @@ +use crate::client::{RpcClient, RpcConfig}; +use crate::imports::*; +use js_sys::Array; +pub use kaspa_rpc_macros::declare_typescript_wasm_interface as declare; +use kaspa_wrpc_client::node::NodeDescriptor; +use kaspa_wrpc_client::Resolver as NativeResolver; +use serde::ser; +use workflow_wasm::extensions::ObjectExtension; + +declare! { + IResolverConfig, + "IResolverConfig | string[]", + r#" + /** + * RPC Resolver configuration options + * + * @category Node RPC + */ + export interface IResolverConfig { + /** + * Optional URLs for one or multiple resolvers. + */ + urls?: string[]; + } + "#, +} + +declare! { + IResolverConnect, + "IResolverConnect | NetworkId | string", + r#" + /** + * RPC Resolver connection options + * + * @category Node RPC + */ + export interface IResolverConnect { + /** + * RPC encoding: `borsh` (default) or `json` + */ + encoding?: Encoding | string; + /** + * Network identifier: `mainnet` or `testnet-11` etc. + */ + networkId?: NetworkId | string; + } + "#, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolverConnect { + pub encoding: Option, + pub network_id: NetworkId, +} + +impl TryFrom for ResolverConnect { + type Error = Error; + fn try_from(config: IResolverConnect) -> Result { + if let Ok(network_id) = NetworkId::try_owned_from(&config) { + Ok(Self { encoding: None, network_id }) + } else { + Ok(serde_wasm_bindgen::from_value(config.into())?) + } + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Array, typescript_type = "string[]")] + pub type ResolverArrayT; +} + +/// +/// Resolver is a client for obtaining public Kaspa wRPC URL. +/// +/// Resolver queries a list of public Kaspa Resolver URLs using HTTP to fetch +/// wRPC endpoints for the given encoding, network identifier and other +/// parameters. It then provides this information to the {@link RpcClient}. +/// +/// Each time {@link RpcClient} disconnects, it will query the resolver +/// to fetch a new wRPC URL. +/// +/// ```javascript +/// // using integrated public URLs +/// let rpc = RpcClient({ +/// resolver: new Resolver(), +/// networkId : "mainnet" +/// }); +/// +/// // specifying custom resolver URLs +/// let rpc = RpcClient({ +/// resolver: new Resolver({urls: ["",...]}), +/// networkId : "mainnet" +/// }); +/// ``` +/// +/// @see {@link IResolverConfig}, {@link IResolverConnect}, {@link RpcClient} +/// @category Node RPC +/// +#[derive(Debug, Clone, CastFromJs)] +#[wasm_bindgen(inspectable)] +pub struct Resolver { + resolver: NativeResolver, +} + +impl Resolver { + pub fn new(resolver: NativeResolver) -> Self { + Self { resolver } + } +} + +#[wasm_bindgen] +impl Resolver { + /// Creates a new Resolver client with the given + /// configuration supplied as {@link IResolverConfig} + /// interface. If not supplied, the default configuration + /// containing a list of community-operated resolvers + /// will be used. + #[wasm_bindgen(constructor)] + pub fn ctor(args: Option) -> Result { + if let Some(args) = args { + Ok(Self { resolver: NativeResolver::try_from(args)? }) + } else { + Ok(Self { resolver: NativeResolver::default() }) + } + } +} + +#[wasm_bindgen] +impl Resolver { + /// List of public Kaspa Resolver URLs. + #[wasm_bindgen(getter)] + pub fn urls(&self) -> ResolverArrayT { + Array::from_iter(self.resolver.urls().iter().map(|v| JsValue::from(v.as_str()))).unchecked_into() + } + + /// Fetches a public Kaspa wRPC endpoint for the given encoding and network identifier. + /// @see {@link Encoding}, {@link NetworkId}, {@link Node} + #[wasm_bindgen(js_name = getNode)] + pub async fn get_node(&self, encoding: Encoding, network_id: NetworkIdT) -> Result { + self.resolver.get_node(encoding, *network_id.try_into_cast()?).await + } + + /// Fetches a public Kaspa wRPC endpoint URL for the given encoding and network identifier. + /// @see {@link Encoding}, {@link NetworkId} + #[wasm_bindgen(js_name = getUrl)] + pub async fn get_url(&self, encoding: Encoding, network_id: NetworkIdT) -> Result { + self.resolver.get_url(encoding, *network_id.try_into_cast()?).await + } + + /// Connect to a public Kaspa wRPC endpoint for the given encoding and network identifier + /// supplied via {@link IResolverConnect} interface. + /// @see {@link IResolverConnect}, {@link RpcClient} + pub async fn connect(&self, options: IResolverConnect) -> Result { + let ResolverConnect { encoding, network_id } = options.try_into()?; + let config = RpcConfig { resolver: Some(self.clone()), url: None, encoding, network_id: Some(network_id) }; + let client = RpcClient::new(Some(config))?; + client.connect(None).await?; + Ok(client) + } +} + +impl TryFrom for NativeResolver { + type Error = Error; + fn try_from(config: IResolverConfig) -> Result { + let resolver = config + .get_vec("urls") + .map(|urls| urls.into_iter().map(|v| v.as_string()).collect::>>()) + .or_else(|_| config.dyn_into::().map(|urls| urls.into_iter().map(|v| v.as_string()).collect::>>())) + .map_err(|_| Error::custom("Invalid or missing resolver URL"))? + .map(|urls| NativeResolver::new(urls.into_iter().map(Arc::new).collect())); + + Ok(resolver.unwrap_or_default()) + } +} + +impl TryCastFromJs for Resolver { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result> { + Ok(Self::try_ref_from_js_value_as_cast(value)?) + } +} + +impl TryFrom<&JsValue> for Resolver { + type Error = Error; + fn try_from(js_value: &JsValue) -> Result { + Ok(Resolver::try_ref_from_js_value(js_value)?.clone()) + } +} + +impl TryFrom for Resolver { + type Error = Error; + fn try_from(js_value: JsValue) -> Result { + Resolver::try_from(js_value.as_ref()) + } +} + +impl From for NativeResolver { + fn from(resolver: Resolver) -> Self { + resolver.resolver + } +} + +impl From for Resolver { + fn from(resolver: NativeResolver) -> Self { + Self { resolver } + } +} diff --git a/rpc/wrpc/wasm/web/index.html b/rpc/wrpc/wasm/web/index.html index c97a9eb17..a26f38512 100644 --- a/rpc/wrpc/wasm/web/index.html +++ b/rpc/wrpc/wasm/web/index.html @@ -3,7 +3,8 @@ diff --git a/simpa/src/simulator/miner.rs b/simpa/src/simulator/miner.rs index 4e167583b..1bcf86d27 100644 --- a/simpa/src/simulator/miner.rs +++ b/simpa/src/simulator/miner.rs @@ -137,7 +137,7 @@ impl Miner { let virtual_state = virtual_read.state.get().unwrap(); let virtual_utxo_view = &virtual_read.utxo_set; let multiple_outputs = self.possible_unspent_outpoints.len() < 5_000; - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, &self.secret_key.secret_bytes()).unwrap(); + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &self.secret_key.secret_bytes()).unwrap(); let txs = self .possible_unspent_outpoints .iter() diff --git a/test b/test index 459b638c2..8eeebc817 100755 --- a/test +++ b/test @@ -1,3 +1,6 @@ +#!/bin/bash +set -e + # tests cargo nextest run --release --workspace -p kaspa-testing-integration --lib if [ $? -ne 0 ]; then diff --git a/testing/integration/src/common/utils.rs b/testing/integration/src/common/utils.rs index ebe812f08..824bda388 100644 --- a/testing/integration/src/common/utils.rs +++ b/testing/integration/src/common/utils.rs @@ -19,7 +19,7 @@ use kaspa_grpc_client::GrpcClient; use kaspa_rpc_core::{api::rpc::RpcApi, BlockAddedNotification, Notification, VirtualDaaScoreChangedNotification}; use kaspa_txscript::pay_to_address_script; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; -use secp256k1::KeyPair; +use secp256k1::Keypair; use std::{ collections::{hash_map::Entry::Occupied, HashMap, HashSet}, future::Future, @@ -43,7 +43,7 @@ pub const fn required_fee(num_inputs: usize, num_outputs: u64) -> u64 { /// Builds a TX DAG based on the initial UTXO set and on constant params pub fn generate_tx_dag( mut utxoset: UtxoCollection, - schnorr_key: KeyPair, + schnorr_key: Keypair, spk: ScriptPublicKey, target_levels: usize, target_width: usize, @@ -134,7 +134,7 @@ where } pub fn generate_tx( - schnorr_key: KeyPair, + schnorr_key: Keypair, utxos: &[(TransactionOutpoint, UtxoEntry)], amount: u64, num_outputs: u64, diff --git a/testing/integration/src/daemon_integration_tests.rs b/testing/integration/src/daemon_integration_tests.rs index 9c0e11718..29f74e75e 100644 --- a/testing/integration/src/daemon_integration_tests.rs +++ b/testing/integration/src/daemon_integration_tests.rs @@ -164,7 +164,7 @@ async fn daemon_utxos_propagation_test() { let (miner_sk, miner_pk) = secp256k1::generate_keypair(&mut thread_rng()); let miner_address = Address::new(kaspad1.network.into(), kaspa_addresses::Version::PubKey, &miner_pk.x_only_public_key().0.serialize()); - let miner_schnorr_key = secp256k1::KeyPair::from_secret_key(secp256k1::SECP256K1, &miner_sk); + let miner_schnorr_key = secp256k1::Keypair::from_secret_key(secp256k1::SECP256K1, &miner_sk); let miner_spk = pay_to_address_script(&miner_address); // User key and address diff --git a/testing/integration/src/mempool_benchmarks.rs b/testing/integration/src/mempool_benchmarks.rs index d9b8ff576..3df716594 100644 --- a/testing/integration/src/mempool_benchmarks.rs +++ b/testing/integration/src/mempool_benchmarks.rs @@ -78,7 +78,7 @@ async fn bench_bbt_latency() { let (prealloc_sk, prealloc_pk) = secp256k1::generate_keypair(&mut thread_rng()); let prealloc_address = Address::new(NetworkType::Simnet.into(), kaspa_addresses::Version::PubKey, &prealloc_pk.x_only_public_key().0.serialize()); - let schnorr_key = secp256k1::KeyPair::from_secret_key(secp256k1::SECP256K1, &prealloc_sk); + let schnorr_key = secp256k1::Keypair::from_secret_key(secp256k1::SECP256K1, &prealloc_sk); let spk = pay_to_address_script(&prealloc_address); let args = Args { @@ -326,7 +326,7 @@ async fn bench_bbt_latency_2() { let (prealloc_sk, prealloc_pk) = secp256k1::generate_keypair(&mut thread_rng()); let prealloc_address = Address::new(NetworkType::Simnet.into(), kaspa_addresses::Version::PubKey, &prealloc_pk.x_only_public_key().0.serialize()); - let schnorr_key = secp256k1::KeyPair::from_secret_key(secp256k1::SECP256K1, &prealloc_sk); + let schnorr_key = secp256k1::Keypair::from_secret_key(secp256k1::SECP256K1, &prealloc_sk); let spk = pay_to_address_script(&prealloc_address); let args = ArgsBuilder::simnet(TX_LEVEL_WIDTH as u64 * CONTRACT_FACTOR, 500) diff --git a/testing/integration/src/subscribe_benchmarks.rs b/testing/integration/src/subscribe_benchmarks.rs index 375e2359f..8efefd842 100644 --- a/testing/integration/src/subscribe_benchmarks.rs +++ b/testing/integration/src/subscribe_benchmarks.rs @@ -176,7 +176,7 @@ async fn utxos_changed_subscriptions_client(address_cycle_seconds: u64, address_ let (prealloc_sk, prealloc_pk) = secp256k1::generate_keypair(&mut thread_rng()); let prealloc_address = Address::new(NetworkType::Simnet.into(), kaspa_addresses::Version::PubKey, &prealloc_pk.x_only_public_key().0.serialize()); - let schnorr_key = secp256k1::KeyPair::from_secret_key(secp256k1::SECP256K1, &prealloc_sk); + let schnorr_key = secp256k1::Keypair::from_secret_key(secp256k1::SECP256K1, &prealloc_sk); let spk = pay_to_address_script(&prealloc_address); let args = ArgsBuilder::simnet(TX_LEVEL_WIDTH as u64 * CONTRACT_FACTOR, PREALLOC_AMOUNT) diff --git a/testing/integration/src/tasks/daemon.rs b/testing/integration/src/tasks/daemon.rs index b800a8b9f..502180fec 100644 --- a/testing/integration/src/tasks/daemon.rs +++ b/testing/integration/src/tasks/daemon.rs @@ -98,7 +98,7 @@ impl DaemonArgs { pub fn prealloc_address(&self) -> Address { let mut private_key_bytes = [0u8; 32]; faster_hex::hex_decode(self.private_key.as_bytes(), &mut private_key_bytes).unwrap(); - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, &private_key_bytes).unwrap(); + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &private_key_bytes).unwrap(); Address::new( NetworkType::Simnet.into(), kaspa_addresses::Version::PubKey, diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 2202ab719..a3002afab 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -24,6 +24,7 @@ thiserror.workspace = true triggered.workspace = true uuid.workspace = true log.workspace = true +wasm-bindgen.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rlimit.workspace = true diff --git a/utils/src/networking.rs b/utils/src/networking.rs index e72c4d4d1..bb38b4d04 100644 --- a/utils/src/networking.rs +++ b/utils/src/networking.rs @@ -9,6 +9,27 @@ use std::{ str::FromStr, }; use uuid::Uuid; +use wasm_bindgen::prelude::*; + +// A network address serialization of [`ContextualNetAddress`]. +#[wasm_bindgen(typescript_custom_section)] +const TS_IP_ADDRESS: &'static str = r#" + /** + * Generic network address representation. + * + * @category General + */ + export interface INetworkAddress { + /** + * IPv4 or IPv6 address. + */ + ip: string; + /** + * Optional port number. + */ + port?: number; + } +"#; /// A bucket based on an ip's prefix bytes. /// for ipv4 it consists of 6 leading zero bytes, and the first two octets, @@ -421,6 +442,15 @@ mod tests { assert!(addr.prefix_bucket() == PrefixBucket(u16::from_be_bytes(prefix_bytes) as u64)); } + #[test] + fn test_contextual_address_ser() { + let addr = IpAddress::from_str("127.0.0.1").unwrap(); + let port = Some(1234); + let net_addr = ContextualNetAddress::new(addr, port); + let s = serde_json::to_string(&net_addr).unwrap(); + assert_eq!(s, r#"{"ip":"127.0.0.1","port":1234}"#); + } + #[test] fn test_is_publicly_routable() { // RFC 2544 tests diff --git a/wallet/bip32/src/lib.rs b/wallet/bip32/src/lib.rs index e883f56f0..a406067f6 100644 --- a/wallet/bip32/src/lib.rs +++ b/wallet/bip32/src/lib.rs @@ -18,7 +18,6 @@ mod mnemonic; mod prefix; mod result; pub mod types; -pub mod wasm; pub use address_type::AddressType; pub use attrs::ExtendedKeyAttrs; diff --git a/wallet/bip32/src/mnemonic/language.rs b/wallet/bip32/src/mnemonic/language.rs index b06207477..bb4f386db 100644 --- a/wallet/bip32/src/mnemonic/language.rs +++ b/wallet/bip32/src/mnemonic/language.rs @@ -9,9 +9,14 @@ use super::bits::{Bits, Bits11}; use std::{collections::BTreeMap, vec::Vec}; use wasm_bindgen::prelude::*; -/// Supported languages. /// -/// Presently only English is specified by the BIP39 standard +/// Languages supported by BIP39. +/// +/// Presently only English is specified by the BIP39 standard. +/// +/// @see {@link Mnemonic} +/// +/// @category Wallet SDK #[derive(Copy, Clone, Debug, Default)] #[wasm_bindgen] pub enum Language { diff --git a/wallet/bip32/src/mnemonic/phrase.rs b/wallet/bip32/src/mnemonic/phrase.rs index 6d9bf68b7..76450590a 100644 --- a/wallet/bip32/src/mnemonic/phrase.rs +++ b/wallet/bip32/src/mnemonic/phrase.rs @@ -43,6 +43,7 @@ impl TryFrom for WordCount { } /// BIP39 mnemonic phrases: sequences of words representing cryptographic keys. +/// @category Wallet SDK #[derive(Clone)] #[wasm_bindgen(inspectable)] pub struct Mnemonic { @@ -63,6 +64,11 @@ impl Mnemonic { Mnemonic::new(phrase, language.unwrap_or(Language::English)) } + /// Validate mnemonic phrase. Returns `true` if the phrase is valid, `false` otherwise. + pub fn validate(phrase: &str, language: Option) -> bool { + Mnemonic::new(phrase, language.unwrap_or(Language::English)).is_ok() + } + #[wasm_bindgen(getter, js_name = entropy)] pub fn get_entropy(&self) -> String { self.entropy.to_hex() diff --git a/wallet/bip32/src/prefix.rs b/wallet/bip32/src/prefix.rs index 989c75c1e..8a4bcc627 100644 --- a/wallet/bip32/src/prefix.rs +++ b/wallet/bip32/src/prefix.rs @@ -215,6 +215,9 @@ impl TryFrom<&str> for Prefix { "kprv" => Ok(Prefix::KPRV), "kpub" => Ok(Prefix::KPUB), + "ktrv" => Ok(Prefix::KTRV), + "ktub" => Ok(Prefix::KTUB), + "tprv" => Ok(Prefix::TPRV), "tpub" => Ok(Prefix::TPUB), diff --git a/wallet/bip32/src/wasm/mod.rs b/wallet/bip32/src/wasm/mod.rs deleted file mode 100644 index 4351badde..000000000 --- a/wallet/bip32/src/wasm/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod derivation_path; -pub mod xprv; -pub mod xpub; - -pub use derivation_path::*; -pub use xprv::*; -pub use xpub::*; diff --git a/wallet/bip32/src/wasm/xprv.rs b/wallet/bip32/src/wasm/xprv.rs deleted file mode 100644 index dda2a0336..000000000 --- a/wallet/bip32/src/wasm/xprv.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::{ - wasm::{DerivationPath, XPub}, - ChildNumber, Error, ExtendedPrivateKey, Result, SecretKey, -}; -use kaspa_utils::hex::*; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -pub struct XPrv { - inner: ExtendedPrivateKey, -} - -#[wasm_bindgen] -impl XPrv { - #[wasm_bindgen(constructor)] - pub fn new(seed: String) -> Result { - let seed_bytes = Vec::::from_hex(&seed).map_err(|_| Error::String("Invalid seed".to_string()))?; - - let inner = ExtendedPrivateKey::::new(seed_bytes)?; - Ok(Self { inner }) - } - - #[wasm_bindgen(js_name=deriveChild)] - pub fn derive_child(&self, chile_number: u32, hardened: Option) -> Result { - let chile_number = ChildNumber::new(chile_number, hardened.unwrap_or(false))?; - let inner = self.inner.derive_child(chile_number)?; - Ok(Self { inner }) - } - - #[wasm_bindgen(js_name=derivePath)] - pub fn derive_path(&self, path: JsValue) -> Result { - let path = DerivationPath::try_from(path)?; - let inner = self.inner.clone().derive_path(path.into())?; - Ok(Self { inner }) - } - - //#[wasm_bindgen(js_name = toString)] - #[wasm_bindgen(js_name = intoString)] - pub fn to_str(&self, prefix: &str) -> Result { - let str = self.inner.to_extended_key(prefix.try_into()?).to_string(); - Ok(str) - } - - #[wasm_bindgen(js_name = publicKey)] - pub fn public_key(&self) -> Result { - let publick_key = self.inner.public_key(); - Ok(publick_key.into()) - } -} diff --git a/wallet/bip32/src/wasm/xpub.rs b/wallet/bip32/src/wasm/xpub.rs deleted file mode 100644 index f6a2ada75..000000000 --- a/wallet/bip32/src/wasm/xpub.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::{wasm::DerivationPath, ChildNumber, ExtendedPublicKey, Result}; -use secp256k1::PublicKey; -use std::str::FromStr; -use wasm_bindgen::prelude::*; -//use js_sys::Array; - -#[wasm_bindgen] -pub struct XPub { - inner: ExtendedPublicKey, -} - -#[wasm_bindgen] -impl XPub { - #[wasm_bindgen(constructor)] - pub fn new(xpub: &str) -> Result { - let inner = ExtendedPublicKey::::from_str(xpub)?; - Ok(Self { inner }) - } - - #[wasm_bindgen(js_name=deriveChild)] - pub fn derive_child(&self, chile_number: u32, hardened: Option) -> Result { - let chile_number = ChildNumber::new(chile_number, hardened.unwrap_or(false))?; - let inner = self.inner.derive_child(chile_number)?; - Ok(Self { inner }) - } - - #[wasm_bindgen(js_name=derivePath)] - pub fn derive_path(&self, path: JsValue) -> Result { - let path = DerivationPath::try_from(path)?; - let inner = self.inner.clone().derive_path(path.into())?; - Ok(Self { inner }) - } - - //#[wasm_bindgen(js_name = toString)] - #[wasm_bindgen(js_name = intoString)] - pub fn to_str(&self, prefix: &str) -> Result { - Ok(self.inner.to_string(Some(prefix.try_into()?))) - } - - // #[wasm_bindgen(js_name = toBytes)] - // pub fn to_bytes(&self)->Array{ - // let array = js_sys::Uint8Array::from(&self.inner.to_bytes()); - // Array::from_iter(self.inner.to_bytes().iter().map(JsValue::from)) - // } -} - -impl From> for XPub { - fn from(inner: ExtendedPublicKey) -> Self { - Self { inner } - } -} diff --git a/wallet/bip32/src/xprivate_key.rs b/wallet/bip32/src/xprivate_key.rs index eec196f56..0b3f5de2e 100644 --- a/wallet/bip32/src/xprivate_key.rs +++ b/wallet/bip32/src/xprivate_key.rs @@ -96,7 +96,7 @@ where Ok(ExtendedPrivateKey { private_key, attrs }) } - pub fn derive_path(self, path: DerivationPath) -> Result { + pub fn derive_path(self, path: &DerivationPath) -> Result { path.iter().try_fold(self, |key, child_num| key.derive_child(child_num)) } diff --git a/wallet/bip32/src/xpublic_key.rs b/wallet/bip32/src/xpublic_key.rs index c971f6a32..a52d8f1a1 100644 --- a/wallet/bip32/src/xpublic_key.rs +++ b/wallet/bip32/src/xpublic_key.rs @@ -76,7 +76,7 @@ where Ok(ExtendedPublicKey { public_key, attrs }) } - pub fn derive_path(self, path: DerivationPath) -> Result { + pub fn derive_path(self, path: &DerivationPath) -> Result { path.iter().try_fold(self, |key, child_num| key.derive_child(child_num)) } diff --git a/wallet/core/Cargo.toml b/wallet/core/Cargo.toml index c882ceedc..fb31afb31 100644 --- a/wallet/core/Cargo.toml +++ b/wallet/core/Cargo.toml @@ -11,9 +11,22 @@ license.workspace = true repository.workspace = true [features] -no-unsafe-eval = ["workflow-core/no-unsafe-eval","workflow-rpc/no-unsafe-eval"] multi-user = [] -default = [] +wasm32-keygen = [ + # "kaspa-consensus-wasm/wasm32-types", +] +wasm32-core = [ + "kaspa-consensus-wasm/wasm32-sdk", + "kaspa-consensus-core/wasm32-sdk", + "kaspa-wrpc-client/wasm32-sdk", + "kaspa-wrpc-wasm/wasm32-sdk", + "kaspa-wasm-core/wasm32-sdk", +] +wasm32-sdk = [ + "wasm32-core" +] +default = ["wasm32-sdk"] +# default = [] [lib] crate-type = ["cdylib", "lib"] @@ -30,6 +43,8 @@ borsh.workspace = true cfb-mode.workspace = true cfg-if.workspace = true chacha20poly1305.workspace = true +convert_case.workspace = true +crypto_box.workspace = true dashmap.workspace = true derivative.workspace = true downcast.workspace = true @@ -43,17 +58,22 @@ itertools.workspace = true js-sys.workspace = true kaspa-addresses.workspace = true kaspa-bip32.workspace = true +kaspa-consensus-client.workspace = true kaspa-consensus-core.workspace = true kaspa-consensus-wasm.workspace = true kaspa-core.workspace = true kaspa-hashes.workspace = true +kaspa-metrics-core.workspace = true kaspa-notify.workspace = true kaspa-rpc-core.workspace = true kaspa-txscript-errors.workspace = true kaspa-txscript.workspace = true kaspa-utils.workspace = true +kaspa-wallet-keys.workspace = true kaspa-wallet-macros.workspace = true +kaspa-wasm-core.workspace = true kaspa-wrpc-client.workspace = true +kaspa-wrpc-wasm.workspace = true md-5.workspace = true pad.workspace = true pbkdf2.workspace = true diff --git a/wallet/core/src/account/descriptor.rs b/wallet/core/src/account/descriptor.rs index 985ff3404..d433b7bf8 100644 --- a/wallet/core/src/account/descriptor.rs +++ b/wallet/core/src/account/descriptor.rs @@ -5,10 +5,13 @@ use crate::derivation::AddressDerivationMeta; use crate::imports::*; use borsh::{BorshDeserialize, BorshSerialize}; +use convert_case::{Case, Casing}; use kaspa_addresses::Address; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +/// @category Wallet API #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct AccountDescriptor { pub kind: AccountKind, @@ -40,7 +43,7 @@ impl AccountDescriptor { } #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] - +#[serde(rename_all = "camelCase")] pub enum AccountDescriptorProperty { AccountIndex, XpubKeys, @@ -73,6 +76,33 @@ pub enum AccountDescriptorValue { Json(String), } +impl TryFrom for JsValue { + type Error = Error; + fn try_from(value: AccountDescriptorValue) -> Result { + let js_value = match value { + AccountDescriptorValue::U64(value) => BigInt::from(value).into(), + AccountDescriptorValue::String(value) => JsValue::from(value), + AccountDescriptorValue::Bool(value) => JsValue::from(value), + AccountDescriptorValue::AddressDerivationMeta(value) => { + let object = Object::new(); + object.set("receive", &value.receive().into())?; + object.set("change", &value.change().into())?; + object.into() + } + AccountDescriptorValue::XPubKeys(value) => { + let array = Array::new(); + for xpub in value.iter() { + array.push(&JsValue::from(xpub.to_string(None))); + } + array.into() + } + AccountDescriptorValue::Json(value) => JsValue::from(value), + }; + + Ok(js_value) + } +} + impl std::fmt::Display for AccountDescriptorValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -179,3 +209,47 @@ impl AccountDescriptor { &self.receive_address } } + +declare! { + IAccountDescriptor, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountDescriptor { + kind : AccountKind, + accountId : HexString, + accountName? : string, + receiveAddress? : Address, + changeAddress? : Address, + prvKeyDataIds : HexString[], + [key: string]: any + } + "#, +} + +impl TryFrom for IAccountDescriptor { + type Error = Error; + fn try_from(descriptor: AccountDescriptor) -> Result { + let object = IAccountDescriptor::default(); + + object.set("kind", &descriptor.kind.into())?; + object.set("accountId", &descriptor.account_id.into())?; + object.set("accountName", &descriptor.account_name.into())?; + object.set("receiveAddress", &descriptor.receive_address.into())?; + object.set("changeAddress", &descriptor.change_address.into())?; + + let prv_key_data_ids = js_sys::Array::from_iter(descriptor.prv_key_data_ids.into_iter().map(JsValue::from)); + object.set("prvKeyDataIds", &prv_key_data_ids)?; + + // let properties = Object::new(); + for (property, value) in descriptor.properties { + let ident = property.to_string().to_case(Case::Camel); + object.set(&ident, &value.try_into()?)?; + } + + Ok(object) + } +} diff --git a/wallet/core/src/account/kind.rs b/wallet/core/src/account/kind.rs index 768f26e7f..faf968f29 100644 --- a/wallet/core/src/account/kind.rs +++ b/wallet/core/src/account/kind.rs @@ -6,11 +6,25 @@ use crate::imports::*; use fixedstr::*; use std::hash::Hash; use std::str::FromStr; +use workflow_wasm::convert::CastFromJs; -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)] +/// @category Wallet SDK +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash, CastFromJs)] #[wasm_bindgen] pub struct AccountKind(str64); +#[wasm_bindgen] +impl AccountKind { + #[wasm_bindgen(constructor)] + pub fn ctor(kind: &str) -> Result { + Self::from_str(kind) + } + #[wasm_bindgen(js_name=toString)] + pub fn js_to_string(&self) -> String { + self.0.as_str().to_string() + } +} + impl AccountKind { pub fn as_str(&self) -> &str { self.0.as_str() @@ -61,7 +75,9 @@ impl FromStr for AccountKind { impl TryFrom for AccountKind { type Error = Error; fn try_from(kind: JsValue) -> Result { - if let Some(kind) = kind.as_string() { + if let Ok(kind_ref) = Self::try_ref_from_js_value(&kind) { + Ok(*kind_ref) + } 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/account/mod.rs b/wallet/core/src/account/mod.rs index 7f74f547e..b921bf491 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -10,7 +10,6 @@ pub use kind::*; pub use variants::*; use crate::derivation::build_derivate_paths; -use crate::derivation::gen0; use crate::derivation::AddressDerivationManagerTrait; use crate::imports::*; use crate::storage::account::AccountSettings; @@ -21,7 +20,8 @@ use crate::tx::{Fees, Generator, GeneratorSettings, GeneratorSummary, PaymentDes use crate::utxo::balance::{AtomicBalance, BalanceStrings}; use crate::utxo::UtxoContextBinding; use kaspa_bip32::{ChildNumber, ExtendedPrivateKey, PrivateKey}; -use kaspa_consensus_wasm::UtxoEntryReference; +use kaspa_consensus_client::UtxoEntryReference; +use kaspa_wallet_keys::derivation::gen0::WalletDerivationManagerV0; use workflow_core::abortable::Abortable; /// Notification callback type used by [`Account::sweep`] and [`Account::send`]. @@ -109,7 +109,7 @@ pub trait Account: AnySync + Send + Sync + 'static { } fn balance_as_strings(&self, padding: Option) -> Result { - Ok(BalanceStrings::from((&self.balance(), &self.wallet().network_id()?.into(), padding))) + Ok(BalanceStrings::from((self.balance().as_ref(), &self.wallet().network_id()?.into(), padding))) } fn name(&self) -> Option { @@ -490,7 +490,7 @@ pub trait DerivationCapableAccount: Account { let last = (index + window) as u32; index = last as usize; - let (keys, addresses) = if sweep { + let (mut keys, addresses) = if sweep { let mut keypairs = derivation.get_range_with_keys(false, first..last, false, &xkey).await?; let change_keypairs = derivation.get_range_with_keys(true, first..last, false, &xkey).await?; keypairs.extend(change_keypairs); @@ -537,7 +537,7 @@ pub trait DerivationCapableAccount: Account { let mut stream = generator.stream(); while let Some(transaction) = stream.try_next().await? { - transaction.try_sign_with_keys(keys.clone())?; + transaction.try_sign_with_keys(&keys)?; let id = transaction.try_submit(&rpc).await?; if let Some(notifier) = notifier { notifier(index, aggregate_utxo_count, balance, Some(id)); @@ -559,6 +559,8 @@ pub trait DerivationCapableAccount: Account { } yield_executor().await; } + + keys.zeroize(); } if index > last_notification { @@ -630,21 +632,21 @@ pub(crate) fn create_private_keys<'l>( let paths = build_derivate_paths(account_kind, account_index, cosigner_index)?; let mut private_keys = vec![]; if matches!(account_kind.as_ref(), LEGACY_ACCOUNT_KIND) { - let (private_key, attrs) = gen0::WalletDerivationManagerV0::derive_key_by_path(xkey, paths.0)?; + let (private_key, attrs) = WalletDerivationManagerV0::derive_key_by_path(xkey, paths.0)?; for (address, index) in receive.iter() { let (private_key, _) = - gen0::WalletDerivationManagerV0::derive_private_key(&private_key, &attrs, ChildNumber::new(*index, true)?)?; + WalletDerivationManagerV0::derive_private_key(&private_key, &attrs, ChildNumber::new(*index, true)?)?; private_keys.push((*address, private_key)); } - let (private_key, attrs) = gen0::WalletDerivationManagerV0::derive_key_by_path(xkey, paths.1)?; + let (private_key, attrs) = WalletDerivationManagerV0::derive_key_by_path(xkey, paths.1)?; for (address, index) in change.iter() { let (private_key, _) = - gen0::WalletDerivationManagerV0::derive_private_key(&private_key, &attrs, ChildNumber::new(*index, true)?)?; + WalletDerivationManagerV0::derive_private_key(&private_key, &attrs, ChildNumber::new(*index, true)?)?; private_keys.push((*address, private_key)); } } else { - let receive_xkey = xkey.clone().derive_path(paths.0)?; - let change_xkey = xkey.clone().derive_path(paths.1)?; + let receive_xkey = xkey.clone().derive_path(&paths.0)?; + let change_xkey = xkey.clone().derive_path(&paths.1)?; for (address, index) in receive.iter() { private_keys.push((*address, *receive_xkey.derive_child(ChildNumber::new(*index, false)?)?.private_key())); @@ -662,13 +664,13 @@ pub(crate) fn create_private_keys<'l>( mod tests { use super::create_private_keys; use super::ExtendedPrivateKey; - use crate::derivation::gen0::PubkeyDerivationManagerV0; use crate::imports::LEGACY_ACCOUNT_KIND; use kaspa_addresses::Address; use kaspa_addresses::Prefix; use kaspa_bip32::secp256k1::SecretKey; use kaspa_bip32::PrivateKey; use kaspa_bip32::SecretKeyExt; + use kaspa_wallet_keys::derivation::gen0::PubkeyDerivationManagerV0; use std::str::FromStr; fn gen0_receive_addresses() -> Vec<&'static str> { diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs index b181d3111..9cd2f830c 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -12,13 +12,13 @@ use kaspa_addresses::Address; #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct PingRequest { - pub payload: Option, + pub message: Option, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct PingResponse { - pub payload: Option, + pub message: Option, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -42,7 +42,7 @@ pub struct FlushResponse {} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct ConnectRequest { - pub url: String, + pub url: Option, pub network_id: NetworkId, } @@ -60,7 +60,39 @@ pub struct DisconnectResponse {} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] -pub struct GetStatusRequest {} +pub struct ChangeNetworkIdRequest { + pub network_id: NetworkId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeNetworkIdResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RetainContextRequest { + pub name: String, + pub data: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct RetainContextResponse { + // pub name : String, + // pub data: Option>>, + // 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 GetStatusRequest { + pub name: Option, +} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] @@ -71,6 +103,10 @@ pub struct GetStatusResponse { pub url: Option, pub is_wrpc_client: bool, pub network_id: Option, + pub context: Option>>, + pub wallet_descriptor: Option, + pub account_descriptors: Option>, + pub selected_account_id: Option, } // --- @@ -82,7 +118,7 @@ pub struct WalletEnumerateRequest {} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct WalletEnumerateResponse { - pub wallet_list: Vec, + pub wallet_descriptors: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -103,7 +139,7 @@ pub struct WalletCreateResponse { #[serde(rename_all = "camelCase")] pub struct WalletOpenRequest { pub wallet_secret: Secret, - pub wallet_filename: Option, + pub filename: Option, pub account_descriptors: bool, pub legacy_accounts: Option, } @@ -208,9 +244,13 @@ pub struct PrvKeyDataCreateResponse { pub prv_key_data_id: PrvKeyDataId, } +// TODO #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] -pub struct PrvKeyDataRemoveRequest {} +pub struct PrvKeyDataRemoveRequest { + pub wallet_secret: Secret, + pub prv_key_data_id: PrvKeyDataId, +} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] @@ -236,7 +276,7 @@ pub struct AccountsEnumerateRequest {} #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsEnumerateResponse { - pub descriptor_list: Vec, + pub account_descriptors: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -251,12 +291,24 @@ pub struct AccountsRenameRequest { #[serde(rename_all = "camelCase")] pub struct AccountsRenameResponse {} -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +/// @category Wallet API +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)] #[serde(rename_all = "camelCase")] +#[wasm_bindgen] pub enum AccountsDiscoveryKind { Bip44, } +impl FromStr for AccountsDiscoveryKind { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "bip44" => Ok(Self::Bip44), + _ => Err(Error::custom(format!("Invalid discovery kind: {s}"))), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsDiscoveryRequest { @@ -264,7 +316,7 @@ pub struct AccountsDiscoveryRequest { pub address_scan_extent: u32, pub account_scan_extent: u32, pub bip39_passphrase: Option, - pub bip39_mnemonic: String, + pub bip39_mnemonic: Secret, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -286,6 +338,23 @@ pub struct AccountsCreateResponse { pub account_descriptor: AccountDescriptor, } +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsEnsureDefaultRequest { + pub wallet_secret: Secret, + pub payment_secret: Option, + pub account_kind: AccountKind, + pub mnemonic_phrase: Option, + // pub account_create_args: AccountCreateArgs, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsEnsureDefaultResponse { + pub account_descriptor: AccountDescriptor, +} + +// TODO #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsImportRequest {} @@ -294,6 +363,16 @@ pub struct AccountsImportRequest {} #[serde(rename_all = "camelCase")] pub struct AccountsImportResponse {} +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsSelectRequest { + pub account_id: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsSelectResponse {} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsActivateRequest { @@ -323,16 +402,32 @@ pub struct AccountsGetRequest { #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsGetResponse { - pub descriptor: AccountDescriptor, + pub account_descriptor: AccountDescriptor, } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +/// Specifies the type of an account address to create. +/// The address can bea receive address or a change address. +/// +/// @category Wallet API +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)] #[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "wasm32-sdk", wasm_bindgen)] pub enum NewAddressKind { Receive, Change, } +impl FromStr for NewAddressKind { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "receive" => Ok(Self::Receive), + "change" => Ok(Self::Change), + _ => Err(Error::custom(format!("Invalid address kind: {s}"))), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsCreateNewAddressRequest { @@ -374,6 +469,7 @@ pub struct AccountsTransferRequest { pub payment_secret: Option, pub transfer_amount_sompi: u64, pub priority_fee_sompi: Option, + // pub priority_fee_sompi: Fees, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -383,6 +479,8 @@ pub struct AccountsTransferResponse { pub transaction_ids: Vec, } +// TODO: Use Generator Summary from WASM module... + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsEstimateRequest { diff --git a/wallet/core/src/api/traits.rs b/wallet/core/src/api/traits.rs index 83b3cfdd9..2eb12907a 100644 --- a/wallet/core/src/api/traits.rs +++ b/wallet/core/src/api/traits.rs @@ -9,8 +9,6 @@ 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; @@ -23,9 +21,16 @@ 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<()>; + async fn retain_context(self: Arc, name: &str, data: Option>) -> Result<()> { + self.retain_context_call(RetainContextRequest { name: name.to_string(), data }).await?; + Ok(()) + } + + async fn retain_context_call(self: Arc, request: RetainContextRequest) -> Result; + /// Wrapper around [`get_status_call()`](Self::get_status_call). - async fn get_status(self: Arc) -> Result { - Ok(self.get_status_call(GetStatusRequest {}).await?) + async fn get_status(self: Arc, name: Option<&str>) -> Result { + Ok(self.get_status_call(GetStatusRequest { name: name.map(String::from) }).await?) } /// Returns the current wallet state comprised of the following: @@ -37,18 +42,36 @@ pub trait WalletApi: Send + Sync + AnySync { /// - `is_wrpc_client` - whether the wallet is connected to a node via wRPC async fn get_status_call(self: Arc, request: GetStatusRequest) -> Result; + async fn connect(self: Arc, url: Option, network_id: NetworkId) -> Result<()> { + self.connect_call(ConnectRequest { url, network_id }).await?; + Ok(()) + } + /// 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; + async fn disconnect(self: Arc) -> Result<()> { + self.disconnect_call(DisconnectRequest {}).await?; + Ok(()) + } /// Disconnect the wallet RPC subsystem from the node. async fn disconnect_call(self: Arc, request: DisconnectRequest) -> Result; + /// Wrapper around [`change_network_id_call()`](Self::change_network_id_call). + async fn change_network_id(self: Arc, network_id: NetworkId) -> Result<()> { + self.change_network_id_call(ChangeNetworkIdRequest { network_id }).await?; + Ok(()) + } + + /// Change the current network id of the wallet. + async fn change_network_id_call(self: Arc, request: ChangeNetworkIdRequest) -> Result; + // --- /// Wrapper around `ping_call()`. - async fn ping(self: Arc, payload: Option) -> Result> { - Ok(self.ping_call(PingRequest { payload }).await?.payload) + async fn ping(self: Arc, message: Option) -> Result> { + Ok(self.ping_call(PingRequest { message }).await?.message) } /// Ping the wallet service. Accepts an optional `u64` value that is returned in the response. async fn ping_call(self: Arc, request: PingRequest) -> Result; @@ -77,7 +100,7 @@ pub trait WalletApi: Send + Sync + AnySync { /// 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) + Ok(self.wallet_enumerate_call(WalletEnumerateRequest {}).await?.wallet_descriptors) } /// Enumerates all wallets available in the storage. Returns `Vec` @@ -100,14 +123,14 @@ pub trait WalletApi: Send + Sync + AnySync { async fn wallet_open( self: Arc, wallet_secret: Secret, - wallet_filename: Option, + filename: Option, account_descriptors: bool, legacy_accounts: bool, ) -> Result>> { Ok(self .wallet_open_call(WalletOpenRequest { wallet_secret, - wallet_filename, + filename, account_descriptors, legacy_accounts: legacy_accounts.then_some(true), }) @@ -241,6 +264,16 @@ pub trait WalletApi: Send + Sync + AnySync { /// around this call. async fn accounts_rename_call(self: Arc, request: AccountsRenameRequest) -> Result; + async fn accounts_select(self: Arc, account_id: Option) -> Result<()> { + self.accounts_select_call(AccountsSelectRequest { account_id }).await?; + Ok(()) + } + + /// Select an account. This call will set the currently *selected* account to the + /// account specified by the `account_id`. The selected account is tracked within + /// the wallet and can be obtained via get_status() API call. + async fn accounts_select_call(self: Arc, request: AccountsSelectRequest) -> 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 @@ -267,7 +300,7 @@ pub trait WalletApi: Send + Sync + AnySync { /// 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) + Ok(self.accounts_enumerate_call(AccountsEnumerateRequest {}).await?.account_descriptors) } /// Returns a list of [`AccountDescriptor`] structs for all accounts stored in the wallet. async fn accounts_enumerate_call(self: Arc, request: AccountsEnumerateRequest) -> Result; @@ -295,6 +328,29 @@ pub trait WalletApi: Send + Sync + AnySync { /// around this call. async fn accounts_create_call(self: Arc, request: AccountsCreateRequest) -> Result; + /// Wrapper around [`accounts_ensure_default_call()`](Self::accounts_ensure_default_call) + async fn accounts_ensure_default( + self: Arc, + wallet_secret: Secret, + payment_secret: Option, + account_kind: AccountKind, + mnemonic_phrase: Option, + ) -> Result { + let request = AccountsEnsureDefaultRequest { wallet_secret, payment_secret, account_kind, mnemonic_phrase }; + Ok(self.accounts_ensure_default_call(request).await?.account_descriptor) + } + + /// Ensure that a default account exists. If the default account does not exist, + /// this call will create a new private key and an associated account and return + /// an [`AccountDescriptor`] for it. A custom mnemonic phrase can be supplied + /// for the private key. This function currently supports only BIP32 accounts. + /// If a `payment_secret` is supplied, the mnemonic phrase will be created + /// with a BIP39 passphrase. + async fn accounts_ensure_default_call( + self: Arc, + request: AccountsEnsureDefaultRequest, + ) -> Result; + // TODO async fn accounts_import_call(self: Arc, request: AccountsImportRequest) -> Result; diff --git a/wallet/core/src/api/transport.rs b/wallet/core/src/api/transport.rs index 4b9039931..9f6485ec8 100644 --- a/wallet/core/src/api/transport.rs +++ b/wallet/core/src/api/transport.rs @@ -10,59 +10,67 @@ //! 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::events::Events; +use crate::imports::*; use crate::result::Result; +use crate::wallet::Wallet; use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_wallet_macros::{build_wallet_client_transport_interface, build_wallet_server_transport_interface}; +use workflow_core::task::spawn; /// Transport interface supporting Borsh serialization #[async_trait] -pub trait BorshTransport: Send + Sync { +pub trait BorshCodec: Send + Sync { async fn call(&self, op: u64, request: Vec) -> Result>; } /// Transport interface supporting Serde JSON serialization #[async_trait] -pub trait SerdeTransport: Send + Sync { +pub trait SerdeCodec: Send + Sync { async fn call(&self, op: &str, request: &str) -> Result; } /// Transport interface enum supporting either Borsh and Serde JSON serialization #[derive(Clone)] -pub enum Transport { - Borsh(Arc), - Serde(Arc), +pub enum Codec { + Borsh(Arc), + Serde(Arc), } -/// [`WalletServer`] is a server-side transport interface that declares -/// API methods that can be invoked via Borsh or Serde messages containing -/// serializations created using the [`Transport`] interface. The [`WalletServer`] -/// is a counter-part to [`WalletClient`]. -pub struct WalletServer { - pub wallet_api: Arc, +/// [`WalletClient`] is a client-side transport interface declaring +/// API methods that can be invoked via WalletApi method calls. +/// [`WalletClient`] is a counter-part to [`WalletServer`]. +pub struct WalletClient { + pub codec: Codec, } -impl WalletServer { - pub fn new(wallet_api: Arc) -> Self { - Self { wallet_api } +impl WalletClient { + pub fn new(codec: Codec) -> Self { + Self { codec } } +} - pub fn wallet_api(&self) -> &Arc { - &self.wallet_api +use workflow_core::channel::{DuplexChannel, 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!() } -} -impl WalletServer { - build_wallet_server_transport_interface! {[ + build_wallet_client_transport_interface! {[ Ping, GetStatus, Connect, Disconnect, + ChangeNetworkId, + RetainContext, Batch, Flush, WalletEnumerate, @@ -79,9 +87,11 @@ impl WalletServer { PrvKeyDataRemove, PrvKeyDataGet, AccountsRename, + AccountsSelect, AccountsEnumerate, AccountsDiscovery, AccountsCreate, + AccountsEnsureDefault, AccountsImport, AccountsActivate, AccountsDeactivate, @@ -97,34 +107,46 @@ impl WalletServer { ]} } -/// [`WalletClient`] is a client-side transport interface declaring -/// API methods that can be invoked via WalletApi method calls. -/// [`WalletClient`] is a counter-part to [`WalletServer`]. -pub struct WalletClient { - pub transport: Transport, +// ---------------------------- + +#[async_trait] +pub trait EventHandler: Send + Sync { + // pub trait EventHandler { + // async fn handle_event(&self, event: &Box); + async fn handle_event(&self, event: &Events); } -impl WalletClient { - pub fn new(transport: Transport) -> Self { - Self { transport } - } +/// [`WalletServer`] is a server-side transport interface that declares +/// API methods that can be invoked via Borsh or Serde messages containing +/// serializations created using the [`Transport`] interface. The [`WalletServer`] +/// is a counter-part to [`WalletClient`]. +pub struct WalletServer { + // pub wallet_api: Arc, + pub wallet: Arc, + pub event_handler: Arc, + task_ctl: DuplexChannel, } -use workflow_core::channel::Receiver; -#[async_trait] -impl WalletApi for WalletClient { - async fn register_notifications(self: Arc, _channel: Receiver) -> Result { - todo!() +impl WalletServer { + // pub fn new(wallet_api: Arc, event_handler : Arc) -> Self { + // Self { wallet_api, event_handler } + pub fn new(wallet: Arc, event_handler: Arc) -> Self { + Self { wallet, event_handler, task_ctl: DuplexChannel::unbounded() } } - async fn unregister_notifications(self: Arc, _channel_id: u64) -> Result<()> { - todo!() + + pub fn wallet_api(&self) -> Arc { + self.wallet.clone() } +} - build_wallet_client_transport_interface! {[ +impl WalletServer { + build_wallet_server_transport_interface! {[ Ping, GetStatus, Connect, Disconnect, + ChangeNetworkId, + RetainContext, Batch, Flush, WalletEnumerate, @@ -141,9 +163,11 @@ impl WalletApi for WalletClient { PrvKeyDataRemove, PrvKeyDataGet, AccountsRename, + AccountsSelect, AccountsEnumerate, AccountsDiscovery, AccountsCreate, + AccountsEnsureDefault, AccountsImport, AccountsActivate, AccountsDeactivate, @@ -158,3 +182,43 @@ impl WalletApi for WalletClient { AddressBookEnumerate, ]} } + +impl WalletServer { + pub fn start(self: &Arc) { + let task_ctl_receiver = self.task_ctl.request.receiver.clone(); + let task_ctl_sender = self.task_ctl.response.sender.clone(); + let events = self.wallet.multiplexer().channel(); + + let this = self.clone(); + spawn(async move { + loop { + select! { + _ = task_ctl_receiver.recv().fuse() => { + break; + }, + + msg = events.receiver.recv().fuse() => { + match msg { + Ok(event) => { + this.event_handler.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(); + }); + } + + pub async fn stop_task(&self) -> Result<()> { + self.task_ctl.signal(()).await.expect("Wallet::stop_task() `signal` error"); + Ok(()) + } +} diff --git a/wallet/core/src/derivation/gen0/import.rs b/wallet/core/src/compat/gen0.rs similarity index 99% rename from wallet/core/src/derivation/gen0/import.rs rename to wallet/core/src/compat/gen0.rs index e89ccc1c7..2d20e690b 100644 --- a/wallet/core/src/derivation/gen0/import.rs +++ b/wallet/core/src/compat/gen0.rs @@ -3,8 +3,7 @@ //! use crate::error::Error; -use crate::result::Result; -use crate::secret::Secret; +use crate::imports::*; use cfb_mode::cipher::AsyncStreamCipher; use cfb_mode::cipher::KeyIvInit; use evpkdf::evpkdf; diff --git a/wallet/core/src/compat/gen1.rs b/wallet/core/src/compat/gen1.rs new file mode 100644 index 000000000..cd66b1016 --- /dev/null +++ b/wallet/core/src/compat/gen1.rs @@ -0,0 +1,224 @@ +use crate::imports::*; +use chacha20poly1305::{aead::AeadMut, Key, KeyInit}; + +pub fn decrypt_mnemonic>( + num_threads: u32, + EncryptedMnemonic { cipher, salt }: EncryptedMnemonic, + pass: &[u8], +) -> Result { + let params = argon2::ParamsBuilder::new().t_cost(1).m_cost(64 * 1024).p_cost(num_threads).output_len(32).build().unwrap(); + let mut key = [0u8; 32]; + argon2::Argon2::new(argon2::Algorithm::Argon2id, Default::default(), params) + .hash_password_into(pass, salt.as_ref(), &mut key[..]) + .unwrap(); + let mut aead = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(&key)); + let (nonce, ciphertext) = cipher.as_ref().split_at(24); + + let decrypted = aead.decrypt(nonce.into(), ciphertext).unwrap(); + Ok(unsafe { String::from_utf8_unchecked(decrypted) }) +} + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod test { + use super::*; + use hex_literal::hex; + use kaspa_addresses::Address; + + #[test] + fn decrypt_go_encrypted_mnemonics_test() { + let file = SingleWalletFileV1{ + encrypted_mnemonic: EncryptedMnemonic { + cipher: hex!("2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc").as_slice(), + salt: hex!("044f5b890e48af4a7dcd7e7766af9380").as_slice(), + }, + xpublic_key: "kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA", + ecdsa: false, + }; + + let decrypted = decrypt_mnemonic(8, file.encrypted_mnemonic, b"").unwrap(); + assert_eq!("dizzy uncover funny time weapon chat volume squirrel comic motion until diamond response remind hurt spider door strategy entire oyster hawk marriage soon fabric", decrypted); + } + + #[tokio::test] + async fn import_golang_single_wallet_test() { + let resident_store = Wallet::resident_store().unwrap(); + let wallet = Arc::new(Wallet::try_new(resident_store, None, Some(NetworkId::new(NetworkType::Mainnet))).unwrap()); + let wallet_secret = Secret::new(vec![]); + + wallet + .create_wallet( + &wallet_secret, + WalletCreateArgs { + title: None, + filename: None, + encryption_kind: EncryptionKind::XChaCha20Poly1305, + user_hint: None, + overwrite_wallet_storage: false, + }, + ) + .await + .unwrap(); + + let file = SingleWalletFileV1{ + encrypted_mnemonic: EncryptedMnemonic { + cipher: hex!("2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc").as_slice(), + salt: hex!("044f5b890e48af4a7dcd7e7766af9380").as_slice(), + }, + xpublic_key: "kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA", + ecdsa: false, + }; + let import_secret = Secret::new(vec![]); + + let acc = wallet.import_kaspawallet_golang_single_v1(&import_secret, &wallet_secret, file).await.unwrap(); + assert_eq!( + acc.receive_address().unwrap(), + Address::try_from("kaspa:qpuvlauc6a5syze9g70dnxzzvykhkuatsjrx87mxqccqh7kf9kcssdkp9ec7w").unwrap(), // taken from golang impl + ); + } + + #[tokio::test] + async fn import_golang_multisig_v1_wallet_test() { + let resident_store = Wallet::resident_store().unwrap(); + let wallet = Arc::new(Wallet::try_new(resident_store, None, Some(NetworkId::new(NetworkType::Mainnet))).unwrap()); + let wallet_secret = Secret::new(vec![]); + + wallet + .create_wallet( + &wallet_secret, + WalletCreateArgs { + title: None, + filename: None, + encryption_kind: EncryptionKind::XChaCha20Poly1305, + user_hint: None, + overwrite_wallet_storage: false, + }, + ) + .await + .unwrap(); + + let file = MultisigWalletFileV1{ + encrypted_mnemonics: vec![ + EncryptedMnemonic { + cipher: hex!("f587dbc539b5303605e7065f4a473caffc91d5992dc0c4ec0b111e5362aa089c6ed034d4165697c13776777fa6a9396b0396515f75fa8fa34d13a3abdbf126bf8575be389177998c77170f3dba80c18d7cb5e223802cd4df51584ea280c08f31a8ecccca31000f4ebd78d584ba95ad2424b57a2945c60a7a36174bf69ecf251c141f01644aeb10268f3321bc2114a24da8ab8983540224e494634889a48f846ceea4238869d1e397f041f5594c53453ea63606a4bb50").as_slice(), + salt: hex!("04fb57493be318c3bb1cddb6dde05e09").as_slice(), + }, + EncryptedMnemonic { + cipher: hex!("2244d1b757e635cec13347d8b6d57c446063b9b72f54c425055eefd983c11cd4d75b0303e47848b5df29991056769c109cad73844fcc4de3d68122fdc09ec31a9e26334cb65141de1fb74718fd44e1d7312eaf975871833026569f06624f02ea79ba189e2db8cbfc4a1ada7fc4801179fb9b838618418043a335e8e01ab9dc8b6b8a1aa963a827a7914bab0815337d3955e5d2a4fc2df738506d5eb537ca7c52c690106bde9d2b686949a2e651099311796df3698499e8606cdbdc9963fc9172b12b").as_slice(), + salt: hex!("60405c5b3a180e4fdebd5a6d5c51bf76").as_slice(), + }, + ], + xpublic_keys: vec![ + "kpub2J937qL9n85s7HrhYyYYdMkzq1kaMiAf9PAcJzRW3jV7NgntNfGGrNgut7ZxcVrJqH42BCT2WyjfnxJh3SBDjLhXHe3UC2RJUu5tcjsViuK", + "kpub2Jtuqt6WJWZv3fQUnKhuEaCxbAyzLsFn3UEEaM4g7CXa2LZjQZH4o6tpj83tFaewMEyX56qrAF4Q64uqunVyBayuuRNwjru5DWchDEcq5vz", + "kpub2JZg9pofE54nqvkhFRRx18pAMhYDPL2CpYqBx2AkzvsEknCh8V4rtez9ZYeab3HCW1Xsm9f4d6J5dfJVg9NADWN7rtqNft21batcii1SjXy", + "kpub2HuRXjAmhs3KwQ9WpHVaiHRjBP37TQUiUGFQBTwp7cdbArCo5s2MT6415nd3ZYaELvNbZ4qTJjCGTavExv514tWftaGQzCK8gQz6BQJNySp", + "kpub2KCvcuKVgfy1h7PvCw4xFcdLAPoerVZBG4qTo8vRGH2Qe6p5AgLyRek5CEnuCDkduXHqgwtvaVfYYBS7gQBR1J4XowdvqvPXsHZGA5WyRJF", + ], + required_signatures: 2, + cosigner_index: 1, + ecdsa: false, + }; + let import_secret = Secret::new(vec![]); + + let acc = wallet.import_kaspawallet_golang_multisig_v1(&import_secret, &wallet_secret, file).await.unwrap(); + assert_eq!( + acc.receive_address().unwrap(), + Address::try_from("kaspa:pqvgkyjeuxmd8k70egrrzpdz5rqj0acmr6y94mwsltxfp6nc50742295c3998").unwrap(), // taken from golang impl + ); + } + + #[test] + fn deser_golang_wallet_test() { + #[allow(dead_code)] + #[derive(Debug)] + enum WalletType<'a> { + SingleV0(SingleWalletFileV0<'a, Vec>), + SingleV1(SingleWalletFileV1<'a, Vec>), + MultiV0(MultisigWalletFileV0<'a, Vec>), + MultiV1(MultisigWalletFileV1<'a, Vec>), + } + + #[derive(Debug, Default, Deserialize)] + struct EncryptedMnemonicIntermediate { + #[serde(with = "kaspa_utils::serde_bytes")] + cipher: Vec, + #[serde(with = "kaspa_utils::serde_bytes")] + salt: Vec, + } + impl From for EncryptedMnemonic> { + fn from(value: EncryptedMnemonicIntermediate) -> Self { + Self { cipher: value.cipher, salt: value.salt } + } + } + + #[derive(serde_repr::Deserialize_repr, PartialEq, Debug)] + #[repr(u8)] + enum WalletVersion { + Zero = 0, + One = 1, + } + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct UnifiedWalletIntermediate<'a> { + version: WalletVersion, + num_threads: Option, + encrypted_mnemonics: Vec, + #[serde(borrow)] + public_keys: Vec<&'a str>, + minimum_signatures: u16, + cosigner_index: u8, + ecdsa: bool, + } + + impl<'a> UnifiedWalletIntermediate<'a> { + fn into_wallet_type(mut self) -> WalletType<'a> { + let single = self.encrypted_mnemonics.len() == 1 && self.public_keys.len() == 1; + match (single, self.version) { + (true, WalletVersion::Zero) => WalletType::SingleV0(SingleWalletFileV0 { + num_threads: self.num_threads.expect("num_threads must present in case of v0") as u32, + encrypted_mnemonic: std::mem::take(&mut self.encrypted_mnemonics[0]).into(), + xpublic_key: self.public_keys[0], + ecdsa: self.ecdsa, + }), + (true, WalletVersion::One) => WalletType::SingleV1(SingleWalletFileV1 { + encrypted_mnemonic: std::mem::take(&mut self.encrypted_mnemonics[0]).into(), + xpublic_key: self.public_keys[0], + ecdsa: self.ecdsa, + }), + (false, WalletVersion::Zero) => WalletType::MultiV0(MultisigWalletFileV0 { + num_threads: self.num_threads.expect("num_threads must present in case of v0") as u32, + encrypted_mnemonics: self + .encrypted_mnemonics + .into_iter() + .map(|EncryptedMnemonicIntermediate { cipher, salt }| EncryptedMnemonic { cipher, salt }) + .collect(), + xpublic_keys: self.public_keys, + required_signatures: self.minimum_signatures, + cosigner_index: self.cosigner_index, + ecdsa: self.ecdsa, + }), + (false, WalletVersion::One) => WalletType::MultiV1(MultisigWalletFileV1 { + encrypted_mnemonics: self + .encrypted_mnemonics + .into_iter() + .map(|EncryptedMnemonicIntermediate { cipher, salt }| EncryptedMnemonic { cipher, salt }) + .collect(), + xpublic_keys: self.public_keys, + required_signatures: self.minimum_signatures, + cosigner_index: self.cosigner_index, + ecdsa: self.ecdsa, + }), + } + } + } + + let single_json_v0 = r#"{"numThreads":8,"version":0,"encryptedMnemonics":[{"cipher":"2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc","salt":"044f5b890e48af4a7dcd7e7766af9380"}],"publicKeys":["kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA"],"minimumSignatures":1,"cosignerIndex":0,"lastUsedExternalIndex":0,"lastUsedInternalIndex":0,"ecdsa":false}"#.to_owned(); + let single_json_v1 = r#"{"version":1,"encryptedMnemonics":[{"cipher":"2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc","salt":"044f5b890e48af4a7dcd7e7766af9380"}],"publicKeys":["kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA"],"minimumSignatures":1,"cosignerIndex":0,"lastUsedExternalIndex":0,"lastUsedInternalIndex":0,"ecdsa":false}"#.to_owned(); + + let unified: UnifiedWalletIntermediate = serde_json::from_str(&single_json_v0).unwrap(); + assert!(matches!(unified.into_wallet_type(), WalletType::SingleV0(_))); + let unified: UnifiedWalletIntermediate = serde_json::from_str(&single_json_v1).unwrap(); + assert!(matches!(unified.into_wallet_type(), WalletType::SingleV1(_))); + } +} diff --git a/wallet/core/src/compat/mod.rs b/wallet/core/src/compat/mod.rs new file mode 100644 index 000000000..79c8e11dd --- /dev/null +++ b/wallet/core/src/compat/mod.rs @@ -0,0 +1,4 @@ +pub mod gen0; +pub use gen0::*; +pub mod gen1; +pub use gen1::*; diff --git a/wallet/core/src/cryptobox.rs b/wallet/core/src/cryptobox.rs new file mode 100644 index 000000000..fa9b188f3 --- /dev/null +++ b/wallet/core/src/cryptobox.rs @@ -0,0 +1,41 @@ +use crate::imports::*; +use crypto_box::{ + aead::{Aead, AeadCore, OsRng}, + ChaChaBox, +}; +pub use crypto_box::{PublicKey, SecretKey}; + +// https://docs.rs/crypto_box/0.9.1/crypto_box/ + +pub struct CryptoBox { + public_key: PublicKey, + codec: ChaChaBox, +} + +impl CryptoBox { + pub fn new(secret_key: &SecretKey, peer_public_key: &PublicKey) -> CryptoBox { + let cha_cha_box = ChaChaBox::new(peer_public_key, secret_key); + CryptoBox { public_key: secret_key.public_key(), codec: cha_cha_box } + } + + pub fn secret_to_public_key(secret_key: &[u8]) -> Vec { + let secret_key = SecretKey::from_slice(secret_key).unwrap(); + secret_key.public_key().as_bytes().to_vec() + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn encrypt(&self, plaintext: &[u8]) -> Result> { + let nonce = ChaChaBox::generate_nonce(&mut OsRng); + Ok(nonce.into_iter().chain(self.codec.encrypt(&nonce, plaintext)?).collect::>()) + } + + pub fn decrypt(&self, ciphertext: &[u8]) -> Result> { + if ciphertext.len() < 24 + 1 { + return Err(Error::CipherMessageTooShort); + } + Ok(self.codec.decrypt((&ciphertext[0..24]).into(), &ciphertext[24..])?) + } +} diff --git a/wallet/core/src/derivation/mod.rs b/wallet/core/src/derivation.rs similarity index 90% rename from wallet/core/src/derivation/mod.rs rename to wallet/core/src/derivation.rs index d9bd0c6b0..15ad78503 100644 --- a/wallet/core/src/derivation/mod.rs +++ b/wallet/core/src/derivation.rs @@ -2,16 +2,15 @@ //! Module handling bip32 address derivation (bip32+bip44 and legacy accounts) //! -pub mod gen0; -pub mod gen1; -pub mod traits; +use kaspa_wallet_keys::derivation::gen0::{PubkeyDerivationManagerV0, WalletDerivationManagerV0}; +use kaspa_wallet_keys::derivation::gen1::{PubkeyDerivationManager, WalletDerivationManager}; -pub use traits::*; +pub use kaspa_wallet_keys::derivation::traits::*; +use kaspa_wallet_keys::publickey::{PublicKey, PublicKeyArrayT, PublicKeyT}; +pub use kaspa_wallet_keys::types::*; 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::result::Result; @@ -20,7 +19,6 @@ use kaspa_consensus_core::network::NetworkType; use kaspa_txscript::{ extract_script_pub_key_address, multisig_redeem_script, multisig_redeem_script_ecdsa, pay_to_script_hash_script, }; -use workflow_wasm::serde::from_value; #[derive(Default, Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct AddressDerivationMeta([u32; 2]); @@ -95,7 +93,7 @@ impl AddressManager { let list = self.pubkey_managers.iter().map(|m| m.current_pubkey()); // let keys = join_all(list).await.into_iter().collect::>>()?; - let keys = list.into_iter().collect::>>()?; + let keys = list.into_iter().collect::>>()?; let address = self.create_address(keys)?; self.update_address_to_index_map(self.index(), &[address.clone()])?; @@ -128,7 +126,7 @@ impl AddressManager { let list = self.pubkey_managers.iter().map(|m| m.get_range(indexes.clone())); - let manager_keys = list.into_iter().collect::>>()?; + let manager_keys = list.into_iter().collect::>>()?; let is_multisig = manager_length > 1; @@ -204,14 +202,12 @@ impl AddressDerivationManager { let mut derivators = vec![]; 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)?) - } + LEGACY_ACCOUNT_KIND => Arc::new(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(xpub.clone(), Some(cosigner_index))?) + Arc::new(WalletDerivationManager::from_extended_public_key(xpub.clone(), Some(cosigner_index))?) } - _ => Arc::new(gen1::WalletDerivationManager::from_extended_public_key(xpub.clone(), cosigner_index)?), + _ => Arc::new(WalletDerivationManager::from_extended_public_key(xpub.clone(), cosigner_index)?), }; receive_pubkey_managers.push(derivator.receive_pubkey_manager()); @@ -258,7 +254,7 @@ impl AddressDerivationManager { let mut receive_pubkey_managers = vec![]; let mut change_pubkey_managers = vec![]; let derivator: Arc = - Arc::new(gen0::WalletDerivationManagerV0::create_uninitialized(account_index, None, None)?); + Arc::new(WalletDerivationManagerV0::create_uninitialized(account_index, None, None)?); receive_pubkey_managers.push(derivator.receive_pubkey_manager()); change_pubkey_managers.push(derivator.change_pubkey_manager()); @@ -459,33 +455,28 @@ pub fn create_multisig_address( Ok(address) } -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(extends = js_sys::Array, typescript_type="Array")] - pub type PublicKeys; -} - +/// @category Wallet SDK #[wasm_bindgen(js_name=createAddress)] pub fn create_address_js( - key: &str, + key: PublicKeyT, network_type: NetworkType, ecdsa: Option, account_kind: Option, ) -> Result
{ - let key: secp256k1::PublicKey = from_value(key.into())?; - create_address(1, vec![key], network_type.into(), ecdsa.unwrap_or(false), account_kind) + let public_key = PublicKey::try_cast_from(key)?; + create_address(1, vec![public_key.as_ref().try_into()?], network_type.into(), ecdsa.unwrap_or(false), account_kind) } +/// @category Wallet SDK #[wasm_bindgen(js_name=createMultisigAddress)] pub fn create_multisig_address_js( minimum_signatures: usize, - keys: PublicKeys, + keys: PublicKeyArrayT, network_type: NetworkType, ecdsa: Option, account_kind: Option, ) -> Result
{ - let keys: Vec = from_value(keys.into())?; - create_address(minimum_signatures, keys, network_type.into(), ecdsa.unwrap_or(false), account_kind) + create_address(minimum_signatures, keys.try_into()?, network_type.into(), ecdsa.unwrap_or(false), account_kind) } pub fn create_address( @@ -505,9 +496,9 @@ pub fn create_address( } if account_kind.map(|kind| kind == LEGACY_ACCOUNT_KIND).unwrap_or(false) { - PubkeyDerivationManagerV0::create_address(&keys[0], prefix, ecdsa) + Ok(PubkeyDerivationManagerV0::create_address(&keys[0], prefix, ecdsa)?) } else { - PubkeyDerivationManager::create_address(&keys[0], prefix, ecdsa) + Ok(PubkeyDerivationManager::create_address(&keys[0], prefix, ecdsa)?) } } @@ -523,7 +514,7 @@ pub async fn create_xpub_from_mnemonic( 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)?, + _ => WalletDerivationManager::derive_extended_key_from_master_key(xkey, false, account_index)?, }; let xkey = ExtendedPublicKey { public_key: secret_key.get_public_key(), attrs }; @@ -555,10 +546,10 @@ pub fn build_derivate_path( address_type: AddressType, ) -> Result { 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)), + LEGACY_ACCOUNT_KIND => Ok(WalletDerivationManagerV0::build_derivate_path(account_index, Some(address_type))?), + BIP32_ACCOUNT_KIND => Ok(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)) + Ok(WalletDerivationManager::build_derivate_path(true, account_index, Some(cosigner_index), Some(address_type))?) } _ => { panic!("build derivate path not supported for account kind: {:?}", account_kind); diff --git a/wallet/core/src/derivation/gen1/import.rs b/wallet/core/src/derivation/gen1/import.rs deleted file mode 100644 index cfc20484e..000000000 --- a/wallet/core/src/derivation/gen1/import.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::result::Result; -use crate::secret::Secret; -pub struct PrivateKeyDataV1; - -pub async fn load_v1_keydata(_phrase: &Secret) -> Result { - unimplemented!() -} diff --git a/wallet/core/src/deterministic.rs b/wallet/core/src/deterministic.rs index c774fa097..c383da601 100644 --- a/wallet/core/src/deterministic.rs +++ b/wallet/core/src/deterministic.rs @@ -50,6 +50,27 @@ impl ToHex for AccountId { } } +impl FromHex for AccountId { + type Error = Error; + fn from_hex(hex_str: &str) -> Result { + Ok(Self(Hash::from_hex(hex_str)?)) + } +} + +impl TryFrom<&JsValue> for AccountId { + type Error = Error; + fn try_from(value: &JsValue) -> Result { + let string = value.as_string().ok_or(Error::InvalidAccountId(format!("{value:?}")))?; + Self::from_hex(&string) + } +} + +impl From for JsValue { + fn from(value: AccountId) -> Self { + JsValue::from(value.to_hex()) + } +} + 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/encryption.rs b/wallet/core/src/encryption.rs index 3b7b2c546..bee57a1f5 100644 --- a/wallet/core/src/encryption.rs +++ b/wallet/core/src/encryption.rs @@ -4,9 +4,7 @@ use crate::imports::*; use crate::result::Result; -use crate::secret::Secret; use argon2::Argon2; -use base64::{engine::general_purpose, Engine as _}; use chacha20poly1305::{ aead::{AeadCore, AeadInPlace, KeyInit, OsRng}, Key, XChaCha20Poly1305, @@ -16,8 +14,9 @@ use std::ops::{Deref, DerefMut}; use zeroize::Zeroize; /// Encryption algorithms supported by the Wallet framework. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub enum EncryptionKind { + #[default] XChaCha20Poly1305, } @@ -204,31 +203,8 @@ impl Encrypted { } } -/// WASM32 binding for `SHA256` hash function. -#[wasm_bindgen(js_name = "sha256")] -pub fn js_sha256_hash(data: JsValue) -> Result { - let data = data.try_as_vec_u8()?; - let hash = sha256_hash(&data); - Ok(hash.as_ref().to_hex()) -} - -/// WASM32 binding for `SHA256d` hash function. -#[wasm_bindgen(js_name = "sha256d")] -pub fn js_sha256d_hash(data: JsValue) -> Result { - let data = data.try_as_vec_u8()?; - let hash = sha256d_hash(&data); - Ok(hash.as_ref().to_hex()) -} - -/// WASM32 binding for `argon2sha256iv` hash function. -#[wasm_bindgen(js_name = "argon2sha256iv")] -pub fn js_argon2_sha256iv_phash(data: JsValue, byte_length: usize) -> Result { - let data = data.try_as_vec_u8()?; - let hash = argon2_sha256iv_hash(&data, byte_length)?; - Ok(hash.as_ref().to_hex()) -} - /// Produces `SHA256` hash of the given data. +#[inline] pub fn sha256_hash(data: &[u8]) -> Secret { let mut sha256 = Sha256::default(); sha256.update(data); @@ -236,6 +212,7 @@ pub fn sha256_hash(data: &[u8]) -> Secret { } /// Produces `SHA256d` hash of the given data. +#[inline] pub fn sha256d_hash(data: &[u8]) -> Secret { let mut sha256 = Sha256::default(); sha256.update(data); @@ -250,14 +227,6 @@ pub fn argon2_sha256iv_hash(data: &[u8], byte_length: usize) -> Result { Ok(key.into()) } -/// WASM32 binding for `encryptXChaCha20Poly1305` function. -#[wasm_bindgen(js_name = "encryptXChaCha20Poly1305")] -pub fn js_encrypt_xchacha20poly1305(text: String, password: String) -> Result { - let secret = sha256_hash(password.as_bytes()); - let encrypted = encrypt_xchacha20poly1305(text.as_bytes(), &secret)?; - Ok(general_purpose::STANDARD.encode(encrypted)) -} - /// Encrypts the given data using `XChaCha20Poly1305` algorithm. pub fn encrypt_xchacha20poly1305(data: &[u8], secret: &Secret) -> Result> { let private_key_bytes = argon2_sha256iv_hash(secret.as_ref(), 32)?; @@ -271,15 +240,6 @@ pub fn encrypt_xchacha20poly1305(data: &[u8], secret: &Secret) -> Result Ok(buffer) } -/// WASM32 binding for `decryptXChaCha20Poly1305` function. -#[wasm_bindgen(js_name = "decryptXChaCha20Poly1305")] -pub fn js_decrypt_xchacha20poly1305(text: String, password: String) -> Result { - let secret = sha256_hash(password.as_bytes()); - let bytes = general_purpose::STANDARD.decode(text)?; - let encrypted = decrypt_xchacha20poly1305(bytes.as_ref(), &secret)?; - Ok(String::from_utf8(encrypted.as_ref().to_vec())?) -} - /// Decrypts the given data using `XChaCha20Poly1305` algorithm. pub fn decrypt_xchacha20poly1305(data: &[u8], secret: &Secret) -> Result { let private_key_bytes = argon2_sha256iv_hash(secret.as_ref(), 32)?; diff --git a/wallet/core/src/error.rs b/wallet/core/src/error.rs index ff525307b..a89b1dcf0 100644 --- a/wallet/core/src/error.rs +++ b/wallet/core/src/error.rs @@ -24,6 +24,9 @@ pub enum Error { #[error("{0}")] Custom(String), + #[error(transparent)] + WalletKeys(#[from] kaspa_wallet_keys::error::Error), + #[error("please select an account")] AccountSelection, @@ -135,9 +138,12 @@ pub enum Error { #[error(transparent)] ParseFloatError(#[from] std::num::ParseFloatError), - #[error("Unable to decrypt this wallet")] + #[error("Unable to decrypt")] Chacha20poly1305(chacha20poly1305::Error), + #[error("Unable to decrypt this wallet")] + WalletDecrypt(chacha20poly1305::Error), + #[error(transparent)] FromUtf8Error(#[from] std::string::FromUtf8Error), @@ -171,6 +177,9 @@ pub enum Error { #[error("wallet secret is required")] WalletSecretRequired, + #[error("Supplied secret in key '{0}' is empty")] + SecretIsEmpty(String), + #[error("task aborted")] Aborted, @@ -186,6 +195,12 @@ pub enum Error { #[error("Account not active: {0}")] AccountNotActive(AccountId), + #[error("Invalid account id: {0}")] + InvalidAccountId(String), + + #[error("Invalid id: {0}")] + InvalidKeyDataId(String), + #[error("Invalid account type (must be one of: bip32|multisig|legacy")] InvalidAccountKind, @@ -225,12 +240,18 @@ pub enum Error { #[error("{0}")] DowncastError(String), + #[error(transparent)] + ConsensusClient(#[from] kaspa_consensus_client::error::Error), + #[error(transparent)] ConsensusWasm(#[from] kaspa_consensus_wasm::error::Error), - #[error("Fees::Include or Fees::Exclude are not allowed in sweep transactions")] + #[error("Fees::SenderPays or Fees::ReceiverPays are not allowed in sweep transactions")] GeneratorFeesInSweepTransaction, + #[error("Transactions with output must have Fees::SenderPays or Fees::ReceiverPays")] + GeneratorNoFeesForFinalTransaction, + #[error("Change address does not match supplied network type")] GeneratorChangeAddressNetworkTypeMismatch, @@ -281,6 +302,30 @@ pub enum Error { #[error("Mass calculation error")] MassCalculationError, + + #[error("Invalid argument: {0}")] + InvalidArgument(String), + + #[error("Unable to convert BigInt value {0}")] + BigInt(String), + + #[error("Invalid mnemonic phrase")] + InvalidMnemonicPhrase, + + #[error("Invalid transaction kind {0}")] + InvalidTransactionKind(String), + + #[error("Cipher message is too short")] + CipherMessageTooShort, + + #[error("Invalid secret key length")] + InvalidPrivateKeyLength, + + #[error("Invalid public key length")] + InvalidPublicKeyLength, + + #[error(transparent)] + Metrics(#[from] kaspa_metrics_core::error::Error), } impl From for Error { diff --git a/wallet/core/src/events.rs b/wallet/core/src/events.rs index 913f2ca8c..63d7d5bca 100644 --- a/wallet/core/src/events.rs +++ b/wallet/core/src/events.rs @@ -7,11 +7,12 @@ use crate::imports::*; use crate::storage::{Hint, PrvKeyDataInfo, StorageDescriptor, TransactionRecord, WalletDescriptor}; use crate::utxo::context::UtxoContextId; +use transaction::TransactionRecordNotification; /// Sync state of the kaspad node -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "kebab-case")] -#[serde(tag = "sync", content = "state")] +#[serde(tag = "type", content = "data")] pub enum SyncState { Proof { level: u64, @@ -48,15 +49,16 @@ impl SyncState { } /// Events emitted by the wallet framework -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "kebab-case")] -#[serde(tag = "event", content = "data")] +#[serde(tag = "type", content = "data")] pub enum Events { + WalletPing, /// Successful RPC connection Connect { #[serde(rename = "networkId")] network_id: NetworkId, - /// Kaspa node RPC url on which connection + /// Node RPC url on which connection /// has been established url: Option, }, @@ -69,13 +71,14 @@ pub enum Events { /// A special event emitted if the connected node /// does not have UTXO index enabled UtxoIndexNotEnabled { - /// Kaspa node RPC url on which connection + /// Node RPC url on which connection /// has been established url: Option, }, /// [`SyncState`] notification posted /// when the node sync state changes SyncState { + #[serde(rename = "syncState")] sync_state: SyncState, }, /// Emitted after the wallet has loaded and @@ -104,6 +107,7 @@ pub enum Events { /// Wallet has been closed WalletClose, PrvKeyDataCreate { + #[serde(rename = "prvKeyDataInfo")] prv_key_data_info: PrvKeyDataInfo, }, /// Accounts have been activated @@ -136,7 +140,7 @@ pub enum Events { server_version: String, #[serde(rename = "isSynced")] is_synced: bool, - /// Kaspa node RPC url on which connection + /// Node RPC url on which connection /// has been established url: Option, }, @@ -156,7 +160,8 @@ pub enum Events { message: String, }, /// DAA score change - DAAScoreChange { + DaaScoreChange { + #[serde(rename = "currentDaaScore")] current_daa_score: u64, }, /// New incoming pending UTXO/transaction @@ -202,19 +207,202 @@ pub enum Events { /// 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, 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, }, + /// Periodic metrics updates (on-request) + Metrics { + #[serde(rename = "networkId")] + network_id: NetworkId, + // #[serde(rename = "metricsData")] + // metrics_data: MetricsData, + metrics: MetricsUpdate, + }, /// A general wallet framework error, emitted when an unexpected /// error occurs within the wallet framework. Error { message: String, }, } + +impl Events { + pub fn kind(&self) -> String { + EventKind::from(self).to_string() + } + + pub fn to_js_value(&self) -> wasm_bindgen::JsValue { + match self { + Events::Pending { record } + | Events::Reorg { record } + | Events::Stasis { record } + | Events::Maturity { record } + | Events::Discovery { record } => TransactionRecordNotification::new(self.kind(), record.clone()).into(), + _ => serde_wasm_bindgen::to_value(self).unwrap(), + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Eq, PartialEq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum EventKind { + All, + Connect, + Disconnect, + UtxoIndexNotEnabled, + SyncState, + WalletStart, + WalletHint, + WalletOpen, + WalletCreate, + WalletReload, + WalletError, + WalletClose, + PrvKeyDataCreate, + AccountActivation, + AccountDeactivation, + AccountSelection, + AccountCreate, + AccountUpdate, + ServerStatus, + UtxoProcStart, + UtxoProcStop, + UtxoProcError, + DaaScoreChange, + Pending, + Reorg, + Stasis, + Maturity, + Discovery, + Balance, + Metrics, + Error, +} + +impl From<&Events> for EventKind { + fn from(event: &Events) -> Self { + match event { + Events::WalletPing { .. } => EventKind::WalletStart, + + Events::Connect { .. } => EventKind::Connect, + Events::Disconnect { .. } => EventKind::Disconnect, + Events::UtxoIndexNotEnabled { .. } => EventKind::UtxoIndexNotEnabled, + Events::SyncState { .. } => EventKind::SyncState, + Events::WalletHint { .. } => EventKind::WalletHint, + Events::WalletOpen { .. } => EventKind::WalletOpen, + Events::WalletCreate { .. } => EventKind::WalletCreate, + Events::WalletReload { .. } => EventKind::WalletReload, + Events::WalletError { .. } => EventKind::WalletError, + Events::WalletClose => EventKind::WalletClose, + Events::PrvKeyDataCreate { .. } => EventKind::PrvKeyDataCreate, + Events::AccountActivation { .. } => EventKind::AccountActivation, + Events::AccountDeactivation { .. } => EventKind::AccountDeactivation, + Events::AccountSelection { .. } => EventKind::AccountSelection, + Events::AccountCreate { .. } => EventKind::AccountCreate, + Events::AccountUpdate { .. } => EventKind::AccountUpdate, + Events::ServerStatus { .. } => EventKind::ServerStatus, + Events::UtxoProcStart => EventKind::UtxoProcStart, + Events::UtxoProcStop => EventKind::UtxoProcStop, + Events::UtxoProcError { .. } => EventKind::UtxoProcError, + Events::DaaScoreChange { .. } => EventKind::DaaScoreChange, + Events::Pending { .. } => EventKind::Pending, + Events::Reorg { .. } => EventKind::Reorg, + Events::Stasis { .. } => EventKind::Stasis, + Events::Maturity { .. } => EventKind::Maturity, + Events::Discovery { .. } => EventKind::Discovery, + Events::Balance { .. } => EventKind::Balance, + Events::Metrics { .. } => EventKind::Metrics, + Events::Error { .. } => EventKind::Error, + } + } +} + +impl FromStr for EventKind { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "*" => Ok(EventKind::All), + "connect" => Ok(EventKind::Connect), + "disconnect" => Ok(EventKind::Disconnect), + "utxo-index-not-enabled" => Ok(EventKind::UtxoIndexNotEnabled), + "sync-state" => Ok(EventKind::SyncState), + "wallet-start" => Ok(EventKind::WalletStart), + "wallet-hint" => Ok(EventKind::WalletHint), + "wallet-open" => Ok(EventKind::WalletOpen), + "wallet-create" => Ok(EventKind::WalletCreate), + "wallet-reload" => Ok(EventKind::WalletReload), + "wallet-error" => Ok(EventKind::WalletError), + "wallet-close" => Ok(EventKind::WalletClose), + "prv-key-data-create" => Ok(EventKind::PrvKeyDataCreate), + "account-activation" => Ok(EventKind::AccountActivation), + "account-deactivation" => Ok(EventKind::AccountDeactivation), + "account-selection" => Ok(EventKind::AccountSelection), + "account-create" => Ok(EventKind::AccountCreate), + "account-update" => Ok(EventKind::AccountUpdate), + "server-status" => Ok(EventKind::ServerStatus), + "utxo-proc-start" => Ok(EventKind::UtxoProcStart), + "utxo-proc-stop" => Ok(EventKind::UtxoProcStop), + "utxo-proc-error" => Ok(EventKind::UtxoProcError), + "daa-score-change" => Ok(EventKind::DaaScoreChange), + "pending" => Ok(EventKind::Pending), + "reorg" => Ok(EventKind::Reorg), + "stasis" => Ok(EventKind::Stasis), + "maturity" => Ok(EventKind::Maturity), + "discovery" => Ok(EventKind::Discovery), + "balance" => Ok(EventKind::Balance), + "metrics" => Ok(EventKind::Metrics), + "error" => Ok(EventKind::Error), + _ => Err(Error::custom("Invalid event kind")), + } + } +} + +impl TryFrom for EventKind { + type Error = Error; + fn try_from(js_value: JsValue) -> Result { + let s = js_value.as_string().ok_or_else(|| Error::custom("Invalid event kind"))?; + EventKind::from_str(&s) + } +} + +impl std::fmt::Display for EventKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let str = match self { + EventKind::All => "all", + EventKind::WalletStart => "wallet-start", + EventKind::Connect => "connect", + EventKind::Disconnect => "disconnect", + EventKind::UtxoIndexNotEnabled => "utxo-index-not-enabled", + EventKind::SyncState => "sync-state", + EventKind::WalletHint => "wallet-hint", + EventKind::WalletOpen => "wallet-open", + EventKind::WalletCreate => "wallet-create", + EventKind::WalletReload => "wallet-reload", + EventKind::WalletError => "wallet-error", + EventKind::WalletClose => "wallet-close", + EventKind::PrvKeyDataCreate => "prv-key-data-create", + EventKind::AccountActivation => "account-activation", + EventKind::AccountDeactivation => "account-deactivation", + EventKind::AccountSelection => "account-selection", + EventKind::AccountCreate => "account-create", + EventKind::AccountUpdate => "account-update", + EventKind::ServerStatus => "server-status", + EventKind::UtxoProcStart => "utxo-proc-start", + EventKind::UtxoProcStop => "utxo-proc-stop", + EventKind::UtxoProcError => "utxo-proc-error", + EventKind::DaaScoreChange => "daa-score-change", + EventKind::Pending => "pending", + EventKind::Reorg => "reorg", + EventKind::Stasis => "stasis", + EventKind::Maturity => "maturity", + EventKind::Discovery => "discovery", + EventKind::Balance => "balance", + EventKind::Metrics => "metrics", + EventKind::Error => "error", + }; + + write!(f, "{str}") + } +} diff --git a/wallet/core/src/imports.rs b/wallet/core/src/imports.rs index 21c026198..2d2ce79fd 100644 --- a/wallet/core/src/imports.rs +++ b/wallet/core/src/imports.rs @@ -9,16 +9,15 @@ 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::events::{EventKind, Events, SyncState}; pub use crate::factory::{factories, Factory}; +pub use crate::metrics::{MetricsUpdate, MetricsUpdateKind}; 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::tx::MassCombinationStrategy; -pub use crate::types::*; pub use crate::utxo::balance::Balance; pub use crate::utxo::scan::{Scan, ScanExtent}; pub use crate::utxo::{Maturity, NetworkParams, OutgoingTransaction, UtxoContext, UtxoEntryReference, UtxoProcessor}; @@ -26,6 +25,7 @@ pub use crate::wallet::*; pub use crate::{storage, utils}; pub use ahash::{AHashMap, AHashSet}; +pub use async_std::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; pub use async_trait::async_trait; pub use borsh::{BorshDeserialize, BorshSerialize}; pub use cfg_if::cfg_if; @@ -37,8 +37,11 @@ pub use js_sys::{Array, BigInt, Object}; pub use kaspa_addresses::{Address, Prefix}; pub use kaspa_consensus_core::network::{NetworkId, NetworkType}; pub use kaspa_consensus_core::tx::{ScriptPublicKey, TransactionId, TransactionIndexType}; +pub use kaspa_metrics_core::{Metric, Metrics, MetricsSnapshot}; pub use kaspa_utils::hashmap::*; pub use kaspa_utils::hex::{FromHex, ToHex}; +pub use kaspa_wallet_keys::secret::Secret; +pub use kaspa_wallet_keys::types::*; pub use pad::PadStr; pub use separator::Separatable; pub use serde::{Deserialize, Deserializer, Serialize}; @@ -53,5 +56,10 @@ 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 zeroize::*; + +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + pub use workflow_wasm::convert::CastFromJs; + } +} diff --git a/wallet/core/src/lib.rs b/wallet/core/src/lib.rs index 350ef3090..6029d6c4c 100644 --- a/wallet/core/src/lib.rs +++ b/wallet/core/src/lib.rs @@ -40,7 +40,7 @@ //! The `kaspa-wasm` module is a pure WASM32 module that includes //! the entire wallet framework, but does not support RPC due to an absence //! of a native WebSocket in NodeJs environment, while -//! the `kaspa` module includes `isomorphic-ws` dependency simulating +//! the `kaspa` module includes `websocket` module dependency simulating //! the W3C WebSocket and thus supports RPC. //! //! JavaScript examples for using this framework can be found at: @@ -54,8 +54,40 @@ extern crate alloc; extern crate self as kaspa_wallet_core; +// use cfg_if::cfg_if; + +// cfg_if! { +// if #[cfg(feature = "wasm32-core")] { +// // pub mod wasm; +// // pub use wasm::*; + +// pub mod account; +// pub mod api; +// pub mod compat; +// 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 serializer; +// pub mod settings; +// pub mod storage; +// pub mod tx; +// pub mod utils; +// pub mod utxo; +// pub mod wallet; + +// } else if #[cfg(any(feature = "wasm32-sdk", not(target_arch = "wasm32")))] { pub mod account; pub mod api; +pub mod compat; +pub mod cryptobox; pub mod derivation; pub mod deterministic; pub mod encryption; @@ -64,18 +96,22 @@ pub mod events; pub mod factory; mod imports; pub mod message; +pub mod metrics; pub mod prelude; pub mod result; pub mod rpc; -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; +// } + +// } + +#[cfg(any(feature = "wasm32-sdk", feature = "wasm32-core"))] pub mod wasm; /// Returns the version of the Wallet framework. diff --git a/wallet/core/src/message.rs b/wallet/core/src/message.rs index 5368eda98..160c8f040 100644 --- a/wallet/core/src/message.rs +++ b/wallet/core/src/message.rs @@ -18,8 +18,8 @@ impl AsRef<[u8]> for PersonalMessage<'_> { pub fn sign_message(msg: &PersonalMessage, privkey: &[u8; 32]) -> Result, Error> { let hash = calc_personal_message_hash(msg); - let msg = secp256k1::Message::from_slice(hash.as_bytes().as_slice())?; - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice())?; + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref(); Ok(sig.to_vec()) @@ -32,7 +32,7 @@ pub fn sign_message(msg: &PersonalMessage, privkey: &[u8; 32]) -> Result /// pub fn verify_message(msg: &PersonalMessage, signature: &Vec, pubkey: &XOnlyPublicKey) -> Result<(), Error> { let hash = calc_personal_message_hash(msg); - let msg = secp256k1::Message::from_slice(hash.as_bytes().as_slice())?; + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice())?; let sig = secp256k1::schnorr::Signature::from_slice(signature.as_slice())?; sig.verify(&msg, pubkey) } @@ -52,8 +52,8 @@ mod tests { fn sign_message_with_aux_rand(msg: &PersonalMessage, privkey: &[u8; 32], aux_rand: &[u8; 32]) -> Result, Error> { let hash = calc_personal_message_hash(msg); - let msg = secp256k1::Message::from_slice(hash.as_bytes().as_slice())?; - let schnorr_key = secp256k1::KeyPair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice())?; + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; let curve = secp256k1::Secp256k1::new(); let sig: [u8; 64] = *curve.sign_schnorr_with_aux_rand(&msg, &schnorr_key, aux_rand).as_ref(); diff --git a/wallet/core/src/metrics.rs b/wallet/core/src/metrics.rs new file mode 100644 index 000000000..87a3f9913 --- /dev/null +++ b/wallet/core/src/metrics.rs @@ -0,0 +1,44 @@ +use crate::imports::*; +// use kaspa_metrics_core::MetricsSnapshot; + +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(tag = "type", content = "data")] +#[serde(rename_all = "kebab-case")] +pub enum MetricsUpdate { + WalletMetrics { + #[serde(rename = "mempoolSize")] + mempool_size: u64, + #[serde(rename = "nodePeers")] + node_peers: u32, + #[serde(rename = "networkTPS")] + network_tps: f64, + }, + // NodeMetrics { + // snapshot : Box + // } +} + +#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum MetricsUpdateKind { + WalletMetrics, + // NodeMetrics +} + +impl MetricsUpdate { + pub fn kind(&self) -> MetricsUpdateKind { + match self { + MetricsUpdate::WalletMetrics { .. } => MetricsUpdateKind::WalletMetrics, + // MetricsUpdate::NodeMetrics { .. } => MetricsUpdateKind::NodeMetrics + } + } +} + +// impl MetricsUpdate { +// pub fn wallet_metrics(mempool_size: u64, peers: usize) -> Self { +// MetricsUpdate::WalletMetrics { mempool_size, peers } +// } + +// pub fn node_metrics(snapshot: MetricsSnapshot) -> Self { +// MetricsUpdate::NodeMetrics(Box::new(snapshot)) +// } +// } diff --git a/wallet/core/src/prelude.rs b/wallet/core/src/prelude.rs index 65297ff99..eb9bb2b1e 100644 --- a/wallet/core/src/prelude.rs +++ b/wallet/core/src/prelude.rs @@ -9,8 +9,8 @@ pub use crate::api::*; pub use crate::deterministic::{AccountId, AccountStorageKey}; pub use crate::encryption::EncryptionKind; pub use crate::events::{Events, SyncState}; +pub use crate::metrics::{MetricsUpdate, MetricsUpdateKind}; 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}; @@ -19,4 +19,5 @@ pub use crate::wallet::args::*; pub use crate::wallet::Wallet; pub use kaspa_addresses::{Address, Prefix as AddressPrefix}; pub use kaspa_bip32::{Language, Mnemonic, WordCount}; +pub use kaspa_wallet_keys::secret::Secret; pub use kaspa_wrpc_client::{KaspaRpcClient, WrpcEncoding}; diff --git a/wallet/core/src/rpc.rs b/wallet/core/src/rpc.rs index 603d190f4..999e09e30 100644 --- a/wallet/core/src/rpc.rs +++ b/wallet/core/src/rpc.rs @@ -10,6 +10,7 @@ pub type DynRpcApi = dyn RpcApi; pub type NotificationChannel = kaspa_utils::channel::Channel; pub use kaspa_rpc_core::notify::mode::NotificationMode; pub use kaspa_wrpc_client::client::{ConnectOptions, ConnectStrategy}; +pub use kaspa_wrpc_client::Resolver; pub use kaspa_wrpc_client::WrpcEncoding; /// RPC adaptor class that holds the [`RpcApi`] diff --git a/wallet/core/src/settings.rs b/wallet/core/src/settings.rs index 3370ae488..35fde4486 100644 --- a/wallet/core/src/settings.rs +++ b/wallet/core/src/settings.rs @@ -27,7 +27,7 @@ pub enum WalletSettings { #[async_trait] impl DefaultSettings for WalletSettings { async fn defaults() -> Vec<(Self, Value)> { - vec![(Self::Server, to_value("127.0.0.1").unwrap()), (Self::Wallet, to_value("kaspa").unwrap())] + vec![(Self::Server, to_value("public").unwrap()), (Self::Wallet, to_value("kaspa").unwrap())] } } diff --git a/wallet/core/src/storage/interface.rs b/wallet/core/src/storage/interface.rs index 54771add1..bc756a723 100644 --- a/wallet/core/src/storage/interface.rs +++ b/wallet/core/src/storage/interface.rs @@ -3,8 +3,6 @@ //! use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use async_trait::async_trait; use downcast::{downcast_sync, AnySync}; @@ -13,6 +11,20 @@ pub struct WalletExportOptions { pub include_transactions: bool, } +#[wasm_bindgen(typescript_custom_section)] +const TS_WALLET_DESCRIPTOR: &'static str = r#" +/** + * Wallet storage information. + * + * @category Wallet API + */ +export interface IWalletDescriptor { + title?: string; + filename: string; +} +"#; + +/// @category Wallet API #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[wasm_bindgen(inspectable)] pub struct WalletDescriptor { @@ -28,9 +40,20 @@ impl WalletDescriptor { } } +#[wasm_bindgen(typescript_custom_section)] +const TS_STORAGE_DESCRIPTOR: &'static str = r#" +/** + * Wallet storage information. + */ +export interface IStorageDescriptor { + kind: string; + data: string; +} +"#; + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] -#[serde(tag = "kind", content = "meta")] +#[serde(tag = "kind", content = "data")] pub enum StorageDescriptor { Resident, Internal(String), @@ -51,6 +74,7 @@ pub type StorageStream = Pin> + Send>>; #[async_trait] pub trait PrvKeyDataStore: Send + Sync { + async fn is_empty(&self) -> Result; async fn iter(&self) -> Result>>; async fn load_key_info(&self, id: &PrvKeyDataId) -> Result>>; async fn load_key_data(&self, wallet_secret: &Secret, id: &PrvKeyDataId) -> Result>; @@ -60,6 +84,7 @@ pub trait PrvKeyDataStore: Send + Sync { #[async_trait] pub trait AccountStore: Send + Sync { + async fn is_empty(&self) -> Result; async fn iter( &self, prv_key_data_id_filter: Option, @@ -75,6 +100,9 @@ pub trait AccountStore: Send + Sync { #[async_trait] pub trait AddressBookStore: Send + Sync { + async fn is_empty(&self) -> Result { + Err(Error::NotImplemented) + } async fn iter(&self) -> Result>> { Err(Error::NotImplemented) } diff --git a/wallet/core/src/storage/keydata/id.rs b/wallet/core/src/storage/keydata/id.rs index 1906abcf9..94ff65452 100644 --- a/wallet/core/src/storage/keydata/id.rs +++ b/wallet/core/src/storage/keydata/id.rs @@ -34,6 +34,20 @@ impl FromHex for KeyDataId { } } +impl TryFrom<&JsValue> for KeyDataId { + type Error = Error; + fn try_from(value: &JsValue) -> Result { + let string = value.as_string().ok_or(Error::InvalidKeyDataId(format!("{value:?}")))?; + Self::from_hex(&string) + } +} + +impl From for JsValue { + fn from(value: KeyDataId) -> Self { + JsValue::from(value.to_hex()) + } +} + 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()) diff --git a/wallet/core/src/storage/keydata/info.rs b/wallet/core/src/storage/keydata/info.rs index 9a4af33ec..6e251f7f6 100644 --- a/wallet/core/src/storage/keydata/info.rs +++ b/wallet/core/src/storage/keydata/info.rs @@ -3,12 +3,36 @@ //! use crate::imports::*; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; use std::fmt::{Display, Formatter}; +declare! { + IPrvKeyDataInfo, + r#" + /** + * Private key data information. + * @category Wallet API + */ + export interface IPrvKeyDataInfo { + /** Deterministic wallet id of the private key */ + id: HexString; + /** Optional name of the private key */ + name?: string; + /** + * Indicates if the key requires additional payment or a recovery secret + * to perform wallet operations that require access to it. + * For BIP39 keys this indicates that the key was created with a BIP39 passphrase. + */ + isEncrypted: boolean; + } + "#, +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct PrvKeyDataInfo { pub id: PrvKeyDataId, pub name: Option, + #[serde(rename = "isEncrypted")] pub is_encrypted: bool, } diff --git a/wallet/core/src/storage/local/cache.rs b/wallet/core/src/storage/local/cache.rs index 8558f190f..432e56c84 100644 --- a/wallet/core/src/storage/local/cache.rs +++ b/wallet/core/src/storage/local/cache.rs @@ -3,8 +3,6 @@ //! use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use crate::storage::local::wallet::WalletStorage; use crate::storage::local::*; use std::collections::HashMap; diff --git a/wallet/core/src/storage/local/interface.rs b/wallet/core/src/storage/local/interface.rs index 4425c3e46..1e998eb61 100644 --- a/wallet/core/src/storage/local/interface.rs +++ b/wallet/core/src/storage/local/interface.rs @@ -5,8 +5,6 @@ //! use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use crate::storage::interface::{ AddressBookStore, CreateArgs, OpenArgs, StorageDescriptor, StorageStream, WalletDescriptor, WalletExportOptions, }; @@ -489,6 +487,10 @@ impl Interface for LocalStore { #[async_trait] impl PrvKeyDataStore for LocalStoreInner { + async fn is_empty(&self) -> Result { + Ok(self.cache.read().unwrap().prv_key_data_info.is_empty()) + } + async fn iter(&self) -> Result>> { Ok(Box::pin(PrvKeyDataInfoStream::new(self.cache.clone()))) } @@ -527,6 +529,10 @@ impl PrvKeyDataStore for LocalStoreInner { #[async_trait] impl AccountStore for LocalStoreInner { + async fn is_empty(&self) -> Result { + Ok(self.cache.read().unwrap().accounts.is_empty()) + } + async fn iter( &self, prv_key_data_id_filter: Option, diff --git a/wallet/core/src/storage/local/mod.rs b/wallet/core/src/storage/local/mod.rs index 314021f85..07e612a51 100644 --- a/wallet/core/src/storage/local/mod.rs +++ b/wallet/core/src/storage/local/mod.rs @@ -97,6 +97,7 @@ pub unsafe fn set_default_storage_folder(folder: String) -> Result<()> { /// /// @param {String} folder - the path to the storage folder /// +/// @category Wallet API #[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 @@ -133,9 +134,10 @@ pub unsafe fn set_default_wallet_file(folder: String) -> Result<()> { /// This function should be called before using any /// other wallet SDK functions. /// -/// @param {String} - the name to the wallet file or key. +/// @param {String} folder - the name to the wallet file or key. /// -#[wasm_bindgen(js_name = setDefaultWalletFile, skip_jsdoc)] +/// @category Wallet API +#[wasm_bindgen(js_name = setDefaultWalletFile)] 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. diff --git a/wallet/core/src/storage/local/payload.rs b/wallet/core/src/storage/local/payload.rs index 0a38ea59f..2cc5c9091 100644 --- a/wallet/core/src/storage/local/payload.rs +++ b/wallet/core/src/storage/local/payload.rs @@ -3,8 +3,6 @@ //! use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use crate::storage::{AddressBookEntry, PrvKeyData, PrvKeyDataId}; use kaspa_bip32::Mnemonic; use zeroize::{Zeroize, ZeroizeOnDrop}; diff --git a/wallet/core/src/storage/local/storage.rs b/wallet/core/src/storage/local/storage.rs index 2216a9fa2..e10b73417 100644 --- a/wallet/core/src/storage/local/storage.rs +++ b/wallet/core/src/storage/local/storage.rs @@ -9,6 +9,7 @@ use workflow_core::runtime; use workflow_store::fs; /// Wallet file storage interface +/// @category Wallet SDK #[wasm_bindgen(inspectable)] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Storage { @@ -92,13 +93,15 @@ impl Storage { } pub fn ensure_dir_sync(&self) -> Result<()> { - if self.exists_sync()? { - return Ok(()); - } - - let file = self.filename(); - if let Some(dir) = file.parent() { - fs::create_dir_all_sync(dir)?; + if !runtime::is_web() && !runtime::is_chrome_extension() { + if self.exists_sync()? { + return Ok(()); + } + + let file = self.filename(); + if let Some(dir) = file.parent() { + fs::create_dir_all_sync(dir)?; + } } Ok(()) } diff --git a/wallet/core/src/storage/local/transaction/fsio.rs b/wallet/core/src/storage/local/transaction/fsio.rs index 0feed66c1..ac44a136d 100644 --- a/wallet/core/src/storage/local/transaction/fsio.rs +++ b/wallet/core/src/storage/local/transaction/fsio.rs @@ -4,8 +4,6 @@ use crate::encryption::*; use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use crate::storage::interface::{StorageStream, TransactionRangeResult}; use crate::storage::TransactionRecord; use crate::storage::{Binding, TransactionKind, TransactionRecordStore}; diff --git a/wallet/core/src/storage/local/wallet.rs b/wallet/core/src/storage/local/wallet.rs index 17596c9d0..afea36ad3 100644 --- a/wallet/core/src/storage/local/wallet.rs +++ b/wallet/core/src/storage/local/wallet.rs @@ -3,8 +3,6 @@ //! use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use crate::storage::local::Payload; use crate::storage::local::Storage; use crate::storage::Encryptable; @@ -42,7 +40,10 @@ impl WalletStorage { } pub fn payload(&self, secret: &Secret) -> Result> { - self.payload.decrypt::(secret) + self.payload.decrypt::(secret).map_err(|err| match err { + Error::Chacha20poly1305(e) => Error::WalletDecrypt(e), + _ => err, + }) } pub async fn try_load(store: &Storage) -> Result { diff --git a/wallet/core/src/storage/mod.rs b/wallet/core/src/storage/mod.rs index 65d3af90f..21b30186a 100644 --- a/wallet/core/src/storage/mod.rs +++ b/wallet/core/src/storage/mod.rs @@ -36,8 +36,6 @@ 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::WalletStorage; use kaspa_bip32::{Language, Mnemonic}; diff --git a/wallet/core/src/storage/transaction/data.rs b/wallet/core/src/storage/transaction/data.rs index 56cfe5fc1..51fff3df8 100644 --- a/wallet/core/src/storage/transaction/data.rs +++ b/wallet/core/src/storage/transaction/data.rs @@ -8,10 +8,10 @@ use kaspa_consensus_core::tx::Transaction; pub use kaspa_consensus_core::tx::TransactionId; #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", content = "transaction")] +#[serde(tag = "type", content = "data")] // 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. +// enum tags to be lower-kebab-case. #[serde(rename_all = "kebab-case")] pub enum TransactionData { Reorg { @@ -138,6 +138,24 @@ impl TransactionData { TransactionData::Change { .. } => TransactionKind::Change, } } + + pub fn has_address(&self, address: &Address) -> bool { + match self { + TransactionData::Reorg { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + TransactionData::Stasis { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + TransactionData::Incoming { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + TransactionData::External { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + TransactionData::Outgoing { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + TransactionData::Batch { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + TransactionData::TransferIncoming { utxo_entries, .. } => { + utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)) + } + TransactionData::TransferOutgoing { utxo_entries, .. } => { + utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)) + } + TransactionData::Change { utxo_entries, .. } => utxo_entries.iter().any(|utxo| utxo.address.as_ref() == Some(address)), + } + } } impl BorshSerialize for TransactionData { diff --git a/wallet/core/src/storage/transaction/kind.rs b/wallet/core/src/storage/transaction/kind.rs index 6f666f079..22944a34c 100644 --- a/wallet/core/src/storage/transaction/kind.rs +++ b/wallet/core/src/storage/transaction/kind.rs @@ -5,6 +5,27 @@ use crate::imports::*; pub use kaspa_consensus_core::tx::TransactionId; +#[wasm_bindgen(typescript_custom_section)] +const TS_TRANSACTION_KIND: &'static str = r#" +/** + * + * + * @category Wallet SDK + * + */ +export enum TransactionKind { + Reorg = "reorg", + Stasis = "stasis", + Batch = "batch", + Change = "change", + Incoming = "incoming", + Outgoing = "outgoing", + External = "external", + TransferIncoming = "transfer-incoming", + TransferOutgoing = "transfer-outgoing", +} +"#; + // Do not change the order of the variants in this enum. seal! { 0x93c6, { #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Eq, PartialEq)] @@ -83,3 +104,25 @@ impl std::fmt::Display for TransactionKind { write!(f, "{s}") } } + +impl TryFrom for TransactionKind { + type Error = Error; + fn try_from(js_value: JsValue) -> std::result::Result { + if let Some(s) = js_value.as_string() { + match s.as_str() { + "incoming" => Ok(TransactionKind::Incoming), + "outgoing" => Ok(TransactionKind::Outgoing), + "external" => Ok(TransactionKind::External), + "batch" => Ok(TransactionKind::Batch), + "reorg" => Ok(TransactionKind::Reorg), + "stasis" => Ok(TransactionKind::Stasis), + "transfer-incoming" => Ok(TransactionKind::TransferIncoming), + "transfer-outgoing" => Ok(TransactionKind::TransferOutgoing), + "change" => Ok(TransactionKind::Change), + _ => Err(Error::InvalidTransactionKind(s)), + } + } else { + Err(Error::InvalidTransactionKind(format!("{:?}", js_value))) + } + } +} diff --git a/wallet/core/src/storage/transaction/record.rs b/wallet/core/src/storage/transaction/record.rs index 2f0ef4a13..7a0457133 100644 --- a/wallet/core/src/storage/transaction/record.rs +++ b/wallet/core/src/storage/transaction/record.rs @@ -7,27 +7,334 @@ use crate::imports::*; use crate::storage::Binding; use crate::tx::PendingTransactionInner; use workflow_core::time::{unixtime_as_millis_u64, unixtime_to_locale_string}; +use workflow_wasm::utils::try_get_js_value_prop; pub use kaspa_consensus_core::tx::TransactionId; use zeroize::Zeroize; +#[wasm_bindgen(typescript_custom_section)] +const ITransactionRecord: &'static str = r#" + +/** + * + * @category Wallet SDK + */ +export interface IUtxoRecord { + address?: Address; + index: number; + amount: bigint; + scriptPublicKey: HexString; + isCoinbase: boolean; +} + +/** + * Type of transaction data record. + * @see {@link ITransactionData}, {@link ITransactionDataVariant}, {@link ITransactionRecord} + * @category Wallet SDK + */ +export enum TransactionDataType { + /** + * Transaction has been invalidated due to a BlockDAG reorganization. + * Such transaction is no longer valid and its UTXO entries are removed. + * @see {@link ITransactionDataReorg} + */ + Reorg = "reorg", + /** + * Transaction has been received and its UTXO entries are added to the + * pending or mature UTXO set. + * @see {@link ITransactionDataIncoming} + */ + Incoming = "incoming", + /** + * Transaction is in stasis and its UTXO entries are not yet added to the UTXO set. + * This event is generated for **Coinbase** transactions only. + * @see {@link ITransactionDataStasis} + */ + Stasis = "stasis", + /** + * Observed transaction is not performed by the wallet subsystem but is executed + * against the address set managed by the wallet subsystem. + * @see {@link ITransactionDataExternal} + */ + External = "external", + /** + * Transaction is outgoing and its UTXO entries are removed from the UTXO set. + * @see {@link ITransactionDataOutgoing} + */ + Outgoing = "outgoing", + /** + * Transaction is a batch transaction (compounding UTXOs to an internal change address). + * @see {@link ITransactionDataBatch} + */ + Batch = "batch", + /** + * Transaction is an incoming transfer from another {@link UtxoContext} managed by the {@link UtxoProcessor}. + * When operating under the integrated wallet, these are transfers between different wallet accounts. + * @see {@link ITransactionDataTransferIncoming} + */ + TransferIncoming = "transfer-incoming", + /** + * Transaction is an outgoing transfer to another {@link UtxoContext} managed by the {@link UtxoProcessor}. + * When operating under the integrated wallet, these are transfers between different wallet accounts. + * @see {@link ITransactionDataTransferOutgoing} + */ + TransferOutgoing = "transfer-outgoing", + /** + * Transaction is a change transaction and its UTXO entries are added to the UTXO set. + * @see {@link ITransactionDataChange} + */ + Change = "change", +} + +/** + * Contains UTXO entries and value for a transaction + * that has been invalidated due to a BlockDAG reorganization. + * @category Wallet SDK + */ +export interface ITransactionDataReorg { + utxoEntries: IUtxoRecord[]; + value: bigint; +} + +/** + * Contains UTXO entries and value for an incoming transaction. + * @category Wallet SDK + */ +export interface ITransactionDataIncoming { + utxoEntries: IUtxoRecord[]; + value: bigint; +} + +/** + * Contains UTXO entries and value for a stasis transaction. + * @category Wallet SDK + */ +export interface ITransactionDataStasis { + utxoEntries: IUtxoRecord[]; + value: bigint; +} + +/** + * Contains UTXO entries and value for an external transaction. + * An external transaction is a transaction that was not issued + * by this instance of the wallet but belongs to this address set. + * @category Wallet SDK + */ +export interface ITransactionDataExternal { + utxoEntries: IUtxoRecord[]; + value: bigint; +} + +/** + * Batch transaction data (created by the {@link Generator} as a + * result of UTXO compounding process). + * @category Wallet SDK + */ +export interface ITransactionDataBatch { + fees: bigint; + inputValue: bigint; + outputValue: bigint; + transaction: ITransaction; + paymentValue: bigint; + changeValue: bigint; + acceptedDaaScore?: bigint; + utxoEntries: IUtxoRecord[]; +} + +/** + * Outgoing transaction data. + * @category Wallet SDK + */ +export interface ITransactionDataOutgoing { + fees: bigint; + inputValue: bigint; + outputValue: bigint; + transaction: ITransaction; + paymentValue: bigint; + changeValue: bigint; + acceptedDaaScore?: bigint; + utxoEntries: IUtxoRecord[]; +} + +/** + * Incoming transfer transaction data. + * Transfer occurs when a transaction is issued between + * two {@link UtxoContext} (wallet account) instances. + * @category Wallet SDK + */ +export interface ITransactionDataTransferIncoming { + fees: bigint; + inputValue: bigint; + outputValue: bigint; + transaction: ITransaction; + paymentValue: bigint; + changeValue: bigint; + acceptedDaaScore?: bigint; + utxoEntries: IUtxoRecord[]; +} + +/** + * Outgoing transfer transaction data. + * Transfer occurs when a transaction is issued between + * two {@link UtxoContext} (wallet account) instances. + * @category Wallet SDK + */ +export interface ITransactionDataTransferOutgoing { + fees: bigint; + inputValue: bigint; + outputValue: bigint; + transaction: ITransaction; + paymentValue: bigint; + changeValue: bigint; + acceptedDaaScore?: bigint; + utxoEntries: IUtxoRecord[]; +} + +/** + * Change transaction data. + * @category Wallet SDK + */ +export interface ITransactionDataChange { + inputValue: bigint; + outputValue: bigint; + transaction: ITransaction; + paymentValue: bigint; + changeValue: bigint; + acceptedDaaScore?: bigint; + utxoEntries: IUtxoRecord[]; +} + +/** + * Transaction record data variants. + * @category Wallet SDK + */ +export type ITransactionDataVariant = + ITransactionDataReorg + | ITransactionDataIncoming + | ITransactionDataStasis + | ITransactionDataExternal + | ITransactionDataOutgoing + | ITransactionDataBatch + | ITransactionDataTransferIncoming + | ITransactionDataTransferOutgoing + | ITransactionDataChange; + +/** + * Internal transaction data contained within the transaction record. + * @see {@link ITransactionRecord} + * @category Wallet SDK + */ +export interface ITransactionData { + type : TransactionDataType; + data : ITransactionDataVariant; +} + +/** + * Transaction record generated by the Kaspa Wallet SDK. + * This data structure is delivered within {@link UtxoProcessor} and `Wallet` notification events. + * @see {@link ITransactionData}, {@link TransactionDataType}, {@link ITransactionDataVariant} + * @category Wallet SDK + */ +export interface ITransactionRecord { + /** + * Transaction id. + */ + id: string; + /** + * Transaction UNIX time in milliseconds. + */ + unixtimeMsec?: bigint; + /** + * Transaction value in SOMPI. + */ + value: bigint; + /** + * Transaction binding (id of UtxoContext or Wallet Account). + */ + binding: HexString; + /** + * Block DAA score. + */ + blockDaaScore: bigint; + /** + * Network id on which this transaction has occurred. + */ + network: NetworkId; + /** + * Transaction data. + */ + data: ITransactionData; + /** + * Optional transaction note as a human-readable string. + */ + note?: string; + /** + * Optional transaction metadata. + * + * If present, this must contain a JSON-serialized string. + * A client application updating the metadata must deserialize + * the string into JSON, add a key with it's own identifier + * and store its own metadata into the value of this key. + */ + metadata?: string; + + /** + * Transaction data type. + */ + type: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = Object, typescript_type = "ITransactionRecord")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type ITransactionRecord; +} + +#[wasm_bindgen(inspectable)] +#[derive(Debug, Clone, Serialize)] +pub struct TransactionRecordNotification { + #[serde(rename = "type")] + #[wasm_bindgen(js_name = "type", getter_with_clone)] + pub type_: String, + #[wasm_bindgen(getter_with_clone)] + pub data: TransactionRecord, +} + +impl TransactionRecordNotification { + pub fn new(type_: String, data: TransactionRecord) -> Self { + Self { type_, data } + } +} + +/// @category Wallet SDK +#[wasm_bindgen(inspectable)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransactionRecord { pub id: TransactionId, + /// Unix time in milliseconds #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "unixtimeMsec")] + #[wasm_bindgen(js_name = unixtimeMsec)] pub unixtime_msec: Option, pub value: u64, + #[wasm_bindgen(skip)] pub binding: Binding, #[serde(rename = "blockDaaScore")] + #[wasm_bindgen(js_name = blockDaaScore)] pub block_daa_score: u64, #[serde(rename = "network")] + #[wasm_bindgen(js_name = network)] pub network_id: NetworkId, #[serde(rename = "data")] + #[wasm_bindgen(skip)] pub transaction_data: TransactionData, #[serde(skip_serializing_if = "Option::is_none")] + #[wasm_bindgen(getter_with_clone)] pub note: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[wasm_bindgen(getter_with_clone)] pub metadata: Option, } @@ -175,7 +482,7 @@ impl TransactionRecord { utxos: &[UtxoEntryReference], ) -> Self { let binding = Binding::from(utxo_context.binding()); - let block_daa_score = utxos[0].utxo.entry.block_daa_score; + let block_daa_score = utxos[0].utxo.block_daa_score; let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); let aggregate_input_value = utxo_entries.iter().map(|utxo| utxo.amount).sum::(); @@ -206,7 +513,7 @@ impl TransactionRecord { /// 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 block_daa_score = utxos[0].utxo.block_daa_score; let utxo_entries = utxos.iter().map(UtxoRecord::from).collect::>(); let aggregate_input_value = utxo_entries.iter().map(|utxo| utxo.amount).sum::(); @@ -235,7 +542,7 @@ impl TransactionRecord { 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 utxo_entries = outgoing_tx.utxo_entries().values().map(UtxoRecord::from).collect::>(); let unixtime = unixtime_as_millis_u64(); @@ -281,7 +588,7 @@ impl TransactionRecord { 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 utxo_entries = outgoing_tx.utxo_entries().values().map(UtxoRecord::from).collect::>(); let unixtime = unixtime_as_millis_u64(); @@ -475,6 +782,35 @@ impl TransactionRecord { } } +#[wasm_bindgen] +impl TransactionRecord { + #[wasm_bindgen(getter, js_name = "binding")] + pub fn binding_as_js_value(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.binding).unwrap() + } + + #[wasm_bindgen(getter, js_name = "data")] + pub fn data_as_js_value(&self) -> JsValue { + try_get_js_value_prop(&serde_wasm_bindgen::to_value(&self.transaction_data).unwrap(), "data").unwrap() + } + + #[wasm_bindgen(getter, js_name = "type")] + pub fn data_type(&self) -> String { + self.transaction_data.kind().to_string() + } + + /// Check if the transaction record has the given address within the associated UTXO set. + #[wasm_bindgen(js_name = hasAddress)] + pub fn has_address(&self, address: &Address) -> bool { + self.transaction_data.has_address(address) + } + + /// Serialize the transaction record to a JavaScript object. + pub fn serialize(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self).unwrap() + } +} + impl Zeroize for TransactionRecord { fn zeroize(&mut self) { // TODO - this trait is added due to the @@ -518,3 +854,15 @@ impl BorshDeserialize for TransactionRecord { Ok(Self { id, unixtime_msec: unixtime, value, binding, block_daa_score, network_id, transaction_data, note, metadata }) } } + +// impl From for JsValue { +// fn from(record: TransactionRecord) -> Self { +// serde_wasm_bindgen::to_value(&record).unwrap() +// } +// } + +impl From for ITransactionRecord { + fn from(record: TransactionRecord) -> Self { + JsValue::from(record).unchecked_into() + } +} diff --git a/wallet/core/src/storage/transaction/utxo.rs b/wallet/core/src/storage/transaction/utxo.rs index aa445bc03..5508b8960 100644 --- a/wallet/core/src/storage/transaction/utxo.rs +++ b/wallet/core/src/storage/transaction/utxo.rs @@ -27,9 +27,9 @@ impl From<&UtxoEntryReference> for UtxoRecord { 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, + amount: utxo.amount, + script_public_key: utxo.script_public_key.clone(), + is_coinbase: utxo.is_coinbase, } } } diff --git a/wallet/core/src/tx/fees.rs b/wallet/core/src/tx/fees.rs index 7ad19cf10..0529e7c6c 100644 --- a/wallet/core/src/tx/fees.rs +++ b/wallet/core/src/tx/fees.rs @@ -5,8 +5,6 @@ use crate::result::Result; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use wasm_bindgen::prelude::*; -use workflow_wasm::prelude::*; /// Transaction fees. Fees are comprised of 2 values: /// @@ -96,16 +94,3 @@ impl TryFrom for Fees { Self::try_from(fee.as_str()) } } - -impl TryFrom for Fees { - type Error = crate::error::Error; - fn try_from(fee: JsValue) -> Result { - if fee.is_undefined() || fee.is_null() { - Ok(Fees::None) - } else if let Ok(fee) = fee.try_as_u64() { - Ok(Fees::SenderPays(fee)) - } else { - Err(crate::error::Error::custom("Invalid fee")) - } - } -} diff --git a/wallet/core/src/tx/generator/generator.rs b/wallet/core/src/tx/generator/generator.rs index 6402cdf14..533f2e046 100644 --- a/wallet/core/src/tx/generator/generator.rs +++ b/wallet/core/src/tx/generator/generator.rs @@ -63,11 +63,10 @@ use crate::tx::{ PendingTransactionStream, }; use crate::utxo::{NetworkParams, UtxoContext, UtxoEntryReference}; +use kaspa_consensus_client::UtxoEntry; use kaspa_consensus_core::constants::UNACCEPTED_DAA_SCORE; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; -use kaspa_consensus_core::tx as cctx; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; -use kaspa_consensus_wasm::UtxoEntry; use kaspa_txscript::pay_to_address_script; use std::collections::VecDeque; @@ -225,6 +224,7 @@ impl Data { } /// Helper struct for passing around transaction value +#[derive(Debug)] struct FinalTransaction { /// Total output value required for the final transaction value_no_fees: u64, @@ -298,6 +298,31 @@ struct Inner { context: Mutex, } +impl std::fmt::Debug for Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Inner") + .field("network_id", &self.network_id) + .field("network_params", &self.network_params) + // .field("source_utxo_context", &self.source_utxo_context) + // .field("destination_utxo_context", &self.destination_utxo_context) + // .field("multiplexer", &self.multiplexer) + .field("sig_op_count", &self.sig_op_count) + .field("minimum_signatures", &self.minimum_signatures) + .field("change_address", &self.change_address) + .field("standard_change_output_compute_mass", &self.standard_change_output_compute_mass) + .field("signature_mass_per_input", &self.signature_mass_per_input) + // .field("final_transaction", &self.final_transaction) + .field("final_transaction_priority_fee", &self.final_transaction_priority_fee) + .field("final_transaction_outputs", &self.final_transaction_outputs) + .field("final_transaction_outputs_harmonic", &self.final_transaction_outputs_harmonic) + .field("final_transaction_outputs_compute_mass", &self.final_transaction_outputs_compute_mass) + .field("final_transaction_payload", &self.final_transaction_payload) + .field("final_transaction_payload_mass", &self.final_transaction_payload_mass) + // .field("context", &self.context) + .finish() + } +} + /// /// Transaction generator /// @@ -337,6 +362,10 @@ impl Generator { } PaymentDestination::PaymentOutputs(outputs) => { // sanity checks + if final_transaction_priority_fee.is_none() { + return Err(Error::GeneratorNoFeesForFinalTransaction); + } + for output in outputs.iter() { if NetworkType::try_from(output.address.prefix)? != network_type { return Err(Error::GeneratorPaymentOutputNetworkTypeMismatch); @@ -419,6 +448,7 @@ impl Generator { final_transaction_payload_mass, destination_utxo_context, }; + Ok(Self { inner: Arc::new(inner) }) } @@ -1037,9 +1067,15 @@ impl Generator { script_public_key: ScriptPublicKey, address: &Address, ) -> UtxoEntryReference { - let entry = cctx::UtxoEntry { amount, script_public_key, block_daa_score: UNACCEPTED_DAA_SCORE, is_coinbase: false }; let outpoint = TransactionOutpoint::new(txid, 0); - let utxo = UtxoEntry { address: Some(address.clone()), outpoint: outpoint.into(), entry }; + let utxo = UtxoEntry { + address: Some(address.clone()), + outpoint: outpoint.into(), + amount, + script_public_key, + block_daa_score: UNACCEPTED_DAA_SCORE, + is_coinbase: false, // entry + }; UtxoEntryReference { utxo: Arc::new(utxo) } } diff --git a/wallet/core/src/tx/generator/pending.rs b/wallet/core/src/tx/generator/pending.rs index f5ce8eba0..cd757e54b 100644 --- a/wallet/core/src/tx/generator/pending.rs +++ b/wallet/core/src/tx/generator/pending.rs @@ -7,7 +7,7 @@ use crate::imports::*; use crate::result::Result; use crate::rpc::DynRpcApi; use crate::tx::{DataKind, Generator}; -use crate::utxo::{UtxoContext, UtxoEntryReference}; +use crate::utxo::{UtxoContext, UtxoEntryId, UtxoEntryReference}; use kaspa_consensus_core::sign::sign_with_multiple_v2; use kaspa_consensus_core::tx::{SignableTransaction, Transaction, TransactionId}; use kaspa_rpc_core::{RpcTransaction, RpcTransactionId}; @@ -16,7 +16,7 @@ pub(crate) struct PendingTransactionInner { /// Generator that produced the transaction pub(crate) generator: Generator, /// UtxoEntryReferences of the pending transaction - pub(crate) utxo_entries: AHashSet, + pub(crate) utxo_entries: AHashMap, /// Transaction Id (cached in pending to avoid mutex lock) pub(crate) id: TransactionId, /// Signable transaction (actual transaction that will be signed and sent) @@ -82,9 +82,9 @@ impl PendingTransaction { kind: DataKind, ) -> Result { let id = transaction.id(); - let entries = utxo_entries.iter().map(|e| e.utxo.entry.clone()).collect::>(); + let entries = utxo_entries.iter().map(|e| e.utxo.as_ref().into()).collect::>(); let signable_tx = Mutex::new(SignableTransaction::with_entries(transaction, entries)); - let utxo_entries = utxo_entries.into_iter().collect::>(); + let utxo_entries = utxo_entries.into_iter().map(|entry| (entry.id(), entry)).collect::>(); Ok(Self { inner: Arc::new(PendingTransactionInner { generator: generator.clone(), @@ -126,7 +126,7 @@ impl PendingTransaction { } /// Get UTXO entries [`AHashSet`] of the pending transaction - pub fn utxo_entries(&self) -> &AHashSet { + pub fn utxo_entries(&self) -> &AHashMap { &self.inner.utxo_entries } @@ -166,6 +166,10 @@ impl PendingTransaction { self.inner.signable_tx.lock().unwrap().tx.clone() } + pub fn signable_transaction(&self) -> SignableTransaction { + self.inner.signable_tx.lock().unwrap().clone() + } + pub fn rpc_transaction(&self) -> RpcTransaction { self.inner.signable_tx.lock().unwrap().tx.as_ref().into() } @@ -219,7 +223,7 @@ impl PendingTransaction { Ok(()) } - pub fn try_sign_with_keys(&self, privkeys: Vec<[u8; 32]>) -> Result<()> { + pub fn try_sign_with_keys(&self, privkeys: &[[u8; 32]]) -> Result<()> { let mutable_tx = self.inner.signable_tx.lock()?.clone(); let signed_tx = sign_with_multiple_v2(mutable_tx, privkeys).fully_signed()?; *self.inner.signable_tx.lock().unwrap() = signed_tx; diff --git a/wallet/core/src/tx/generator/settings.rs b/wallet/core/src/tx/generator/settings.rs index d37442b69..0055d8fb4 100644 --- a/wallet/core/src/tx/generator/settings.rs +++ b/wallet/core/src/tx/generator/settings.rs @@ -36,6 +36,24 @@ pub struct GeneratorSettings { pub destination_utxo_context: Option, } +// impl std::fmt::Debug for GeneratorSettings { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.debug_struct("GeneratorSettings") +// .field("network_id", &self.network_id) +// // .field("multiplexer", &self.multiplexer) +// .field("utxo_iterator", &"Box + Send + Sync + 'static>") +// // .field("source_utxo_context", &self.source_utxo_context) +// .field("sig_op_count", &self.sig_op_count) +// .field("minimum_signatures", &self.minimum_signatures) +// .field("change_address", &self.change_address) +// .field("final_transaction_priority_fee", &self.final_transaction_priority_fee) +// .field("final_transaction_destination", &self.final_transaction_destination) +// .field("final_transaction_payload", &self.final_transaction_payload) +// // .field("destination_utxo_context", &self.destination_utxo_context) +// .finish() +// } +// } + impl GeneratorSettings { pub fn try_new_with_account( account: Arc, diff --git a/wallet/core/src/tx/generator/signer.rs b/wallet/core/src/tx/generator/signer.rs index edb611b34..e5d745bbd 100644 --- a/wallet/core/src/tx/generator/signer.rs +++ b/wallet/core/src/tx/generator/signer.rs @@ -3,8 +3,6 @@ //! use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use kaspa_bip32::PrivateKey; use kaspa_consensus_core::{sign::sign_with_multiple_v2, tx::SignableTransaction}; @@ -50,9 +48,11 @@ impl SignerT for Signer { self.ingest(addresses)?; let keys = self.inner.keys.lock().unwrap(); - let keys_for_signing = addresses.iter().map(|address| *keys.get(address).unwrap()).collect::>(); + let mut keys_for_signing = addresses.iter().map(|address| *keys.get(address).unwrap()).collect::>(); // TODO - refactor for multisig - Ok(sign_with_multiple_v2(mutable_tx, keys_for_signing).fully_signed()?) + let signable_tx = sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?; + keys_for_signing.zeroize(); + Ok(signable_tx) } } @@ -75,8 +75,10 @@ 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::>(); + let mut keys_for_signing = addresses.iter().map(|address| *self.inner.keys.get(address).unwrap()).collect::>(); // TODO - refactor for multisig - Ok(sign_with_multiple_v2(mutable_tx, keys_for_signing).fully_signed()?) + let signable_tx = sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?; + keys_for_signing.zeroize(); + Ok(signable_tx) } } diff --git a/wallet/core/src/tx/generator/test.rs b/wallet/core/src/tx/generator/test.rs index c26c38364..1368c51f2 100644 --- a/wallet/core/src/tx/generator/test.rs +++ b/wallet/core/src/tx/generator/test.rs @@ -161,7 +161,7 @@ fn validate(pt: &PendingTransaction) { let network_params = pt.generator().network_params(); let tx = pt.transaction(); - let aggregate_input_value = pt.utxo_entries().iter().map(|o| o.amount()).sum::(); + let aggregate_input_value = pt.utxo_entries().values().map(|o| o.amount()).sum::(); let aggregate_output_value = tx.outputs.iter().map(|o| o.value).sum::(); assert_ne!( aggregate_input_value, aggregate_output_value, @@ -172,7 +172,7 @@ fn validate(pt: &PendingTransaction) { let additional_mass = if pt.is_final() { 0 } else { network_params.additional_compound_transaction_mass }; let compute_mass = calc.calc_mass_for_signed_transaction(&tx, 1); - let utxo_entries = pt.utxo_entries().iter().cloned().collect::>(); + let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); let storage_mass = calc.calc_storage_mass_for_transaction(false, &utxo_entries, &tx.outputs).unwrap_or_default(); let calculated_mass = calc.combine_mass(compute_mass, storage_mass) + additional_mass; @@ -187,7 +187,7 @@ where let network_params = pt.generator().network_params(); let tx = pt.transaction(); - let aggregate_input_value = pt.utxo_entries().iter().map(|o| o.amount()).sum::(); + let aggregate_input_value = pt.utxo_entries().values().map(|o| o.amount()).sum::(); let aggregate_output_value = tx.outputs.iter().map(|o| o.value).sum::(); assert_ne!(aggregate_input_value, aggregate_output_value, "aggregate input and output values can not be the same due to fees"); assert_eq!(pt.is_final(), expected.is_final, "expected final transaction"); @@ -203,7 +203,7 @@ where let compute_mass = calc.calc_mass_for_signed_transaction(&tx, 1); - let utxo_entries = pt.utxo_entries().iter().cloned().collect::>(); + let utxo_entries = pt.utxo_entries().values().cloned().collect::>(); let storage_mass = calc.calc_storage_mass_for_transaction(false, &utxo_entries, &tx.outputs).unwrap_or_default(); if DISPLAY_LOGS && storage_mass != 0 { println!( diff --git a/wallet/core/src/tx/mass.rs b/wallet/core/src/tx/mass.rs index e3f6a0a9f..b5583ddf3 100644 --- a/wallet/core/src/tx/mass.rs +++ b/wallet/core/src/tx/mass.rs @@ -3,9 +3,9 @@ //! use crate::utxo::NetworkParams; +use kaspa_consensus_client::UtxoEntryReference; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutput, SCRIPT_VECTOR_SIZE}; use kaspa_consensus_core::{config::params::Params, constants::*, subnets::SUBNETWORK_ID_SIZE}; -use kaspa_consensus_wasm::UtxoEntryReference; use kaspa_hashes::HASH_SIZE; #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/wallet/core/src/tx/mod.rs b/wallet/core/src/tx/mod.rs index baf3b29ac..3940ab374 100644 --- a/wallet/core/src/tx/mod.rs +++ b/wallet/core/src/tx/mod.rs @@ -8,8 +8,8 @@ pub mod generator; pub mod mass; pub mod payment; -pub use consensus::*; -pub use fees::*; -pub use generator::*; -pub use mass::*; -pub use payment::*; +pub use self::consensus::*; +pub use self::fees::*; +pub use self::generator::*; +pub use self::mass::*; +pub use self::payment::*; diff --git a/wallet/core/src/tx/payment.rs b/wallet/core/src/tx/payment.rs index bd96e9345..0cce1f30f 100644 --- a/wallet/core/src/tx/payment.rs +++ b/wallet/core/src/tx/payment.rs @@ -3,9 +3,39 @@ //! use crate::imports::*; -use kaspa_consensus_wasm::{TransactionOutput, TransactionOutputInner}; +use kaspa_consensus_client::{TransactionOutput, TransactionOutputInner}; use kaspa_txscript::pay_to_address_script; +#[wasm_bindgen(typescript_custom_section)] +const TS_PAYMENT_OUTPUTS: &'static str = r#" +/** + * + * Defines a single payment output. + * + * @see {@link IGeneratorSettingsObject}, {@link Generator} + * @category Wallet SDK + */ +export interface IPaymentOutput { + /** + * Destination address. The address prefix must match the network + * you are transacting on (e.g. `kaspa:` for mainnet, `kaspatest:` for testnet, etc). + */ + address: Address | string; + /** + * Output amount in SOMPI. + */ + amount: bigint; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IPaymentOutput")] + pub type IPaymentOutput; + #[wasm_bindgen(typescript_type = "IPaymentOutput[]")] + pub type IPaymentOutputArray; +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub enum PaymentDestination { Change, @@ -21,7 +51,8 @@ impl PaymentDestination { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +/// @category Wallet SDK +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)] #[wasm_bindgen(inspectable)] pub struct PaymentOutput { #[wasm_bindgen(getter_with_clone)] @@ -29,25 +60,27 @@ pub struct PaymentOutput { pub amount: u64, } -impl TryFrom for PaymentOutput { +impl TryCastFromJs for PaymentOutput { type Error = Error; - fn try_from(js_value: JsValue) -> Result { - if let Ok(array) = js_value.clone().dyn_into::() { - let length = array.length(); - if length != 2 { - Err(Error::Custom("Invalid payment output".to_string())) - } else { - let address = Address::try_from(array.get(0))?; - let amount = array.get(1).try_as_u64()?; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(array) = value.as_ref().dyn_ref::() { + let length = array.length(); + if length != 2 { + Err(Error::Custom("Invalid payment output".to_string())) + } else { + let address = Address::try_owned_from(array.get(0))?; + let amount = array.get(1).try_as_u64()?; + Ok(Self { address, amount }) + } + } else if let Some(object) = Object::try_from(value.as_ref()) { + let address = object.get_cast::
("address")?.into_owned(); + let amount = object.get_u64("amount")?; Ok(Self { address, amount }) + } else { + Err(Error::Custom("Invalid payment output".to_string())) } - } else if let Some(object) = Object::try_from(&js_value) { - let address = object.get::
("address")?; - let amount = object.get_u64("amount")?; - Ok(Self { address, amount }) - } else { - Err(Error::Custom("Invalid payment output".to_string())) - } + }) } } @@ -71,7 +104,8 @@ impl From for PaymentDestination { } } -#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +/// @category Wallet SDK +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)] #[wasm_bindgen] pub struct PaymentOutputs { #[wasm_bindgen(skip)] @@ -97,32 +131,35 @@ impl From for PaymentDestination { #[wasm_bindgen] impl PaymentOutputs { #[wasm_bindgen(constructor)] - pub fn constructor(output_array: JsValue) -> crate::result::Result { + pub fn constructor(output_array: IPaymentOutputArray) -> 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 { - outputs.push((x?).try_into()?); + // outputs.push((x?).try_into_cast()?); + outputs.push(PaymentOutput::try_owned_from(x?)?); } Ok(Self { outputs }) } } -impl TryFrom for PaymentOutputs { +impl TryCastFromJs for PaymentOutputs { type Error = Error; - fn try_from(js_value: JsValue) -> Result { - let outputs = if let Some(output_array) = js_value.dyn_ref::() { - let vec = output_array.to_vec(); - vec.into_iter().map(PaymentOutput::try_from).collect::, _>>()? - } else if let Some(object) = js_value.dyn_ref::() { - Object::entries(object).iter().map(PaymentOutput::try_from).collect::, _>>()? - } else if let Some(map) = js_value.dyn_ref::() { - map.entries().into_iter().flat_map(|v| v.map(PaymentOutput::try_from)).collect::, _>>()? - } else { - return Err(Error::Custom("payment outputs must be an array or an object".to_string())); - }; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + let outputs = if let Some(output_array) = value.as_ref().dyn_ref::() { + let vec = output_array.to_vec(); + vec.into_iter().map(PaymentOutput::try_owned_from).collect::, _>>()? + } else if let Some(object) = value.as_ref().dyn_ref::() { + Object::entries(object).iter().map(PaymentOutput::try_owned_from).collect::, _>>()? + } else if let Some(map) = value.as_ref().dyn_ref::() { + map.entries().into_iter().flat_map(|v| v.map(PaymentOutput::try_owned_from)).collect::, _>>()? + } else { + return Err(Error::Custom("payment outputs must be an array or an object".to_string())); + }; - Ok(Self { outputs }) + Ok(Self { outputs }) + }) } } diff --git a/wallet/core/src/utxo/balance.rs b/wallet/core/src/utxo/balance.rs index dfc67b6ad..ce189e124 100644 --- a/wallet/core/src/utxo/balance.rs +++ b/wallet/core/src/utxo/balance.rs @@ -9,7 +9,7 @@ pub enum DeltaStyle { Pending, } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub enum Delta { #[default] NoChange = 0, @@ -43,7 +43,55 @@ impl From for Delta { } } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[wasm_bindgen(typescript_custom_section)] +const TS_BALANCE: &'static str = r#" +/** + * {@link UtxoContext} (wallet account) balance. + * @category Wallet SDK + */ +export interface IBalance { + /** + * Total amount of Kaspa (in SOMPI) available for + * spending. + */ + mature: bigint; + /** + * Total amount of Kaspa (in SOMPI) that has been + * received and is pending confirmation. + */ + pending: bigint; + /** + * Total amount of Kaspa (in SOMPI) currently + * being sent as a part of the outgoing transaction + * but has not yet been accepted by the network. + */ + outgoing: bigint; + /** + * Number of UTXOs available for spending. + */ + matureUtxoCount: number; + /** + * Number of UTXOs that have been received and + * are pending confirmation. + */ + pendingUtxoCount: number; + /** + * Number of UTXOs currently in stasis (coinbase + * transactions received as a result of mining). + * Unlike regular user transactions, coinbase + * transactions go through `stasis->pending->mature` + * stages. Client applications should ignore `stasis` + * stages and should process transactions only when + * they have reached the `pending` stage. However, + * `stasis` information can be used for informative + * purposes to indicate that coinbase transactions + * have arrived. + */ + stasisUtxoCount: number; +} +"#; + +#[derive(Default, Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct Balance { pub mature: u64, @@ -92,6 +140,10 @@ impl Balance { self.pending_delta = Delta::NoChange; } } + + pub fn to_balance_strings(&self, network_type: &NetworkType, padding: Option) -> BalanceStrings { + (Some(self), network_type, padding).into() + } } #[derive(Default, Debug)] @@ -133,8 +185,8 @@ pub struct BalanceStrings { pub pending: Option, } -impl From<(&Option, &NetworkType, Option)> for BalanceStrings { - fn from((balance, network_type, padding): (&Option, &NetworkType, Option)) -> Self { +impl From<(Option<&Balance>, &NetworkType, Option)> for BalanceStrings { + fn from((balance, network_type, padding): (Option<&Balance>, &NetworkType, Option)) -> Self { let suffix = utils::kaspa_suffix(network_type); if let Some(balance) = balance { let mut mature = utils::sompi_to_kaspa_string(balance.mature); diff --git a/wallet/core/src/utxo/context.rs b/wallet/core/src/utxo/context.rs index 4dd3c6349..51ef0e5ea 100644 --- a/wallet/core/src/utxo/context.rs +++ b/wallet/core/src/utxo/context.rs @@ -17,9 +17,9 @@ use crate::utxo::{ use kaspa_hashes::Hash; use sorted_insert::SortedInsertBinaryByKey; -static PROCESSOR_ID_SEQUENCER: AtomicU64 = AtomicU64::new(0); -fn next_processor_id() -> Hash { - let id = PROCESSOR_ID_SEQUENCER.fetch_add(1, Ordering::SeqCst); +static UTXO_CONTEXT_ID_SEQUENCER: AtomicU64 = AtomicU64::new(0); +fn next_utxo_context_id() -> Hash { + let id = UTXO_CONTEXT_ID_SEQUENCER.fetch_add(1, Ordering::SeqCst); Hash::from_slice(sha256_hash(id.to_le_bytes().as_slice()).as_ref()) } @@ -28,7 +28,7 @@ pub struct UtxoContextId(pub(crate) Hash); impl Default for UtxoContextId { fn default() -> Self { - UtxoContextId(next_processor_id()) + UtxoContextId(next_utxo_context_id()) } } @@ -250,7 +250,7 @@ impl UtxoContext { let mut context = self.context(); let pending_utxo_entries = pending_tx.utxo_entries(); - context.mature.retain(|entry| !pending_utxo_entries.contains(entry)); + context.mature.retain(|entry| !pending_utxo_entries.contains_key(&entry.id())); let outgoing_transaction = OutgoingTransaction::new(current_daa_score, self.clone(), pending_tx.clone()); self.processor().register_outgoing_transaction(outgoing_transaction.clone()); @@ -282,7 +282,7 @@ impl UtxoContext { let mut context = self.context(); let outgoing_transaction = context.outgoing.remove(&pending_tx.id()).expect("outgoing transaction"); - outgoing_transaction.utxo_entries().iter().for_each(|entry| { + outgoing_transaction.utxo_entries().iter().for_each(|(_, entry)| { context.mature.push(entry.clone()); }); @@ -319,7 +319,7 @@ impl UtxoContext { } Ok(()) } else { - log_warning!("ignoring duplicate utxo entry"); + log_warn!("ignoring duplicate utxo entry"); Ok(()) } } @@ -448,7 +448,7 @@ impl UtxoContext { } } } else { - log_warning!("ignoring duplicate utxo entry"); + log_warn!("ignoring duplicate utxo entry"); } } @@ -475,8 +475,8 @@ impl UtxoContext { pub async fn calculate_balance(&self) -> Balance { let context = self.context(); - 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(); + let mature: u64 = context.mature.iter().map(|e| e.as_ref().amount).sum(); + let pending: u64 = context.pending.values().map(|e| e.as_ref().amount).sum(); // this will aggregate only transactions containing // the final payments (not compound transactions) @@ -570,9 +570,9 @@ impl UtxoContext { let outgoing_transactions = self.processor().outgoing(); let mut accepted_outgoing_transactions = HashSet::::new(); - utxos.retain(|id| { + utxos.retain(|utxo| { for outgoing_transaction in outgoing_transactions.iter() { - if outgoing_transaction.utxo_entries().contains(id) { + if outgoing_transaction.utxo_entries().contains_key(&utxo.id()) { accepted_outgoing_transactions.insert((*outgoing_transaction).clone()); return false; } @@ -676,7 +676,7 @@ impl UtxoContext { local.remove(address); }); } else { - log_warning!("utxo processor: unregister for an empty address set") + log_warn!("utxo processor: unregister for an empty address set") } Ok(()) @@ -686,11 +686,10 @@ impl UtxoContext { self.register_addresses(&addresses).await?; let resp = self.processor().rpc_api().get_utxos_by_addresses(addresses).await?; let refs: Vec = resp.into_iter().map(UtxoEntryReference::from).collect(); - let current_daa_score = current_daa_score.unwrap_or_else(|| { - self.processor() - .current_daa_score() - .expect("daa score or initialized UtxoProcessor are when invoking scan_and_register_addresses()") - }); + let current_daa_score = current_daa_score.or_else(|| { + self.processor() + .current_daa_score() + }).ok_or(Error::MissingDaaScore("Expecting DAA score or initialized UtxoProcessor when invoking scan_and_register_addresses() - You might be accessing UtxoProcessor APIs before it is initialized (see `utxo-proc-start` event)"))?; 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 09c40d7fc..cf1411193 100644 --- a/wallet/core/src/utxo/iterator.rs +++ b/wallet/core/src/utxo/iterator.rs @@ -4,6 +4,7 @@ use crate::utxo::{UtxoContext, UtxoEntryReference}; +#[derive(Debug)] pub struct UtxoIterator { entries: Vec, cursor: usize, diff --git a/wallet/core/src/utxo/mod.rs b/wallet/core/src/utxo/mod.rs index d30737e6c..cc1feab6e 100644 --- a/wallet/core/src/utxo/mod.rs +++ b/wallet/core/src/utxo/mod.rs @@ -19,7 +19,7 @@ pub use balance::Balance; pub use binding::UtxoContextBinding; pub use context::{UtxoContext, UtxoContextId}; pub use iterator::UtxoIterator; -pub use kaspa_consensus_wasm::UtxoEntryId; +pub use kaspa_consensus_client::UtxoEntryId; pub use outgoing::OutgoingTransaction; pub use pending::PendingUtxoEntryReference; pub use processor::UtxoProcessor; diff --git a/wallet/core/src/utxo/outgoing.rs b/wallet/core/src/utxo/outgoing.rs index 220409a18..7a8578f4a 100644 --- a/wallet/core/src/utxo/outgoing.rs +++ b/wallet/core/src/utxo/outgoing.rs @@ -6,7 +6,7 @@ use crate::imports::*; use crate::tx::PendingTransaction; -use crate::utxo::{UtxoContext, UtxoEntryReference}; +use crate::utxo::{UtxoContext, UtxoEntryId, UtxoEntryReference}; struct Inner { pub id: TransactionId, @@ -81,7 +81,7 @@ impl OutgoingTransaction { self.inner.pending_transaction.is_batch() } - pub fn utxo_entries(&self) -> &AHashSet { + pub fn utxo_entries(&self) -> &AHashMap { self.inner.pending_transaction.utxo_entries() } diff --git a/wallet/core/src/utxo/processor.rs b/wallet/core/src/utxo/processor.rs index 9d2ca9ce7..e788272f2 100644 --- a/wallet/core/src/utxo/processor.rs +++ b/wallet/core/src/utxo/processor.rs @@ -6,6 +6,7 @@ //! use crate::imports::*; +// use futures::pin_mut; use kaspa_notify::{ listener::ListenerId, scope::{Scope, UtxosChangedScope, VirtualDaaScoreChangedScope}, @@ -28,11 +29,12 @@ 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 kaspa_rpc_core::{ notify::connection::{ChannelConnection, ChannelType}, Notification, }; +// use workflow_core::task; +// use kaspa_metrics_core::{Metrics,Metric}; pub struct Inner { /// Coinbase UTXOs in stasis @@ -56,7 +58,10 @@ pub struct Inner { sync_proc: SyncMonitor, multiplexer: Multiplexer>, wallet_bus: Option>, - notification_lock: AsyncMutex<()>, + notification_guard: AsyncMutex<()>, + connect_disconnect_guard: AsyncMutex<()>, + metrics: Arc, + metrics_kinds: Mutex>, } impl Inner { @@ -82,7 +87,10 @@ impl Inner { sync_proc: SyncMonitor::new(rpc.clone(), &multiplexer), multiplexer, wallet_bus, - notification_lock: AsyncMutex::new(()), + notification_guard: Default::default(), + connect_disconnect_guard: Default::default(), + metrics: Arc::new(Metrics::default()), + metrics_kinds: Mutex::new(vec![]), } } } @@ -107,10 +115,18 @@ impl UtxoProcessor { self.inner.rpc.lock().unwrap().as_ref().expect("UtxoProcessor RPC not initialized").rpc_api().clone() } + pub fn try_rpc_api(&self) -> Option> { + self.inner.rpc.lock().unwrap().as_ref().map(|rpc| rpc.rpc_api()).cloned() + } + pub fn rpc_ctl(&self) -> RpcCtl { self.inner.rpc.lock().unwrap().as_ref().expect("UtxoProcessor RPC not initialized").rpc_ctl().clone() } + pub fn try_rpc_ctl(&self) -> Option { + self.inner.rpc.lock().unwrap().as_ref().map(|rpc| rpc.rpc_ctl()).cloned() + } + pub fn rpc_url(&self) -> Option { self.rpc_ctl().descriptor() } @@ -121,10 +137,16 @@ impl UtxoProcessor { pub async fn bind_rpc(&self, rpc: Option) -> Result<()> { self.inner.rpc.lock().unwrap().clone_from(&rpc); + let rpc_api = rpc.as_ref().map(|rpc| rpc.rpc_api().clone()); + self.metrics().bind_rpc(rpc_api); self.sync_proc().bind_rpc(rpc).await?; Ok(()) } + pub fn metrics(&self) -> &Arc { + &self.inner.metrics + } + pub fn wallet_bus(&self) -> &Option> { &self.inner.wallet_bus } @@ -138,7 +160,7 @@ impl UtxoProcessor { } pub async fn notification_lock(&self) -> AsyncMutexGuard<()> { - self.inner.notification_lock.lock().await + self.inner.notification_guard.lock().await } pub fn sync_proc(&self) -> &SyncMonitor { @@ -149,8 +171,8 @@ impl UtxoProcessor { self.inner.listener_id.lock().unwrap().ok_or(Error::ListenerId) } - pub fn set_network_id(&self, network_id: NetworkId) { - self.inner.network_id.lock().unwrap().replace(network_id); + pub fn set_network_id(&self, network_id: &NetworkId) { + self.inner.network_id.lock().unwrap().replace(*network_id); } pub fn network_id(&self) -> Result { @@ -197,7 +219,7 @@ impl UtxoProcessor { let utxos_changed_scope = UtxosChangedScope::new(addresses); self.rpc_api().start_notify(self.listener_id()?, utxos_changed_scope.into()).await?; } else { - log_error!("registering empty address list!"); + log_error!("registering an empty address list!"); } } Ok(()) @@ -236,7 +258,7 @@ impl UtxoProcessor { pub async fn handle_daa_score_change(&self, current_daa_score: u64) -> Result<()> { self.inner.current_daa_score.store(current_daa_score, Ordering::SeqCst); - self.notify(Events::DAAScoreChange { current_daa_score }).await?; + self.notify(Events::DaaScoreChange { current_daa_score }).await?; self.handle_pending(current_daa_score).await?; self.handle_outgoing(current_daa_score).await?; Ok(()) @@ -409,6 +431,10 @@ impl UtxoProcessor { self.sync_proc().is_synced() } + pub fn is_running(&self) -> bool { + self.inner.task_is_running.load(Ordering::SeqCst) + } + pub async fn init_state_from_server(&self) -> Result { let GetServerInfoResponse { server_version, @@ -450,22 +476,43 @@ impl UtxoProcessor { self.notify(Events::UtxoProcStart).await?; self.sync_proc().track(is_synced).await?; + let this = self.clone(); + self.inner.metrics.register_sink(Arc::new(Box::new(move |snapshot: MetricsSnapshot| { + if let Err(err) = this.deliver_metrics_snapshot(Box::new(snapshot)) { + println!("Error ingesting metrics snapshot: {}", err); + } + None + }))); + Ok(()) } pub async fn handle_connect(&self) -> Result<()> { - if let Err(err) = self.handle_connect_impl().await { - log_error!("UtxoProcessor: error while connecting to node: {err}"); - self.notify(Events::UtxoProcError { message: err.to_string() }).await?; - if let Some(client) = self.rpc_client() { - client.disconnect().await?; + let _ = self.inner.connect_disconnect_guard.lock().await; + + match self.handle_connect_impl().await { + Err(err) => { + log_error!("UtxoProcessor: error while connecting to node: {err}"); + self.notify(Events::UtxoProcError { message: err.to_string() }).await?; + if let Some(client) = self.rpc_client() { + // try force disconnect the client if we have failed + // to negotiate the connection to the node. + client.disconnect().await?; + } + Err(err) } + Ok(_) => Ok(()), } - Ok(()) } pub async fn handle_disconnect(&self) -> Result<()> { + let _ = self.inner.connect_disconnect_guard.lock().await; + self.inner.is_connected.store(false, Ordering::SeqCst); + // self.stop_metrics(); + + self.inner.metrics.unregister_sink(); + self.unregister_notification_listener().await?; self.notify(Events::UtxoProcStop).await?; self.cleanup().await?; @@ -488,9 +535,7 @@ impl UtxoProcessor { ChannelType::Persistent, )); *self.inner.listener_id.lock().unwrap() = Some(listener_id); - self.rpc_api().start_notify(listener_id, Scope::VirtualDaaScoreChanged(VirtualDaaScoreChangedScope {})).await?; - Ok(()) } @@ -520,13 +565,44 @@ impl UtxoProcessor { } _ => { - log_warning!("unknown notification: {:?}", notification); + log_warn!("unknown notification: {:?}", notification); } } Ok(()) } + fn deliver_metrics_snapshot(&self, snapshot: Box) -> Result<()> { + let metrics_kinds = self.inner.metrics_kinds.lock().unwrap().clone(); + for kind in metrics_kinds.into_iter() { + match kind { + MetricsUpdateKind::WalletMetrics => { + let mempool_size = snapshot.get(&Metric::NetworkMempoolSize) as u64; + let node_peers = snapshot.get(&Metric::NodeActivePeers) as u32; + let network_tps = snapshot.get(&Metric::NetworkTransactionsPerSecond); + let metrics = MetricsUpdate::WalletMetrics { mempool_size, node_peers, network_tps }; + self.try_notify(Events::Metrics { network_id: self.network_id()?, metrics })?; + } + } + } + + Ok(()) + } + + pub async fn start_metrics(&self) -> Result<()> { + self.inner.metrics.start_task().await?; + self.inner.metrics.bind_rpc(Some(self.rpc_api().clone())); + + Ok(()) + } + + pub async fn stop_metrics(&self) -> Result<()> { + self.inner.metrics.stop_task().await?; + self.inner.metrics.bind_rpc(None); + + Ok(()) + } + pub async fn start(&self) -> Result<()> { let this = self.clone(); if this.inner.task_is_running.load(Ordering::SeqCst) { @@ -553,18 +629,20 @@ impl UtxoProcessor { Ok(msg) => { // handle RPC channel connection and disconnection events - match msg { - RpcState::Opened => { + RpcState::Connected => { if !this.is_connected() { - this.inner.multiplexer.try_broadcast(Box::new(Events::Connect { - network_id : this.network_id().expect("network id expected during connection"), - url : this.rpc_url() - })).unwrap_or_else(|err| log_error!("{err}")); - this.handle_connect().await.unwrap_or_else(|err| log_error!("{err}")); + if let Err(err) = this.handle_connect().await { + log_error!("UtxoProcessor error: {err}"); + } else { + this.inner.multiplexer.try_broadcast(Box::new(Events::Connect { + network_id : this.network_id().expect("network id expected during connection"), + url : this.rpc_url() + })).unwrap_or_else(|err| log_error!("{err}")); + } } }, - RpcState::Closed => { + RpcState::Disconnected => { if this.is_connected() { this.inner.multiplexer.try_broadcast(Box::new(Events::Disconnect { network_id : this.network_id().expect("network id expected during connection"), @@ -626,6 +704,10 @@ impl UtxoProcessor { } Ok(()) } + + pub fn enable_metrics_kinds(&self, metrics_kinds: &[MetricsUpdateKind]) { + *self.inner.metrics_kinds.lock().unwrap() = metrics_kinds.to_vec(); + } } #[cfg(test)] diff --git a/wallet/core/src/utxo/reference.rs b/wallet/core/src/utxo/reference.rs index 17ee786ea..5e35f9e8b 100644 --- a/wallet/core/src/utxo/reference.rs +++ b/wallet/core/src/utxo/reference.rs @@ -3,7 +3,7 @@ //! use crate::imports::*; -pub use kaspa_consensus_wasm::{TryIntoUtxoEntryReferences, UtxoEntryReference}; +pub use kaspa_consensus_client::{TryIntoUtxoEntryReferences, UtxoEntryReference}; pub enum Maturity { /// Coinbase UTXO that has not reached stasis period. diff --git a/wallet/core/src/utxo/scan.rs b/wallet/core/src/utxo/scan.rs index bcc2035ac..f01257c96 100644 --- a/wallet/core/src/utxo/scan.rs +++ b/wallet/core/src/utxo/scan.rs @@ -88,7 +88,7 @@ impl Scan { let resp = utxo_context.processor().rpc_api().get_utxos_by_addresses(addresses).await?; let elapsed_msec = ts.elapsed().as_secs_f32(); if elapsed_msec > 1.0 { - log_warning!("get_utxos_by_address() fetched {} entries in: {} msec", resp.len(), elapsed_msec); + log_warn!("get_utxos_by_address() fetched {} entries in: {} msec", resp.len(), elapsed_msec); } yield_executor().await; diff --git a/wallet/core/src/utxo/settings.rs b/wallet/core/src/utxo/settings.rs index 716b224da..3890263be 100644 --- a/wallet/core/src/utxo/settings.rs +++ b/wallet/core/src/utxo/settings.rs @@ -5,6 +5,7 @@ use crate::imports::*; +#[derive(Debug)] pub struct NetworkParams { pub coinbase_transaction_maturity_period_daa: u64, pub coinbase_transaction_stasis_period_daa: u64, diff --git a/wallet/core/src/utxo/sync.rs b/wallet/core/src/utxo/sync.rs index 1e0e7bbca..e9eece4f0 100644 --- a/wallet/core/src/utxo/sync.rs +++ b/wallet/core/src/utxo/sync.rs @@ -47,7 +47,7 @@ impl SyncMonitor { pub async fn track(&self, is_synced: bool) -> Result<()> { if self.is_synced() != is_synced || !is_synced && !self.is_running() { if is_synced { - log_trace!("sync monitor: node synced state detected"); + // log_trace!("sync monitor: node synced state detected"); self.inner.is_synced.store(true, Ordering::SeqCst); if self.is_running() { log_trace!("sync monitor: stopping sync monitor task"); @@ -56,7 +56,7 @@ impl SyncMonitor { self.notify(Events::SyncState { sync_state: SyncState::Synced }).await?; } else { self.inner.is_synced.store(false, Ordering::SeqCst); - log_trace!("sync monitor: node is not synced"); + // log_trace!("sync monitor: node is not synced"); if !self.is_running() { log_trace!("sync monitor: starting sync monitor task"); self.start_task().await?; diff --git a/wallet/core/src/wallet/api.rs b/wallet/core/src/wallet/api.rs index 364cbdae9..313759ba7 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -19,15 +19,55 @@ impl WalletApi for super::Wallet { todo!() } - async fn get_status_call(self: Arc, _request: GetStatusRequest) -> Result { + async fn get_status_call(self: Arc, request: GetStatusRequest) -> Result { + let GetStatusRequest { name } = request; + let context = name.and_then(|name| self.inner.retained_contexts.lock().unwrap().get(&name).cloned()); + 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) }; + if let Some(wrpc_client) = self.try_wrpc_client() { (wrpc_client.url(), true) } else { (None, false) }; + + let selected_account_id = self.inner.selected_account.lock().unwrap().as_ref().map(|account| *account.id()); + + let (wallet_descriptor, account_descriptors) = if self.is_open() { + let wallet_descriptor = self.descriptor(); + let account_descriptors = self.account_descriptors().await.ok(); + (wallet_descriptor, account_descriptors) + } else { + (None, None) + }; + + Ok(GetStatusResponse { + is_connected, + is_synced, + is_open, + network_id, + url, + is_wrpc_client, + context, + selected_account_id, + wallet_descriptor, + account_descriptors, + }) + } + + async fn retain_context_call(self: Arc, request: RetainContextRequest) -> Result { + let RetainContextRequest { name, data } = request; - Ok(GetStatusResponse { is_connected, is_synced, is_open, network_id, url, is_wrpc_client }) + if let Some(data) = data { + self.inner.retained_contexts.lock().unwrap().insert(name, Arc::new(data)); + + Ok(RetainContextResponse {}) + } else { + self.inner.retained_contexts.lock().unwrap().remove(&name); + // let data = self.inner.retained_contexts.lock().unwrap().get(&name).cloned(); + Ok(RetainContextResponse {}) + } + + // self.retain_context(retain); } // ------------------------------------------------------------------------------------- @@ -37,16 +77,19 @@ impl WalletApi for super::Wallet { let ConnectRequest { url, network_id } = request; - if let Some(wrpc_client) = self.wrpc_client().as_ref() { + if let Some(wrpc_client) = self.try_wrpc_client().as_ref() { + // self.set_network_id(network_id)?; + // 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())?; + let url = url + .map(|url| wrpc_client.parse_url_with_network_type(url, network_id.into()).map_err(|e| e.to_string())) + .transpose()?; + let options = ConnectOptions { block_async_connect: false, strategy: ConnectStrategy::Retry, url, ..Default::default() }; + wrpc_client.disconnect().await?; + + self.set_network_id(&network_id)?; + + wrpc_client.connect(Some(options)).await.map_err(|e| e.to_string())?; Ok(ConnectResponse {}) } else { Err(Error::NotWrpcClient) @@ -54,19 +97,25 @@ impl WalletApi for super::Wallet { } async fn disconnect_call(self: Arc, _request: DisconnectRequest) -> Result { - if let Some(wrpc_client) = self.wrpc_client().as_ref() { - wrpc_client.shutdown().await?; + if let Some(wrpc_client) = self.try_wrpc_client() { + wrpc_client.disconnect().await?; Ok(DisconnectResponse {}) } else { Err(Error::NotWrpcClient) } } + async fn change_network_id_call(self: Arc, request: ChangeNetworkIdRequest) -> Result { + let ChangeNetworkIdRequest { network_id } = &request; + self.set_network_id(network_id)?; + Ok(ChangeNetworkIdResponse {}) + } + // ------------------------------------------------------------------------------------- async fn ping_call(self: Arc, request: PingRequest) -> Result { - log_info!("Wallet received ping request '{:?}' ...", request.payload); - Ok(PingResponse { payload: request.payload }) + log_info!("Wallet received ping request '{:?}' ...", request.message); + Ok(PingResponse { message: request.message }) } async fn batch_call(self: Arc, _request: BatchRequest) -> Result { @@ -81,8 +130,8 @@ impl WalletApi for super::Wallet { } async fn wallet_enumerate_call(self: Arc, _request: WalletEnumerateRequest) -> Result { - let wallet_list = self.store().wallet_list().await?; - Ok(WalletEnumerateResponse { wallet_list }) + let wallet_descriptors = self.store().wallet_list().await?; + Ok(WalletEnumerateResponse { wallet_descriptors }) } async fn wallet_create_call(self: Arc, request: WalletCreateRequest) -> Result { @@ -94,9 +143,9 @@ impl WalletApi for super::Wallet { } async fn wallet_open_call(self: Arc, request: WalletOpenRequest) -> Result { - let WalletOpenRequest { wallet_secret, wallet_filename, account_descriptors, legacy_accounts } = request; + let WalletOpenRequest { wallet_secret, 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?; + let account_descriptors = self.open(&wallet_secret, filename, args).await?; Ok(WalletOpenResponse { account_descriptors }) } @@ -179,11 +228,43 @@ impl WalletApi for super::Wallet { Ok(AccountsRenameResponse {}) } + async fn accounts_select_call(self: Arc, request: AccountsSelectRequest) -> Result { + let AccountsSelectRequest { account_id } = request; + + if let Some(account_id) = account_id { + let account = self.get_account_by_id(&account_id).await?.ok_or(Error::AccountNotFound(account_id))?; + self.select(Some(&account)).await?; + } else { + self.select(None).await?; + } + // self.inner.selected_account.lock().unwrap().replace(account); + + Ok(AccountsSelectResponse {}) + } + 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::>(); + // let iter = self.inner.store.as_account_store().unwrap().iter(None).await.unwrap(); + // let wallet = self.clone(); + + // let stream = iter.then(move |stored| { + // let wallet = wallet.clone(); - Ok(AccountsEnumerateResponse { descriptor_list }) + // async move { + // let (stored_account, stored_metadata) = stored.unwrap(); + // if let Some(account) = wallet.legacy_accounts().get(&stored_account.id) { + // account.descriptor() + // } else if let Some(account) = wallet.active_accounts().get(&stored_account.id) { + // account.descriptor() + // } else { + // try_load_account(&wallet, stored_account, stored_metadata).await?.descriptor() + // } + // } + // }); + + // let account_descriptors = stream.try_collect::>().await?; + + let account_descriptors = self.account_descriptors().await?; + Ok(AccountsEnumerateResponse { account_descriptors }) } async fn accounts_activate_call(self: Arc, request: AccountsActivateRequest) -> Result { @@ -221,6 +302,18 @@ impl WalletApi for super::Wallet { Ok(AccountsCreateResponse { account_descriptor }) } + async fn accounts_ensure_default_call( + self: Arc, + request: AccountsEnsureDefaultRequest, + ) -> Result { + let AccountsEnsureDefaultRequest { wallet_secret, payment_secret, account_kind, mnemonic_phrase } = request; + + let account_descriptor = + self.ensure_default_account_impl(&wallet_secret, payment_secret.as_ref(), account_kind, mnemonic_phrase.as_ref()).await?; + + Ok(AccountsEnsureDefaultResponse { account_descriptor }) + } + async fn accounts_import_call(self: Arc, _request: AccountsImportRequest) -> Result { // TODO handle account imports return Err(Error::NotImplemented); @@ -229,8 +322,8 @@ impl WalletApi for super::Wallet { 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 }) + let account_descriptor = account.descriptor().unwrap(); + Ok(AccountsGetResponse { account_descriptor }) } async fn accounts_create_new_address_call( diff --git a/wallet/core/src/wallet/args.rs b/wallet/core/src/wallet/args.rs index 93d012cab..05d57c445 100644 --- a/wallet/core/src/wallet/args.rs +++ b/wallet/core/src/wallet/args.rs @@ -3,7 +3,7 @@ //! use crate::imports::*; -use crate::secret::Secret; +// use crate::secret::Secret; use crate::storage::interface::CreateArgs; use crate::storage::{Hint, PrvKeyDataId}; use borsh::{BorshDeserialize, BorshSerialize}; @@ -63,11 +63,11 @@ impl WalletOpenArgs { pub struct PrvKeyDataCreateArgs { pub name: Option, pub payment_secret: Option, - pub mnemonic: String, + pub mnemonic: Secret, } impl PrvKeyDataCreateArgs { - pub fn new(name: Option, payment_secret: Option, mnemonic: String) -> Self { + pub fn new(name: Option, payment_secret: Option, mnemonic: Secret) -> Self { Self { name, payment_secret, mnemonic } } } @@ -78,6 +78,29 @@ impl Zeroize for PrvKeyDataCreateArgs { } } +#[wasm_bindgen(typescript_custom_section)] +const TS_ACCOUNT_CREATE_ARGS: &'static str = r#" + +export interface IPrvKeyDataArgs { + prvKeyDataId: HexString; + paymentSecret?: string; +} + +export interface IAccountCreateArgsBip32 { + accountName?: string; + accountIndex?: number; +} + +/** + * @category Wallet API + */ +export interface IAccountCreateArgs { + type : "bip32"; + args : IAccountCreateArgsBip32; + prvKeyDataArgs? : IPrvKeyDataArgs; +} +"#; + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct AccountCreateArgsBip32 { pub account_name: Option, @@ -103,6 +126,7 @@ impl PrvKeyDataArgs { } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(tag = "type", content = "args")] pub enum AccountCreateArgs { Bip32 { prv_key_data_args: PrvKeyDataArgs, diff --git a/wallet/core/src/wallet/mod.rs b/wallet/core/src/wallet/mod.rs index 301ba1be5..d20f041ad 100644 --- a/wallet/core/src/wallet/mod.rs +++ b/wallet/core/src/wallet/mod.rs @@ -1,8 +1,14 @@ //! //! Kaspa wallet runtime implementation. //! +pub mod api; +pub mod args; +pub mod maps; +pub use args::*; use crate::account::ScanNotifier; +use crate::compat::gen1::decrypt_mnemonic; +use crate::error::Error::Custom; use crate::factory::try_load_account; use crate::imports::*; use crate::settings::{SettingsStore, WalletSettings}; @@ -10,23 +16,14 @@ use crate::storage::interface::{OpenArgs, StorageDescriptor}; use crate::storage::local::interface::LocalStore; use crate::storage::local::Storage; use crate::wallet::maps::ActiveAccountMap; -use chacha20poly1305::{aead::AeadMut, Key, KeyInit}; use kaspa_bip32::{ExtendedKey, Language, Mnemonic, Prefix as KeyPrefix, WordCount}; use kaspa_notify::{ listener::ListenerId, scope::{Scope, VirtualDaaScoreChangedScope}, }; -use kaspa_rpc_core::notify::mode::NotificationMode; -use kaspa_wrpc_client::{KaspaRpcClient, WrpcEncoding}; +use kaspa_wrpc_client::{KaspaRpcClient, Resolver, WrpcEncoding}; use workflow_core::task::spawn; -use crate::error::Error::Custom; - -pub mod api; -pub mod args; -pub mod maps; -pub use args::*; - #[derive(Debug)] pub struct EncryptedMnemonic> { pub cipher: T, // raw @@ -92,9 +89,16 @@ pub struct Inner { multiplexer: Multiplexer>, wallet_bus: Channel, estimation_abortables: Mutex>, + retained_contexts: Mutex>>>, } -/// `Wallet` data structure +/// +/// `Wallet` represents a single wallet instance. +/// It is the main data structure responsible for +/// managing a runtime wallet. +/// +/// @category Wallet API +/// #[derive(Clone)] pub struct Wallet { inner: Arc, @@ -109,17 +113,22 @@ impl Wallet { 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_new(storage: Arc, resolver: Option, network_id: Option) -> Result { + Wallet::try_with_wrpc(storage, resolver, network_id) } - pub fn try_with_wrpc(store: Arc, network_id: Option) -> Result { - let rpc_client = Arc::new(KaspaRpcClient::new_with_args( - WrpcEncoding::Borsh, - NotificationMode::MultiListeners, - "wrpc://127.0.0.1:17110", - None, - )?); + pub fn try_with_wrpc(store: Arc, resolver: Option, network_id: Option) -> Result { + let rpc_client = + Arc::new(KaspaRpcClient::new_with_args(WrpcEncoding::Borsh, Some("wrpc://127.0.0.1:17110"), resolver, network_id, None)?); + + // pub fn try_with_wrpc(store: Arc, network_id: Option) -> Result { + // let rpc_client = Arc::new(KaspaRpcClient::new_with_args( + // WrpcEncoding::Borsh, + // NotificationMode::MultiListeners, + // "wrpc://127.0.0.1:17110", + // None, + // )?); + let rpc_ctl = rpc_client.ctl().clone(); let rpc_api: Arc = rpc_client; let rpc = Rpc::new(rpc_api, rpc_ctl); @@ -145,6 +154,7 @@ impl Wallet { utxo_processor: utxo_processor.clone(), wallet_bus, estimation_abortables: Mutex::new(HashMap::new()), + retained_contexts: Mutex::new(HashMap::new()), }), }; @@ -358,7 +368,7 @@ impl Wallet { } } - async fn activate_accounts_impl(self: &Arc, account_ids: Option<&[AccountId]>) -> Result<()> { + 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 { @@ -383,9 +393,9 @@ impl Wallet { } } - self.notify(Events::AccountActivation { ids }).await?; + self.notify(Events::AccountActivation { ids: ids.clone() }).await?; - Ok(()) + Ok(ids) } /// Activates accounts (performs account address space counts, initializes balance tracking, etc.) @@ -414,6 +424,28 @@ impl Wallet { Ok(()) } + pub async fn account_descriptors(self: Arc) -> Result> { + let iter = self.inner.store.as_account_store().unwrap().iter(None).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) { + account.descriptor() + } else if let Some(account) = wallet.active_accounts().get(&stored_account.id) { + account.descriptor() + } else { + try_load_account(&wallet, stored_account, stored_metadata).await?.descriptor() + } + } + }); + + stream.try_collect::>().await + } + 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 } @@ -426,18 +458,26 @@ impl Wallet { 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 try_wrpc_client(&self) -> Option> { + self.try_rpc_api().and_then(|api| api.clone().downcast_arc::().ok()) } pub fn rpc_api(&self) -> Arc { self.utxo_processor().rpc_api() } + pub fn try_rpc_api(&self) -> Option> { + self.utxo_processor().try_rpc_api() + } + pub fn rpc_ctl(&self) -> RpcCtl { self.utxo_processor().rpc_ctl() } + pub fn try_rpc_ctl(&self) -> Option { + self.utxo_processor().try_rpc_ctl() + } + pub fn has_rpc(&self) -> bool { self.utxo_processor().has_rpc() } @@ -468,13 +508,13 @@ impl Wallet { 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(network_id) = settings.get(WalletSettings::Network) { + self.set_network_id(&network_id).unwrap_or_else(|_| log_error!("Unable to select network type: `{}`", network_id)); } 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)); + if let Some(wrpc_client) = self.try_wrpc_client() { + wrpc_client.set_url(Some(url.as_str())).unwrap_or_else(|_| log_error!("Unable to set rpc url: `{}`", url)); } } @@ -489,7 +529,7 @@ impl Wallet { self.start_task().await?; self.utxo_processor().start().await?; // rpc services (notifier) - if let Some(rpc_client) = self.wrpc_client() { + if let Some(rpc_client) = self.try_wrpc_client() { rpc_client.start().await?; } @@ -526,11 +566,15 @@ impl Wallet { Ok(()) } - pub fn set_network_id(&self, network_id: NetworkId) -> Result<()> { + 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); + + if let Some(wrpc_client) = self.try_wrpc_client() { + wrpc_client.set_network_id(network_id)?; + } Ok(()) } @@ -544,7 +588,7 @@ impl Wallet { pub fn default_port(&self) -> Result> { let network_type = self.network_id()?; - if let Some(wrpc_client) = self.wrpc_client() { + if let Some(wrpc_client) = self.try_wrpc_client() { let port = match wrpc_client.encoding() { WrpcEncoding::Borsh => network_type.default_borsh_rpc_port(), WrpcEncoding::SerdeJson => network_type.default_json_rpc_port(), @@ -752,7 +796,7 @@ impl Wallet { wallet_secret: &Secret, prv_key_data_create_args: PrvKeyDataCreateArgs, ) -> Result { - let mnemonic = Mnemonic::new(prv_key_data_create_args.mnemonic, Language::default())?; + let mnemonic = Mnemonic::new(prv_key_data_create_args.mnemonic.as_str()?, Language::default())?; let prv_key_data = PrvKeyData::try_from_mnemonic( mnemonic.clone(), prv_key_data_create_args.payment_secret.as_ref(), @@ -895,6 +939,15 @@ impl Wallet { let events = self.multiplexer().channel(); let wallet_bus_receiver = self.wallet_bus().receiver.clone(); + // let this_clone = self.clone(); + // spawn(async move { + // loop { + // log_info!("Wallet broadcasting ping..."); + // this_clone.notify(Events::WalletPing).await.expect("Wallet::start_task() `notify` error"); + // sleep(Duration::from_secs(5)).await; + // } + // }); + spawn(async move { loop { select! { @@ -942,6 +995,20 @@ impl Wallet { Ok(()) } + pub fn enable_metrics_kinds(&self, kinds: &[MetricsUpdateKind]) { + self.utxo_processor().enable_metrics_kinds(kinds); + } + + pub async fn start_metrics(&self) -> Result<()> { + self.utxo_processor().start_metrics().await?; + Ok(()) + } + + pub async fn stop_metrics(&self) -> Result<()> { + self.utxo_processor().stop_metrics().await?; + Ok(()) + } + pub fn is_open(&self) -> bool { self.inner.store.is_open() } @@ -1256,7 +1323,7 @@ impl Wallet { payment_secret: Option<&Secret>, notifier: Option, ) -> Result> { - use crate::derivation::gen0::import::load_v0_keydata; + use crate::compat::gen0::load_v0_keydata; let notifier = notifier.as_ref(); let keydata = load_v0_keydata(import_secret).await?; @@ -1303,12 +1370,12 @@ impl Wallet { Ok(account) } - pub async fn import_gen1_keydata(self: &Arc, secret: Secret) -> Result<()> { - use crate::derivation::gen1::import::load_v1_keydata; + 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(()) + // let _keydata = load_v1_keydata(&secret).await?; + todo!(); + // Ok(()) } pub async fn import_with_mnemonic( @@ -1377,12 +1444,14 @@ impl Wallet { /// accounts. pub async fn scan_bip44_accounts( self: &Arc, - mut bip39_mnemonic: String, + bip39_mnemonic: Secret, bip39_passphrase: Option, address_scan_extent: u32, account_scan_extent: u32, ) -> Result { - let mnemonic = Mnemonic::new(bip39_mnemonic.as_str(), Language::English)?; + let bip39_mnemonic = std::str::from_utf8(bip39_mnemonic.as_ref()).map_err(|_| Error::InvalidMnemonicPhrase)?; + let mnemonic = Mnemonic::new(bip39_mnemonic, 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)?; @@ -1406,8 +1475,6 @@ impl Wallet { account_index += 1; } - bip39_mnemonic.zeroize(); - Ok(last_account_index) } @@ -1483,40 +1550,82 @@ impl Wallet { store.rename(wallet_secret, title.as_deref(), filename.as_deref()).await?; Ok(()) } -} -fn decrypt_mnemonic>( - num_threads: u32, - EncryptedMnemonic { cipher, salt }: EncryptedMnemonic, - pass: &[u8], -) -> Result { - let params = argon2::ParamsBuilder::new().t_cost(1).m_cost(64 * 1024).p_cost(num_threads).output_len(32).build().unwrap(); - let mut key = [0u8; 32]; - argon2::Argon2::new(argon2::Algorithm::Argon2id, Default::default(), params) - .hash_password_into(pass, salt.as_ref(), &mut key[..]) - .unwrap(); - let mut aead = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(&key)); - let (nonce, ciphertext) = cipher.as_ref().split_at(24); - - let decrypted = aead.decrypt(nonce.into(), ciphertext).unwrap(); - Ok(unsafe { String::from_utf8_unchecked(decrypted) }) + async fn ensure_default_account_impl( + self: Arc, + wallet_secret: &Secret, + payment_secret: Option<&Secret>, + kind: AccountKind, + mnemonic_phrase: Option<&Secret>, + ) -> Result { + if kind != BIP32_ACCOUNT_KIND { + return Err(Error::custom("Account kind is not supported")); + } + + let account = self.store().as_account_store()?.iter(None).await?.next().await; + + if let Some(Ok((stored_account, stored_metadata))) = account { + let account_descriptor = try_load_account(&self, stored_account, stored_metadata).await?.descriptor()?; + Ok(account_descriptor) + } else { + let mnemonic_phrase_string = if let Some(phrase) = mnemonic_phrase.cloned() { + phrase + } else { + let mnemonic = Mnemonic::random(WordCount::Words24, Language::English)?; + Secret::from(mnemonic.phrase_string()) + }; + + let prv_key_data_args = PrvKeyDataCreateArgs::new(None, payment_secret.cloned(), mnemonic_phrase_string); + + self.store().batch().await?; + let prv_key_data_id = self.clone().create_prv_key_data(wallet_secret, prv_key_data_args).await?; + + let account_create_args = AccountCreateArgs::new_bip32(prv_key_data_id, payment_secret.cloned(), None, None); + + let account = self.clone().create_account(wallet_secret, account_create_args, false).await?; + + self.store().flush(wallet_secret).await?; + + Ok(account.descriptor()?) + } + } } +// fn decrypt_mnemonic>( +// num_threads: u32, +// EncryptedMnemonic { cipher, salt }: EncryptedMnemonic, +// pass: &[u8], +// ) -> Result { +// let params = argon2::ParamsBuilder::new().t_cost(1).m_cost(64 * 1024).p_cost(num_threads).output_len(32).build().unwrap(); +// let mut key = [0u8; 32]; +// argon2::Argon2::new(argon2::Algorithm::Argon2id, Default::default(), params) +// .hash_password_into(pass, salt.as_ref(), &mut key[..]) +// .unwrap(); +// let mut aead = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(&key)); +// let (nonce, ciphertext) = cipher.as_ref().split_at(24); + +// let decrypted = aead.decrypt(nonce.into(), ciphertext).unwrap(); +// Ok(unsafe { String::from_utf8_unchecked(decrypted) }) +// } + #[cfg(not(target_arch = "wasm32"))] #[cfg(test)] mod test { - use hex_literal::hex; - use std::{str::FromStr, thread::sleep, time}; + // use hex_literal::hex; - use super::*; + // use super::*; + // use kaspa_addresses::Address; + + /* + use workflow_rpc::client::ConnectOptions; + use std::{str::FromStr, thread::sleep, time}; use crate::derivation::gen1; use crate::utxo::{UtxoContext, UtxoContextBinding, UtxoIterator}; - use kaspa_addresses::{Address, Prefix, Version}; + use kaspa_addresses::{Prefix, Version}; use kaspa_bip32::{ChildNumber, ExtendedPrivateKey, SecretKey}; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; use kaspa_consensus_wasm::{sign_transaction, SignableTransaction, Transaction, TransactionInput, TransactionOutput}; use kaspa_txscript::pay_to_address_script; - use workflow_rpc::client::ConnectOptions; async fn create_utxos_context_with_addresses( rpc: Arc, @@ -1631,201 +1740,5 @@ mod test { Ok(()) } - - #[test] - fn decrypt_go_encrypted_mnemonics_test() { - let file = SingleWalletFileV1{ - encrypted_mnemonic: EncryptedMnemonic { - cipher: hex!("2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc").as_slice(), - salt: hex!("044f5b890e48af4a7dcd7e7766af9380").as_slice(), - }, - xpublic_key: "kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA", - ecdsa: false, - }; - - let decrypted = decrypt_mnemonic(8, file.encrypted_mnemonic, b"").unwrap(); - assert_eq!("dizzy uncover funny time weapon chat volume squirrel comic motion until diamond response remind hurt spider door strategy entire oyster hawk marriage soon fabric", decrypted); - } - - #[tokio::test] - async fn import_golang_single_wallet_test() { - let resident_store = Wallet::resident_store().unwrap(); - let wallet = Arc::new(Wallet::try_new(resident_store, Some(NetworkId::new(NetworkType::Mainnet))).unwrap()); - let wallet_secret = Secret::new(vec![]); - - wallet - .create_wallet( - &wallet_secret, - WalletCreateArgs { - title: None, - filename: None, - encryption_kind: EncryptionKind::XChaCha20Poly1305, - user_hint: None, - overwrite_wallet_storage: false, - }, - ) - .await - .unwrap(); - - let file = SingleWalletFileV1{ - encrypted_mnemonic: EncryptedMnemonic { - cipher: hex!("2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc").as_slice(), - salt: hex!("044f5b890e48af4a7dcd7e7766af9380").as_slice(), - }, - xpublic_key: "kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA", - ecdsa: false, - }; - let import_secret = Secret::new(vec![]); - - let acc = wallet.import_kaspawallet_golang_single_v1(&import_secret, &wallet_secret, file).await.unwrap(); - assert_eq!( - acc.receive_address().unwrap(), - Address::try_from("kaspa:qpuvlauc6a5syze9g70dnxzzvykhkuatsjrx87mxqccqh7kf9kcssdkp9ec7w").unwrap(), // taken from golang impl - ); - } - - #[tokio::test] - async fn import_golang_multisig_v1_wallet_test() { - let resident_store = Wallet::resident_store().unwrap(); - let wallet = Arc::new(Wallet::try_new(resident_store, Some(NetworkId::new(NetworkType::Mainnet))).unwrap()); - let wallet_secret = Secret::new(vec![]); - - wallet - .create_wallet( - &wallet_secret, - WalletCreateArgs { - title: None, - filename: None, - encryption_kind: EncryptionKind::XChaCha20Poly1305, - user_hint: None, - overwrite_wallet_storage: false, - }, - ) - .await - .unwrap(); - - let file = MultisigWalletFileV1{ - encrypted_mnemonics: vec![ - EncryptedMnemonic { - cipher: hex!("f587dbc539b5303605e7065f4a473caffc91d5992dc0c4ec0b111e5362aa089c6ed034d4165697c13776777fa6a9396b0396515f75fa8fa34d13a3abdbf126bf8575be389177998c77170f3dba80c18d7cb5e223802cd4df51584ea280c08f31a8ecccca31000f4ebd78d584ba95ad2424b57a2945c60a7a36174bf69ecf251c141f01644aeb10268f3321bc2114a24da8ab8983540224e494634889a48f846ceea4238869d1e397f041f5594c53453ea63606a4bb50").as_slice(), - salt: hex!("04fb57493be318c3bb1cddb6dde05e09").as_slice(), - }, - EncryptedMnemonic { - cipher: hex!("2244d1b757e635cec13347d8b6d57c446063b9b72f54c425055eefd983c11cd4d75b0303e47848b5df29991056769c109cad73844fcc4de3d68122fdc09ec31a9e26334cb65141de1fb74718fd44e1d7312eaf975871833026569f06624f02ea79ba189e2db8cbfc4a1ada7fc4801179fb9b838618418043a335e8e01ab9dc8b6b8a1aa963a827a7914bab0815337d3955e5d2a4fc2df738506d5eb537ca7c52c690106bde9d2b686949a2e651099311796df3698499e8606cdbdc9963fc9172b12b").as_slice(), - salt: hex!("60405c5b3a180e4fdebd5a6d5c51bf76").as_slice(), - }, - ], - xpublic_keys: vec![ - "kpub2J937qL9n85s7HrhYyYYdMkzq1kaMiAf9PAcJzRW3jV7NgntNfGGrNgut7ZxcVrJqH42BCT2WyjfnxJh3SBDjLhXHe3UC2RJUu5tcjsViuK", - "kpub2Jtuqt6WJWZv3fQUnKhuEaCxbAyzLsFn3UEEaM4g7CXa2LZjQZH4o6tpj83tFaewMEyX56qrAF4Q64uqunVyBayuuRNwjru5DWchDEcq5vz", - "kpub2JZg9pofE54nqvkhFRRx18pAMhYDPL2CpYqBx2AkzvsEknCh8V4rtez9ZYeab3HCW1Xsm9f4d6J5dfJVg9NADWN7rtqNft21batcii1SjXy", - "kpub2HuRXjAmhs3KwQ9WpHVaiHRjBP37TQUiUGFQBTwp7cdbArCo5s2MT6415nd3ZYaELvNbZ4qTJjCGTavExv514tWftaGQzCK8gQz6BQJNySp", - "kpub2KCvcuKVgfy1h7PvCw4xFcdLAPoerVZBG4qTo8vRGH2Qe6p5AgLyRek5CEnuCDkduXHqgwtvaVfYYBS7gQBR1J4XowdvqvPXsHZGA5WyRJF", - ], - required_signatures: 2, - cosigner_index: 1, - ecdsa: false, - }; - let import_secret = Secret::new(vec![]); - - let acc = wallet.import_kaspawallet_golang_multisig_v1(&import_secret, &wallet_secret, file).await.unwrap(); - assert_eq!( - acc.receive_address().unwrap(), - Address::try_from("kaspa:pqvgkyjeuxmd8k70egrrzpdz5rqj0acmr6y94mwsltxfp6nc50742295c3998").unwrap(), // taken from golang impl - ); - } - - #[test] - fn deser_golang_wallet_test() { - #[allow(dead_code)] - #[derive(Debug)] - enum WalletType<'a> { - SingleV0(SingleWalletFileV0<'a, Vec>), - SingleV1(SingleWalletFileV1<'a, Vec>), - MultiV0(MultisigWalletFileV0<'a, Vec>), - MultiV1(MultisigWalletFileV1<'a, Vec>), - } - - #[derive(Debug, Default, Deserialize)] - struct EncryptedMnemonicIntermediate { - #[serde(with = "kaspa_utils::serde_bytes")] - cipher: Vec, - #[serde(with = "kaspa_utils::serde_bytes")] - salt: Vec, - } - impl From for EncryptedMnemonic> { - fn from(value: EncryptedMnemonicIntermediate) -> Self { - Self { cipher: value.cipher, salt: value.salt } - } - } - - #[derive(serde_repr::Deserialize_repr, PartialEq, Debug)] - #[repr(u8)] - enum WalletVersion { - Zero = 0, - One = 1, - } - #[derive(Debug, Deserialize)] - #[serde(rename_all = "camelCase")] - struct UnifiedWalletIntermediate<'a> { - version: WalletVersion, - num_threads: Option, - encrypted_mnemonics: Vec, - #[serde(borrow)] - public_keys: Vec<&'a str>, - minimum_signatures: u16, - cosigner_index: u8, - ecdsa: bool, - } - - impl<'a> UnifiedWalletIntermediate<'a> { - fn into_wallet_type(mut self) -> WalletType<'a> { - let single = self.encrypted_mnemonics.len() == 1 && self.public_keys.len() == 1; - match (single, self.version) { - (true, WalletVersion::Zero) => WalletType::SingleV0(SingleWalletFileV0 { - num_threads: self.num_threads.expect("num_threads must present in case of v0") as u32, - encrypted_mnemonic: std::mem::take(&mut self.encrypted_mnemonics[0]).into(), - xpublic_key: self.public_keys[0], - ecdsa: self.ecdsa, - }), - (true, WalletVersion::One) => WalletType::SingleV1(SingleWalletFileV1 { - encrypted_mnemonic: std::mem::take(&mut self.encrypted_mnemonics[0]).into(), - xpublic_key: self.public_keys[0], - ecdsa: self.ecdsa, - }), - (false, WalletVersion::Zero) => WalletType::MultiV0(MultisigWalletFileV0 { - num_threads: self.num_threads.expect("num_threads must present in case of v0") as u32, - encrypted_mnemonics: self - .encrypted_mnemonics - .into_iter() - .map(|EncryptedMnemonicIntermediate { cipher, salt }| EncryptedMnemonic { cipher, salt }) - .collect(), - xpublic_keys: self.public_keys, - required_signatures: self.minimum_signatures, - cosigner_index: self.cosigner_index, - ecdsa: self.ecdsa, - }), - (false, WalletVersion::One) => WalletType::MultiV1(MultisigWalletFileV1 { - encrypted_mnemonics: self - .encrypted_mnemonics - .into_iter() - .map(|EncryptedMnemonicIntermediate { cipher, salt }| EncryptedMnemonic { cipher, salt }) - .collect(), - xpublic_keys: self.public_keys, - required_signatures: self.minimum_signatures, - cosigner_index: self.cosigner_index, - ecdsa: self.ecdsa, - }), - } - } - } - - let single_json_v0 = r#"{"numThreads":8,"version":0,"encryptedMnemonics":[{"cipher":"2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc","salt":"044f5b890e48af4a7dcd7e7766af9380"}],"publicKeys":["kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA"],"minimumSignatures":1,"cosignerIndex":0,"lastUsedExternalIndex":0,"lastUsedInternalIndex":0,"ecdsa":false}"#.to_owned(); - let single_json_v1 = r#"{"version":1,"encryptedMnemonics":[{"cipher":"2022041df1a5bdcc26445952c53f96518641118bf0f990a01747d631d4607e5b53af3c9f4c07d6e3b84bc766445191b13d1f1fdf7ac96eae9c8859a9add660ac15b938356f936fdf614640d89627d368c57b22cf62844b1e1bcf3feceecbc6bf655df9519d7e3cfede6fe19d87a49e5709211b0b95c8d68781c70c4722bd8e25361492ef38d5cca21664a7f0838e4a1e2994d30c6d4b81d1397169570375ce56608439ae00e84c1f6acdd805f0ee22d4ba7b354c7f7cd4b2d18ce4fd6b8af785f95ed2a69361f318bc","salt":"044f5b890e48af4a7dcd7e7766af9380"}],"publicKeys":["kpub2KUE88roSn5peP1rEZnbRuKYw1fEPbhqBoXVWW7mLfkrLvQBAjUqwx7m1ezeSfqfecv9RUYePuHf99iW51i31WjwWjnzKDCUcTucBSiBbJA"],"minimumSignatures":1,"cosignerIndex":0,"lastUsedExternalIndex":0,"lastUsedInternalIndex":0,"ecdsa":false}"#.to_owned(); - - let unified: UnifiedWalletIntermediate = serde_json::from_str(&single_json_v0).unwrap(); - assert!(matches!(unified.into_wallet_type(), WalletType::SingleV0(_))); - let unified: UnifiedWalletIntermediate = serde_json::from_str(&single_json_v1).unwrap(); - assert!(matches!(unified.into_wallet_type(), WalletType::SingleV1(_))); - } + */ } diff --git a/wallet/core/src/wasm/api/extensions.rs b/wallet/core/src/wasm/api/extensions.rs new file mode 100644 index 000000000..8057d7401 --- /dev/null +++ b/wallet/core/src/wasm/api/extensions.rs @@ -0,0 +1,72 @@ +use crate::imports::*; +use js_sys::Object; +use kaspa_consensus_core::Hash; + +pub trait WalletApiObjectExtension { + fn get_secret(&self, key: &str) -> Result; + fn try_get_secret(&self, key: &str) -> Result>; + fn get_network_id(&self, key: &str) -> Result; + fn try_get_prv_key_data_id(&self, key: &str) -> Result>; + fn get_prv_key_data_id(&self, key: &str) -> Result; + fn get_account_id(&self, key: &str) -> Result; + fn try_get_account_id_list(&self, key: &str) -> Result>>; + fn get_transaction_id(&self, key: &str) -> Result; +} + +impl WalletApiObjectExtension for Object { + fn get_secret(&self, key: &str) -> Result { + let string = self.get_value(key)?.as_string().ok_or(Error::InvalidArgument(key.to_string())).map(|s| s.trim().to_string())?; + if string.is_empty() { + Err(Error::SecretIsEmpty(key.to_string())) + } else { + Ok(Secret::from(string)) + } + } + + fn try_get_secret(&self, key: &str) -> Result> { + let string = self.try_get_value(key)?.and_then(|value| value.as_string()); + if let Some(string) = string { + if string.is_empty() { + Err(Error::SecretIsEmpty(key.to_string())) + } else { + Ok(Some(Secret::from(string))) + } + } else { + Ok(None) + } + } + + fn get_network_id(&self, key: &str) -> Result { + let value = self.get_value(key)?; + Ok(NetworkId::try_from(value)?) + } + + fn try_get_prv_key_data_id(&self, key: &str) -> Result> { + if let Some(value) = self.try_get_value(key)? { + Ok(Some(PrvKeyDataId::try_from(&value)?)) + } else { + Ok(None) + } + } + + fn get_prv_key_data_id(&self, key: &str) -> Result { + PrvKeyDataId::try_from(&self.get_value(key)?) + } + + fn get_account_id(&self, key: &str) -> Result { + AccountId::try_from(&self.get_value(key)?) + } + + fn get_transaction_id(&self, key: &str) -> Result { + Ok(Hash::try_owned_from(self.get_value(key)?)?) + } + + fn try_get_account_id_list(&self, key: &str) -> Result>> { + if let Ok(array) = self.get_vec(key) { + let account_ids = array.into_iter().map(|js_value| AccountId::try_from(&js_value)).collect::>>()?; + Ok(Some(account_ids)) + } else { + Ok(None) + } + } +} diff --git a/wallet/core/src/wasm/api/message.rs b/wallet/core/src/wasm/api/message.rs new file mode 100644 index 000000000..33bf5e00d --- /dev/null +++ b/wallet/core/src/wasm/api/message.rs @@ -0,0 +1,1764 @@ +#![allow(non_snake_case)] + +use super::extensions::*; +use crate::account::descriptor::IAccountDescriptor; +use crate::api::message::*; +use crate::imports::*; +use crate::tx::{Fees, PaymentDestination, PaymentOutputs}; +use crate::wasm::tx::fees::IFees; +use crate::wasm::tx::GeneratorSummary; +use js_sys::Array; +use serde_wasm_bindgen::from_value; +use workflow_wasm::serde::to_value; + +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; + +macro_rules! try_from { + ($name:ident : $from_type:ty, $to_type:ty, $body:block) => { + impl TryFrom<$from_type> for $to_type { + type Error = Error; + fn try_from($name: $from_type) -> Result { + $body + } + } + }; +} + +#[wasm_bindgen(typescript_custom_section)] +const TS_CATEGORY_WALLET: &'static str = r#" +/** + * @categoryDescription Wallet API + * Wallet API for interfacing with Rusty Kaspa Wallet implementation. + */ +"#; + +// --- + +// declare! { +// IPingRequest, +// r#" +// /** +// * +// */ +// export interface IPingRequest { +// message?: string; +// } +// "#, +// } + +// try_from! ( args: IPingRequest, PingRequest, { +// let message = args.try_get_string("message")?; +// Ok(PingRequest { message }) +// }); + +// declare! { +// IPingResponse, +// r#" +// /** +// * +// */ +// export interface IPingResponse { +// message?: string; +// } +// "#, +// } + +// try_from! ( args: PingResponse, IPingResponse, { +// let response = IPingResponse::default(); +// if let Some(message) = args.message { +// response.set("message", &JsValue::from_str(&message))?; +// } +// Ok(response) +// }); + +// --- + +declare! { + IBatchRequest, + r#" + /** + * Suspend storage operations until invocation of flush(). + * + * @category Wallet API + */ + export interface IBatchRequest { } + "#, +} + +try_from! ( _args: IBatchRequest, BatchRequest, { + Ok(BatchRequest { }) +}); + +declare! { + IBatchResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IBatchResponse { } + "#, +} + +try_from! ( _args: BatchResponse, IBatchResponse, { + Ok(IBatchResponse::default()) +}); + +// --- + +declare! { + IFlushRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IFlushRequest { + walletSecret : string; + } + "#, +} + +try_from! ( args: IFlushRequest, FlushRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + Ok(FlushRequest { wallet_secret }) +}); + +declare! { + IFlushResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IFlushResponse { } + "#, +} + +try_from! ( _args: FlushResponse, IFlushResponse, { + Ok(IFlushResponse::default()) +}); + +// --- + +declare! { + IConnectRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IConnectRequest { + url : string; + networkId : NetworkId | string; + } + "#, +} + +try_from! ( args: IConnectRequest, ConnectRequest, { + let url = args.try_get_string("url")?; + let network_id = args.get_network_id("networkId")?; + Ok(ConnectRequest { url, network_id }) +}); + +declare! { + IConnectResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IConnectResponse { } + "#, +} + +try_from! ( _args: ConnectResponse, IConnectResponse, { + Ok(IConnectResponse::default()) +}); + +// --- + +declare! { + IDisconnectRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IDisconnectRequest { } + "#, +} + +try_from! ( _args: IDisconnectRequest, DisconnectRequest, { + Ok(DisconnectRequest { }) +}); + +declare! { + IDisconnectResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IDisconnectResponse { } + "#, +} + +try_from! ( _args: DisconnectResponse, IDisconnectResponse, { + Ok(IDisconnectResponse::default()) +}); + +// --- + +declare! { + IGetStatusRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IGetStatusRequest { + /** + * Optional context creation name. + * @see {@link IRetainContextRequest} + */ + name? : string; + } + "#, +} + +try_from! ( args: IGetStatusRequest, GetStatusRequest, { + let name = args.try_get_string("name")?; + Ok(GetStatusRequest { name }) +}); + +declare! { + IGetStatusResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IGetStatusResponse { + isConnected : boolean; + isSynced : boolean; + isOpen : boolean; + url? : string; + networkId? : NetworkId; + context? : HexString; + } + "#, +} + +try_from! ( args: GetStatusResponse, IGetStatusResponse, { + let GetStatusResponse { is_connected, is_synced, is_open, url, network_id, .. } = args; + let response = IGetStatusResponse::default(); + response.set("isConnected", &is_connected.into())?; + response.set("isSynced", &is_synced.into())?; + response.set("isOpen", &is_open.into())?; + if let Some(url) = url { + response.set("url", &url.into())?; + } + if let Some(network_id) = network_id { + response.set("networkId", &network_id.into())?; + } + Ok(response) +}); + +// --- + +declare! { + IRetainContextRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IRetainContextRequest { + /** + * Optional context creation name. + */ + name : string; + /** + * Optional context data to retain. + */ + data? : string; + } + "#, +} + +try_from! ( args: IRetainContextRequest, RetainContextRequest, { + let name = args.get_string("name")?; + let data = args.try_get_string("data")?; + let data = data.map(|data|Vec::::from_hex(data.as_str())).transpose()?; + Ok(RetainContextRequest { name, data }) +}); + +declare! { + IRetainContextResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IRetainContextResponse { + } + "#, +} + +try_from! ( _args: RetainContextResponse, IRetainContextResponse, { + Ok(IRetainContextResponse::default()) +}); + +// --- + +declare! { + IWalletEnumerateRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletEnumerateRequest { } + "#, +} + +try_from! ( _args: IWalletEnumerateRequest, WalletEnumerateRequest, { + Ok(WalletEnumerateRequest { }) +}); + +declare! { + IWalletEnumerateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletEnumerateResponse { + walletDescriptors: WalletDescriptor[]; + } + "#, +} + +try_from! ( args: WalletEnumerateResponse, IWalletEnumerateResponse, { + let response = IWalletEnumerateResponse::default(); + let wallet_descriptors = Array::from_iter(args.wallet_descriptors.into_iter().map(JsValue::from)); + response.set("walletDescriptors", &JsValue::from(&wallet_descriptors))?; + Ok(response) +}); + +// --- + +declare! { + IWalletCreateRequest, + r#" + /** + * + * If filename is not supplied, the filename will be derived from the wallet title. + * If both wallet title and filename are not supplied, the wallet will be create + * with the default filename `kaspa`. + * + * @category Wallet API + */ + export interface IWalletCreateRequest { + /** Wallet encryption secret */ + walletSecret: string; + /** Optional wallet title */ + title?: string; + /** Optional wallet filename */ + filename?: string; + /** Optional user hint */ + userHint?: string; + /** + * Overwrite wallet data if the wallet with the same filename already exists. + * (Use with caution!) + */ + overwriteWalletStorage?: boolean; + } + "#, +} + +// TODO +try_from! ( args: IWalletCreateRequest, WalletCreateRequest, { + + let wallet_secret = args.get_secret("walletSecret")?; + let title = args.try_get_string("title")?; + let filename = args.try_get_string("filename")?; + let user_hint = args.try_get_string("userHint")?.map(Hint::from); + let encryption_kind = EncryptionKind::default(); + let overwrite_wallet_storage = args.try_get_bool("overwriteWalletStorage")?.unwrap_or(false); + + let wallet_args = WalletCreateArgs { + title, + filename, + user_hint, + encryption_kind, + overwrite_wallet_storage, + }; + + Ok(WalletCreateRequest { wallet_secret, wallet_args }) +}); + +declare! { + IWalletCreateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletCreateResponse { + walletDescriptor: IWalletDescriptor; + storageDescriptor: IStorageDescriptor; + } + "#, +} + +try_from! ( args: WalletCreateResponse, IWalletCreateResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +// --- +// NOTE: `legacy_accounts` are disabled in JS API +declare! { + IWalletOpenRequest, + r#" + /** + * + * @category Wallet API + */ + export interface IWalletOpenRequest { + walletSecret: string; + filename?: string; + accountDescriptors: boolean; + } + "#, +} + +try_from! ( args: IWalletOpenRequest, WalletOpenRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + let filename = args.try_get_string("filename")?; + let account_descriptors = args.get_value("accountDescriptors")?.as_bool().unwrap_or(false); + + Ok(WalletOpenRequest { wallet_secret, filename, account_descriptors, legacy_accounts: None }) +}); + +declare! { + IWalletOpenResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletOpenResponse { + accountDescriptors: IAccountDescriptor[]; + } + "# +} + +try_from!(args: WalletOpenResponse, IWalletOpenResponse, { + let response = IWalletOpenResponse::default(); + if let Some(account_descriptors) = args.account_descriptors { + let account_descriptors = account_descriptors.into_iter().map(IAccountDescriptor::try_from).collect::>>()?; + response.set("accountDescriptors", &Array::from_iter(account_descriptors.into_iter()))?; + } + Ok(response) +}); + +// --- + +declare! { + IWalletCloseRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletCloseRequest { } + "#, +} + +try_from! ( _args: IWalletCloseRequest, WalletCloseRequest, { + Ok(WalletCloseRequest { }) +}); + +declare! { + IWalletCloseResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletCloseResponse { } + "#, +} + +try_from! ( _args: WalletCloseResponse, IWalletCloseResponse, { + Ok(IWalletCloseResponse::default()) +}); + +// --- + +declare! { + IWalletReloadRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletReloadRequest { + /** + * Reactivate accounts that are active before the reload. + */ + reactivate: boolean; + } + "#, +} + +try_from! ( args: IWalletReloadRequest, WalletReloadRequest, { + let reactivate = args.get_bool("reactivate")?; + Ok(WalletReloadRequest { reactivate }) +}); + +declare! { + IWalletReloadResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletReloadResponse { } + "#, +} + +try_from! ( _args: WalletReloadResponse, IWalletReloadResponse, { + Ok(IWalletReloadResponse::default()) +}); + +// --- + +declare! { + IWalletChangeSecretRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletChangeSecretRequest { + oldWalletSecret: string; + newWalletSecret: string; + } + "#, +} + +try_from! ( args: IWalletChangeSecretRequest, WalletChangeSecretRequest, { + let old_wallet_secret = args.get_secret("oldWalletSecret")?; + let new_wallet_secret = args.get_secret("newWalletSecret")?; + Ok(WalletChangeSecretRequest { old_wallet_secret, new_wallet_secret }) +}); + +declare! { + IWalletChangeSecretResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletChangeSecretResponse { } + "#, +} + +try_from! ( _args: WalletChangeSecretResponse, IWalletChangeSecretResponse, { + Ok(IWalletChangeSecretResponse::default()) +}); + +// --- + +declare! { + IWalletExportRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletExportRequest { + walletSecret: string; + includeTransactions: boolean; + } + "#, +} + +try_from! ( args: IWalletExportRequest, WalletExportRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + let include_transactions = args.get_bool("includeTransactions")?; + Ok(WalletExportRequest { wallet_secret, include_transactions }) +}); + +declare! { + IWalletExportResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletExportResponse { + walletData: HexString; + } + "#, +} + +// TODO +try_from! ( args: WalletExportResponse, IWalletExportResponse, { + let response = IWalletExportResponse::default(); + response.set("walletData", &JsValue::from_str(&args.wallet_data.to_hex()))?; + Ok(response) +}); + +// --- + +declare! { + IWalletImportRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletImportRequest { + walletSecret: string; + walletData: HexString | Uint8Array; + } + "#, +} + +try_from! ( args: IWalletImportRequest, WalletImportRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + let wallet_data = args.get_vec_u8("walletData").map_err(|err|Error::custom(format!("walletData: {err}")))?; + Ok(WalletImportRequest { wallet_secret, wallet_data }) +}); + +declare! { + IWalletImportResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletImportResponse { } + "#, +} + +try_from! ( _args: WalletImportResponse, IWalletImportResponse, { + Ok(IWalletImportResponse::default()) +}); + +// --- + +declare! { + IPrvKeyDataEnumerateRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPrvKeyDataEnumerateRequest { } + "#, +} + +try_from! ( _args: IPrvKeyDataEnumerateRequest, PrvKeyDataEnumerateRequest, { + Ok(PrvKeyDataEnumerateRequest { }) +}); + +declare! { + IPrvKeyDataEnumerateResponse, + r#" + /** + * + * Response returning a list of private key ids, their optional names and properties. + * + * @see {@link IPrvKeyDataInfo} + * @category Wallet API + */ + export interface IPrvKeyDataEnumerateResponse { + prvKeyDataList: IPrvKeyDataInfo[], + } + "#, +} + +try_from! ( args: PrvKeyDataEnumerateResponse, IPrvKeyDataEnumerateResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IPrvKeyDataCreateRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPrvKeyDataCreateRequest { + /** Wallet encryption secret */ + walletSecret: string; + /** Optional name of the private key */ + name? : string; + /** + * Optional key secret (BIP39 passphrase). + * + * If supplied, all operations requiring access + * to the key will require the `paymentSecret` + * to be provided. + */ + paymentSecret? : string; + /** BIP39 mnemonic phrase (12 or 24 words)*/ + mnemonic : string; + } + "#, +} + +try_from! ( args: IPrvKeyDataCreateRequest, PrvKeyDataCreateRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + let name = args.try_get_string("name")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let mnemonic = args.get_secret("mnemonic")?; + + let prv_key_data_args = PrvKeyDataCreateArgs { + name, + payment_secret, + mnemonic, + }; + + Ok(PrvKeyDataCreateRequest { wallet_secret, prv_key_data_args }) +}); + +// TODO +declare! { + IPrvKeyDataCreateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPrvKeyDataCreateResponse { + prvKeyDataId: HexString; + } + "#, +} + +try_from!(args: PrvKeyDataCreateResponse, IPrvKeyDataCreateResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IPrvKeyDataRemoveRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPrvKeyDataRemoveRequest { + walletSecret: string; + prvKeyDataId: HexString; + } + "#, +} + +try_from! ( args: IPrvKeyDataRemoveRequest, PrvKeyDataRemoveRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + let prv_key_data_id = args.get_prv_key_data_id("prvKeyDataId")?; + Ok(PrvKeyDataRemoveRequest { wallet_secret, prv_key_data_id }) +}); + +declare! { + IPrvKeyDataRemoveResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPrvKeyDataRemoveResponse { } + "#, +} + +// TODO +try_from! ( _args: PrvKeyDataRemoveResponse, IPrvKeyDataRemoveResponse, { + Ok(IPrvKeyDataRemoveResponse::default()) +}); + +// --- + +declare! { + IPrvKeyDataGetRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPrvKeyDataGetRequest { + walletSecret: string; + prvKeyDataId: HexString; + } + "#, +} + +try_from! ( args: IPrvKeyDataGetRequest, PrvKeyDataGetRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + let prv_key_data_id = args.get_prv_key_data_id("prvKeyDataId")?; + Ok(PrvKeyDataGetRequest { wallet_secret, prv_key_data_id }) +}); + +declare! { + IPrvKeyDataGetResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IPrvKeyDataGetResponse { + // prvKeyData: PrvKeyData, + } + "#, +} + +// TODO +try_from! ( _args: PrvKeyDataGetResponse, IPrvKeyDataGetResponse, { + todo!(); + // let response = IPrvKeyDataGetResponse::default(); + // Ok(response) +}); + +// --- + +declare! { + IAccountsEnumerateRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsEnumerateRequest { } + "#, +} + +try_from!(_args: IAccountsEnumerateRequest, AccountsEnumerateRequest, { + Ok(AccountsEnumerateRequest { }) +}); + +declare! { + IAccountsEnumerateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsEnumerateResponse { + accountDescriptors: IAccountDescriptor[]; + } + "#, +} + +// TODO +try_from! ( args: AccountsEnumerateResponse, IAccountsEnumerateResponse, { + let response = IAccountsEnumerateResponse::default(); + let account_descriptors = args.account_descriptors.into_iter().map(IAccountDescriptor::try_from).collect::>>()?; + response.set("accountDescriptors", &Array::from_iter(account_descriptors.into_iter()))?; + Ok(response) +}); + +// --- + +declare! { + IAccountsRenameRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsRenameRequest { + accountId: string; + name?: string; + walletSecret: string; + } + "#, +} + +try_from! ( args: IAccountsRenameRequest, AccountsRenameRequest, { + let account_id = args.get_account_id("accountId")?; + let name = args.try_get_string("name")?; + let wallet_secret = args.get_secret("walletSecret")?; + Ok(AccountsRenameRequest { account_id, name, wallet_secret }) +}); + +declare! { + IAccountsRenameResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsRenameResponse { } + "#, +} + +try_from! ( _args: AccountsRenameResponse, IAccountsRenameResponse, { + Ok(IAccountsRenameResponse::default()) +}); + +// --- + +// TODO +declare! { + IAccountsDiscoveryRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsDiscoveryRequest { + discoveryKind: AccountsDiscoveryKind, + accountScanExtent: number, + addressScanExtent: number, + bip39_passphrase?: string, + bip39_mnemonic: string, + } + "#, +} + +// TODO +try_from! (args: IAccountsDiscoveryRequest, AccountsDiscoveryRequest, { + + let discovery_kind = args.get_value("discoveryKind")?; + let discovery_kind = if let Some(discovery_kind) = discovery_kind.as_string() { + discovery_kind.parse()? + } else { + AccountsDiscoveryKind::try_cast_from(&discovery_kind)? + }; + let account_scan_extent = args.get_u32("accountScanExtent")?; + let address_scan_extent = args.get_u32("addressScanExtent")?; + let bip39_passphrase = args.try_get_secret("bip39_passphrase")?; + let bip39_mnemonic = args.get_secret("bip39_mnemonic")?; + + Ok(AccountsDiscoveryRequest { + discovery_kind, + account_scan_extent, + address_scan_extent, + bip39_passphrase, + bip39_mnemonic, + }) +}); + +declare! { + IAccountsDiscoveryResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsDiscoveryResponse { + lastAccountIndexFound : number; + } + "#, +} + +try_from! ( args: AccountsDiscoveryResponse, IAccountsDiscoveryResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IAccountsCreateRequest, + r#" + /** + * + * + * @category Wallet API + */ + export type IAccountsCreateRequest = { + walletSecret: string; + type: "bip32"; + accountName:string; + accountIndex?:number; + prvKeyDataId:string; + paymentSecret?:string; + }; + // |{ + // walletSecret: string; + // type: "multisig"; + // accountName:string; + // accountIndex?:number; + // prvKeyDataId:string; + // pubkeys:HexString[]; + // paymentSecret?:string; + // } + + // |{ + // walletSecret: string; + // type: "bip32-readonly"; + // accountName:string; + // accountIndex?:number; + // pubkey:HexString; + // paymentSecret?:string; + // } + "#, +} + +try_from! (args: IAccountsCreateRequest, AccountsCreateRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + + let kind = AccountKind::try_from(args.try_get_value("type")?.ok_or(Error::custom("type is required"))?)?; + + if kind != crate::account::BIP32_ACCOUNT_KIND { + return Err(Error::custom("only BIP32 accounts are currently supported")); + } + + let prv_key_data_args = PrvKeyDataArgs { + prv_key_data_id: args.try_get_prv_key_data_id("prvKeyDataId")?.ok_or(Error::custom("prvKeyDataId is required"))?, + payment_secret: args.try_get_secret("paymentSecret")?, + }; + + let account_args = AccountCreateArgsBip32 { + account_name: args.try_get_string("accountName")?, + account_index: args.get_u64("accountIndex").ok(), + }; + + let account_create_args = AccountCreateArgs::Bip32 { prv_key_data_args, account_args }; + + Ok(AccountsCreateRequest { wallet_secret, account_create_args }) +}); + +declare! { + IAccountsCreateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsCreateResponse { + accountDescriptor : IAccountDescriptor; + } + "#, +} + +try_from!(args: AccountsCreateResponse, IAccountsCreateResponse, { + let response = IAccountsCreateResponse::default(); + response.set("accountDescriptor", &IAccountDescriptor::try_from(args.account_descriptor)?.into())?; + Ok(response) +}); + +// --- + +declare! { + IAccountsEnsureDefaultRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsEnsureDefaultRequest { + walletSecret: string; + paymentSecret?: string; + type : AccountKind | string; + mnemonic? : string; + } + "#, +} + +try_from! (args: IAccountsEnsureDefaultRequest, AccountsEnsureDefaultRequest, { + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let account_kind = AccountKind::try_from(args.get_value("type")?)?; + let mnemonic_phrase = args.try_get_secret("mnemonic")?; + + Ok(AccountsEnsureDefaultRequest { wallet_secret, payment_secret, account_kind, mnemonic_phrase }) +}); + +declare! { + IAccountsEnsureDefaultResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsEnsureDefaultResponse { + accountDescriptor : IAccountDescriptor; + } + "#, +} + +try_from!(args: AccountsEnsureDefaultResponse, IAccountsEnsureDefaultResponse, { + let response = IAccountsEnsureDefaultResponse::default(); + response.set("accountDescriptor", &IAccountDescriptor::try_from(args.account_descriptor)?.into())?; + Ok(response) +}); + +// --- + +declare! { + IAccountsImportRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsImportRequest { + walletSecret: string; + // TODO + } + "#, +} + +try_from! ( _args: IAccountsImportRequest, AccountsImportRequest, { + unimplemented!(); + // Ok(AccountsImportRequest { }) +}); + +declare! { + IAccountsImportResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsImportResponse { + // TODO + } + "#, +} + +try_from! ( _args: AccountsImportResponse, IAccountsImportResponse, { + unimplemented!(); + // let response = IAccountsImportResponse::default(); + // Ok(response) +}); + +// --- + +declare! { + IAccountsActivateRequest, + "IAccountsActivateRequest", + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsActivateRequest { + accountIds?: HexString[], + } + "#, +} + +try_from! (args: IAccountsActivateRequest, AccountsActivateRequest, { + Ok(from_value::(args.into())?) +}); + +declare! { + IAccountsActivateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsActivateResponse { } + "#, +} + +try_from! ( _args: AccountsActivateResponse, IAccountsActivateResponse, { + Ok(IAccountsActivateResponse::default()) +}); + +// --- + +declare! { + IAccountsDeactivateRequest, + "IAccountsDeactivateRequest", + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsDeactivateRequest { + accountIds?: string[]; + } + "#, +} + +try_from! ( args: IAccountsDeactivateRequest, AccountsDeactivateRequest, { + Ok(from_value::(args.into())?) +}); + +declare! { + IAccountsDeactivateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsDeactivateResponse { } + "#, +} + +try_from! ( _args: AccountsDeactivateResponse, IAccountsDeactivateResponse, { + Ok(IAccountsDeactivateResponse::default()) +}); + +// --- + +declare! { + IAccountsGetRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsGetRequest { + accountId: string; + } + "#, +} + +try_from! ( args: IAccountsGetRequest, AccountsGetRequest, { + Ok(from_value::(args.into())?) +}); + +declare! { + IAccountsGetResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsGetResponse { + accountDescriptor: IAccountDescriptor; + } + "#, +} + +try_from! ( args: AccountsGetResponse, IAccountsGetResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IAccountsCreateNewAddressRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsCreateNewAddressRequest { + accountId: string; + addressKind?: NewAddressKind | string, + } + "#, +} + +try_from!(args: IAccountsCreateNewAddressRequest, AccountsCreateNewAddressRequest, { + let account_id = args.get_account_id("accountId")?; + let value = args.get_value("addressKind")?; + let kind: NewAddressKind = if let Some(string) = value.as_string() { + string.parse()? + } else if let Ok(kind) = NewAddressKind::try_cast_from(&value) { + kind + } else { + NewAddressKind::Receive + }; + Ok(AccountsCreateNewAddressRequest { account_id, kind }) +}); + +declare! { + IAccountsCreateNewAddressResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsCreateNewAddressResponse { + address: Address; + } + "#, +} + +try_from! ( args: AccountsCreateNewAddressResponse, IAccountsCreateNewAddressResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + IAccountsSendRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsSendRequest { + /** + * Hex identifier of the account. + */ + accountId : HexString; + /** + * Wallet encryption secret. + */ + walletSecret : string; + /** + * Optional key encryption secret or BIP39 passphrase. + */ + paymentSecret? : string; + /** + * Priority fee. + */ + priorityFeeSompi? : IFees | bigint; + /** + * + */ + payload? : Uint8Array | HexString; + /** + * If not supplied, the destination will be the change address resulting in a UTXO compound transaction. + */ + destination? : IPaymentOutput[]; + } + "#, +} + +try_from! ( args: IAccountsSendRequest, AccountsSendRequest, { + let account_id = args.get_account_id("accountId")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let priority_fee_sompi = args.get::("priorityFeeSompi")?.try_into()?; + let payload = args.try_get_value("payload")?.map(|v| v.try_as_vec_u8()).transpose()?; + + let outputs = args.get_value("destination")?; + let destination: PaymentDestination = + if outputs.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(outputs)?.into() }; + + Ok(AccountsSendRequest { account_id, wallet_secret, payment_secret, priority_fee_sompi, destination, payload }) +}); + +declare! { + IAccountsSendResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsSendResponse { + /** + * Summary produced by the transaction generator. + */ + generatorSummary : GeneratorSummary; + /** + * Hex identifiers of successfully submitted transactions. + */ + transactionIds : HexString[]; + } + "#, +} + +try_from!(args: AccountsSendResponse, IAccountsSendResponse, { + + let response = IAccountsSendResponse::default(); + response.set("generatorSummary", &GeneratorSummary::from(args.generator_summary).into())?; + response.set("transactionIds", &to_value(&args.transaction_ids)?)?; + Ok(response) +}); + +// --- + +declare! { + IAccountsTransferRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsTransferRequest { + sourceAccountId : HexString; + destinationAccountId : HexString; + walletSecret : string; + paymentSecret? : string; + priorityFeeSompi? : IFees | bigint; + transferAmountSompi : bigint; + } + "#, +} + +try_from! ( args: IAccountsTransferRequest, AccountsTransferRequest, { + let source_account_id = args.get_account_id("sourceAccountId")?; + let destination_account_id = args.get_account_id("destinationAccountId")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let priority_fee_sompi = args.try_get::("priorityFeeSompi")?.map(Fees::try_from).transpose()?; + let transfer_amount_sompi = args.get_u64("transferAmountSompi")?; + + Ok(AccountsTransferRequest { + source_account_id, + destination_account_id, + wallet_secret, + payment_secret, + priority_fee_sompi, + transfer_amount_sompi, + }) +}); + +declare! { + IAccountsTransferResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsTransferResponse { + generatorSummary : GeneratorSummary; + transactionIds : HexString[]; + } + "#, +} + +try_from! ( args: AccountsTransferResponse, IAccountsTransferResponse, { + let response = IAccountsTransferResponse::default(); + response.set("generatorSummary", &GeneratorSummary::from(args.generator_summary).into())?; + response.set("transactionIds", &to_value(&args.transaction_ids)?)?; + Ok(response) +}); + +// --- + +declare! { + IAccountsEstimateRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsEstimateRequest { + accountId : HexString; + destination : IPaymentOutput[]; + priorityFeeSompi : IFees | bigint; + payload? : Uint8Array | string; + } + "#, +} + +try_from! ( args: IAccountsEstimateRequest, AccountsEstimateRequest, { + let account_id = args.get_account_id("accountId")?; + let priority_fee_sompi = args.get::("priorityFeeSompi")?.try_into()?; + let payload = args.try_get_value("payload")?.map(|v| v.try_as_vec_u8()).transpose()?; + + let outputs = args.get_value("destination")?; + let destination: PaymentDestination = + if outputs.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(outputs)?.into() }; + + Ok(AccountsEstimateRequest { account_id, priority_fee_sompi, destination, payload }) +}); + +declare! { + IAccountsEstimateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsEstimateResponse { + generatorSummary : GeneratorSummary; + } + "#, +} + +try_from! ( args: AccountsEstimateResponse, IAccountsEstimateResponse, { + let response = IAccountsEstimateResponse::default(); + response.set("generatorSummary", &GeneratorSummary::from(args.generator_summary).into())?; + Ok(response) +}); + +// --- + +declare! { + ITransactionsDataGetRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface ITransactionsDataGetRequest { + accountId : HexString; + networkId : NetworkId | string; + filter? : TransactionKind[]; + start : bigint; + end : bigint; + } + "#, +} + +try_from! ( args: ITransactionsDataGetRequest, TransactionsDataGetRequest, { + let account_id = args.get_account_id("accountId")?; + let network_id = args.get_network_id("networkId")?; + let filter = args.get_vec("filter").ok().map(|filter| { + filter.into_iter().map(TransactionKind::try_from).collect::>>() + }).transpose()?; + let start = args.get_u64("start")?; + let end = args.get_u64("end")?; + + let request = TransactionsDataGetRequest { + account_id, + network_id, + filter, + start, + end, + }; + Ok(request) +}); + +declare! { + ITransactionsDataGetResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface ITransactionsDataGetResponse { + accountId : HexString; + transactions : ITransactionRecord[]; + start : bigint; + total : bigint; + } + "#, +} + +try_from! ( args: TransactionsDataGetResponse, ITransactionsDataGetResponse, { + Ok(to_value(&args)?.into()) +}); + +// --- + +declare! { + ITransactionsReplaceNoteRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface ITransactionsReplaceNoteRequest { + /** + * The id of account the transaction belongs to. + */ + accountId: HexString, + /** + * The network id of the transaction. + */ + networkId: NetworkId | string, + /** + * The id of the transaction. + */ + transactionId: HexString, + /** + * Optional note string to replace the existing note. + * If not supplied, the note will be removed. + */ + note?: string, + } + "#, +} + +try_from! ( args: ITransactionsReplaceNoteRequest, TransactionsReplaceNoteRequest, { + + let account_id = args.get_account_id("accountId")?; + let network_id = args.get_network_id("networkId")?; + let transaction_id = args.get_transaction_id("transactionId")?; + let note = args.try_get_string("note")?; + + Ok(TransactionsReplaceNoteRequest { + account_id, + network_id, + transaction_id, + note, + }) +}); + +declare! { + ITransactionsReplaceNoteResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface ITransactionsReplaceNoteResponse { } + "#, +} + +try_from! ( _args: TransactionsReplaceNoteResponse, ITransactionsReplaceNoteResponse, { + Ok(ITransactionsReplaceNoteResponse::default()) +}); + +// --- + +// TODO +declare! { + ITransactionsReplaceMetadataRequest, + r#" + /** + * Metadata is a wallet-specific string that can be used to store arbitrary data. + * It should contain a serialized JSON string with `key` containing the custom + * data stored by the wallet. When interacting with metadata, the wallet should + * always deserialize the JSON string and then serialize it again after making + * changes, preserving any foreign keys that it might encounter. + * + * To preserve foreign metadata, the pattern of access should be: + * `Get -> Modify -> Replace` + * + * @category Wallet API + */ + export interface ITransactionsReplaceMetadataRequest { + /** + * The id of account the transaction belongs to. + */ + accountId: HexString, + /** + * The network id of the transaction. + */ + networkId: NetworkId | string, + /** + * The id of the transaction. + */ + transactionId: HexString, + /** + * Optional metadata string to replace the existing metadata. + * If not supplied, the metadata will be removed. + */ + metadata?: string, + } + "#, +} + +try_from! ( args: ITransactionsReplaceMetadataRequest, TransactionsReplaceMetadataRequest, { + let account_id = args.get_account_id("accountId")?; + let network_id = args.get_network_id("networkId")?; + let transaction_id = args.get_transaction_id("transactionId")?; + let metadata = args.try_get_string("metadata")?; + + Ok(TransactionsReplaceMetadataRequest { + account_id, + network_id, + transaction_id, + metadata, + }) +}); + +declare! { + ITransactionsReplaceMetadataResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface ITransactionsReplaceMetadataResponse { } + "#, +} + +try_from! ( _args: TransactionsReplaceMetadataResponse, ITransactionsReplaceMetadataResponse, { + Ok(ITransactionsReplaceMetadataResponse::default()) +}); + +// --- + +declare! { + IAddressBookEnumerateRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAddressBookEnumerateRequest { } + "#, +} + +try_from! ( _args: IAddressBookEnumerateRequest, AddressBookEnumerateRequest, { + Ok(AddressBookEnumerateRequest { }) +}); + +declare! { + IAddressBookEnumerateResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAddressBookEnumerateResponse { + // TODO + } + "#, +} + +try_from! ( _args: AddressBookEnumerateResponse, IAddressBookEnumerateResponse, { + Err(Error::NotImplemented) +}); + +// --- diff --git a/wallet/core/src/wasm/api/mod.rs b/wallet/core/src/wasm/api/mod.rs new file mode 100644 index 000000000..9d06e017d --- /dev/null +++ b/wallet/core/src/wasm/api/mod.rs @@ -0,0 +1,55 @@ +use crate::api::message::*; +use crate::api::traits::*; +use crate::imports::*; +use crate::result::Result; +use kaspa_wallet_macros::declare_wasm_handlers; + +pub mod extensions; +pub mod message; + +use self::message::*; + +use super::Wallet; + +declare_wasm_handlers!([ + /// Ping backend + // Ping, + Batch, + Flush, + // Connect, + // Disconnect, + RetainContext, + GetStatus, + WalletEnumerate, + WalletCreate, + WalletOpen, + WalletReload, + WalletClose, + // WalletExists, + // WalletRename, + WalletChangeSecret, + WalletExport, + WalletImport, + PrvKeyDataEnumerate, + PrvKeyDataCreate, + PrvKeyDataRemove, + PrvKeyDataGet, + AccountsEnumerate, + AccountsRename, + AccountsDiscovery, + AccountsCreate, + AccountsEnsureDefault, + AccountsImport, + AccountsActivate, + AccountsDeactivate, + // AccountsRemove, + AccountsGet, + AccountsCreateNewAddress, + AccountsSend, + AccountsTransfer, + AccountsEstimate, + TransactionsDataGet, + TransactionsReplaceNote, + TransactionsReplaceMetadata, + AddressBookEnumerate, +]); diff --git a/wallet/core/src/wasm/balance.rs b/wallet/core/src/wasm/balance.rs index 4b9763ae7..7bf71384b 100644 --- a/wallet/core/src/wasm/balance.rs +++ b/wallet/core/src/wasm/balance.rs @@ -1,7 +1,15 @@ use crate::imports::*; use crate::result::Result; use crate::utxo::balance as native; +use kaspa_consensus_core::network::NetworkTypeT; +/// +/// Represents a {@link UtxoContext} (account) balance. +/// +/// @see {@link IBalance}, {@link UtxoContext} +/// +/// @category Wallet SDK +/// #[wasm_bindgen] pub struct Balance { inner: native::Balance, @@ -9,19 +17,28 @@ pub struct Balance { #[wasm_bindgen] impl Balance { + /// Confirmed amount of funds available for spending. #[wasm_bindgen(getter)] pub fn mature(&self) -> BigInt { self.inner.mature.into() } + /// Amount of funds that are being received and are not yet confirmed. #[wasm_bindgen(getter)] pub fn pending(&self) -> BigInt { self.inner.pending.into() } - pub fn as_strings(&self, network_type: JsValue) -> Result { + /// Amount of funds that are being send and are not yet accepted by the network. + #[wasm_bindgen(getter)] + pub fn outgoing(&self) -> BigInt { + self.inner.outgoing.into() + } + + #[wasm_bindgen(js_name = "toBalanceStrings")] + pub fn to_balance_strings(&self, network_type: &NetworkTypeT) -> Result { let network_type = NetworkType::try_from(network_type)?; - Ok(native::BalanceStrings::from((&Some(self.inner.clone()), &network_type, None)).into()) + Ok(native::BalanceStrings::from((Some(&self.inner), &network_type, None)).into()) } } @@ -31,6 +48,13 @@ impl From for Balance { } } +/// +/// Formatted string representation of the {@link Balance}. +/// +/// The value is formatted as `123,456.789`. +/// +/// @category Wallet SDK +/// #[wasm_bindgen] pub struct BalanceStrings { inner: native::BalanceStrings, @@ -39,13 +63,13 @@ pub struct BalanceStrings { #[wasm_bindgen] impl BalanceStrings { #[wasm_bindgen(getter)] - pub fn mature(&self) -> JsValue { - self.inner.mature.clone().into() + pub fn mature(&self) -> String { + self.inner.mature.clone() } #[wasm_bindgen(getter)] - pub fn pending(&self) -> JsValue { - self.inner.pending.clone().into() + pub fn pending(&self) -> Option { + self.inner.pending.clone() } } diff --git a/wallet/core/src/wasm/cryptobox.rs b/wallet/core/src/wasm/cryptobox.rs new file mode 100644 index 000000000..1892b08d2 --- /dev/null +++ b/wallet/core/src/wasm/cryptobox.rs @@ -0,0 +1,138 @@ +use crate::cryptobox::CryptoBox as NativeCryptoBox; +use crate::imports::*; +use base64::{engine::general_purpose, Engine as _}; +use crypto_box::{PublicKey, SecretKey, KEY_SIZE}; +use kaspa_wasm_core::types::BinaryT; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "CryptoBoxPrivateKey | HexString | Uint8Array")] + pub type CryptoBoxPrivateKeyT; + + #[wasm_bindgen(typescript_type = "CryptoBoxPublicKey | HexString | Uint8Array")] + pub type CryptoBoxPublicKeyT; +} + +/// @category Wallet SDK +#[derive(Clone, CastFromJs)] +#[wasm_bindgen] +pub struct CryptoBoxPrivateKey { + secret_key: SecretKey, +} + +#[wasm_bindgen] +impl CryptoBoxPrivateKey { + #[wasm_bindgen(constructor)] + #[allow(non_snake_case)] + pub fn ctor(secretKey: BinaryT) -> Result { + CryptoBoxPrivateKey::try_owned_from(secretKey) + } + + pub fn to_public_key(&self) -> CryptoBoxPublicKey { + CryptoBoxPublicKey { public_key: self.secret_key.public_key() } + } +} + +impl TryCastFromJs for CryptoBoxPrivateKey { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result> { + Self::resolve(&value, || { + let secret_key = value.as_ref().try_as_vec_u8()?; + if secret_key.len() != KEY_SIZE { + return Err(Error::InvalidPrivateKeyLength); + } + Ok(Self { secret_key: SecretKey::from_slice(&secret_key)? }) + }) + } +} + +impl std::ops::Deref for CryptoBoxPrivateKey { + type Target = SecretKey; + + fn deref(&self) -> &Self::Target { + &self.secret_key + } +} + +/// @category Wallet SDK +#[derive(Clone, CastFromJs)] +#[wasm_bindgen] +pub struct CryptoBoxPublicKey { + public_key: PublicKey, +} + +impl TryCastFromJs for CryptoBoxPublicKey { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result> { + Self::resolve(&value, || { + let public_key = value.as_ref().try_as_vec_u8()?; + if public_key.len() != KEY_SIZE { + Err(Error::InvalidPublicKeyLength) + } else { + Ok(Self { public_key: PublicKey::from_slice(&public_key)? }) + } + }) + } +} + +#[wasm_bindgen] +impl CryptoBoxPublicKey { + #[wasm_bindgen(constructor)] + #[allow(non_snake_case)] + pub fn ctor(publicKey: BinaryT) -> Result { + Self::try_owned_from(publicKey) + } + + #[wasm_bindgen(js_name = "toString")] + pub fn to_string_impl(&self) -> String { + self.public_key.as_bytes().as_slice().to_hex() + } +} + +impl std::ops::Deref for CryptoBoxPublicKey { + type Target = PublicKey; + + fn deref(&self) -> &Self::Target { + &self.public_key + } +} + +/// +/// CryptoBox allows for encrypting and decrypting messages using the `crypto_box` crate. +/// +/// https://docs.rs/crypto_box/0.9.1/crypto_box/ +/// +/// @category Wallet SDK +/// +#[derive(Clone, CastFromJs)] +#[wasm_bindgen(inspectable)] +pub struct CryptoBox { + inner: Arc, +} + +#[wasm_bindgen] +impl CryptoBox { + #[wasm_bindgen(constructor)] + #[allow(non_snake_case)] + pub fn ctor(secretKey: CryptoBoxPrivateKeyT, peerPublicKey: CryptoBoxPublicKeyT) -> Result { + let secret_key = CryptoBoxPrivateKey::try_cast_from(secretKey)?; + let peer_public_key = CryptoBoxPublicKey::try_cast_from(peerPublicKey)?; + Ok(Self { inner: Arc::new(NativeCryptoBox::new(&secret_key, &peer_public_key)) }) + } + + #[wasm_bindgen(getter, js_name = "publicKey")] + pub fn js_public_key(&self) -> String { + self.inner.public_key().as_bytes().as_slice().to_hex() + } + + pub fn encrypt(&self, plaintext: String) -> Result { + let encrypted = self.inner.encrypt(plaintext.as_bytes())?; + Ok(general_purpose::STANDARD.encode(encrypted)) + } + + pub fn decrypt(&self, base64string: String) -> Result { + let bytes = general_purpose::STANDARD.decode(base64string)?; + let decrypted = self.inner.decrypt(&bytes)?; + Ok(String::from_utf8(decrypted)?) + } +} diff --git a/wallet/core/src/wasm/encryption.rs b/wallet/core/src/wasm/encryption.rs new file mode 100644 index 000000000..6161a0285 --- /dev/null +++ b/wallet/core/src/wasm/encryption.rs @@ -0,0 +1,89 @@ +#![allow(non_snake_case)] + +use crate::encryption::*; +use crate::imports::*; +use base64::{engine::general_purpose, Engine as _}; +use kaspa_wasm_core::types::BinaryT; +use kaspa_wasm_core::types::HexString; + +/// WASM32 binding for `encryptXChaCha20Poly1305` function. +/// @returns The encrypted text as a base64 string. +/// @category Encryption +#[wasm_bindgen(js_name = "encryptXChaCha20Poly1305")] +pub fn js_encrypt_xchacha20poly1305(plainText: String, password: String) -> Result { + let secret = sha256_hash(password.as_bytes()); + let encrypted = encrypt_xchacha20poly1305(plainText.as_bytes(), &secret)?; + Ok(general_purpose::STANDARD.encode(encrypted)) +} + +/// WASM32 binding for `decryptXChaCha20Poly1305` function. +/// @category Encryption +#[wasm_bindgen(js_name = "decryptXChaCha20Poly1305")] +pub fn js_decrypt_xchacha20poly1305(base64string: String, password: String) -> Result { + let secret = sha256_hash(password.as_bytes()); + let bytes = general_purpose::STANDARD.decode(base64string)?; + let decrypted = decrypt_xchacha20poly1305(bytes.as_ref(), &secret)?; + Ok(String::from_utf8(decrypted.as_ref().to_vec())?) +} + +/// WASM32 binding for `SHA256` hash function. +/// @param data - The data to hash ({@link HexString} or Uint8Array). +/// @category Encryption +#[wasm_bindgen(js_name = "sha256FromBinary")] +pub fn js_sha256_hash_from_binary(data: BinaryT) -> Result { + let data = data.try_as_vec_u8()?; + let hash = sha256_hash(&data); + Ok(hash.as_ref().to_hex().into()) +} + +/// WASM32 binding for `SHA256` hash function. +/// @param {string} text - The text string to hash. +/// @category Encryption +#[wasm_bindgen(js_name = "sha256FromText")] +pub fn js_sha256_hash_from_text(text: String) -> Result { + let data = text.as_bytes(); + let hash = sha256_hash(data); + Ok(hash.as_ref().to_hex().into()) +} + +/// WASM32 binding for `SHA256d` hash function. +/// @param data - The data to hash ({@link HexString} or Uint8Array). +/// @category Encryption +#[wasm_bindgen(js_name = "sha256dFromBinary")] +pub fn js_sha256d_hash_from_binary(data: BinaryT) -> Result { + let data = data.try_as_vec_u8()?; + let hash = sha256d_hash(&data); + Ok(hash.as_ref().to_hex().into()) +} + +/// WASM32 binding for `SHA256d` hash function. +/// @param {string} text - The text string to hash. +/// @category Encryption +#[wasm_bindgen(js_name = "sha256dFromText")] +pub fn js_sha256d_hash_from_text(text: String) -> Result { + let data = text.as_bytes(); + let hash = sha256d_hash(data); + Ok(hash.as_ref().to_hex().into()) +} + +/// WASM32 binding for `argon2sha256iv` hash function. +/// @param data - The data to hash ({@link HexString} or Uint8Array). +/// @category Encryption +#[wasm_bindgen(js_name = "argon2sha256ivFromBinary")] +#[allow(non_snake_case)] +pub fn js_argon2_sha256iv_phash_from_binary(data: BinaryT, hashLength: usize) -> Result { + let data = data.try_as_vec_u8()?; + let hash = argon2_sha256iv_hash(&data, hashLength)?; + Ok(hash.as_ref().to_hex().into()) +} + +/// WASM32 binding for `argon2sha256iv` hash function. +/// @param text - The text string to hash. +/// @category Encryption +#[wasm_bindgen(js_name = "argon2sha256ivFromText")] +#[allow(non_snake_case)] +pub fn js_argon2_sha256iv_phash_from_text(text: String, byteLength: usize) -> Result { + let data = text.as_bytes(); + let hash = argon2_sha256iv_hash(data, byteLength)?; + Ok(hash.as_ref().to_hex().into()) +} diff --git a/wallet/core/src/wasm/events.rs b/wallet/core/src/wasm/events.rs new file mode 100644 index 000000000..a43abea5c --- /dev/null +++ b/wallet/core/src/wasm/events.rs @@ -0,0 +1,19 @@ +// use js_sys::Function; + +// #[derive(Clone, Eq, PartialEq)] +// pub struct Sink(pub Function); +// unsafe impl Send for Sink {} +// impl From for Function { +// fn from(f: Sink) -> Self { +// f.0 +// } +// } +// impl From for Sink { +// fn from(f: Function) -> Self { +// Self(f) +// } +// } + +// // pub struct Callbacks { +// // map: AHashMap>, +// // } diff --git a/wallet/core/src/wasm/message.rs b/wallet/core/src/wasm/message.rs index bf232f025..7df6f1672 100644 --- a/wallet/core/src/wasm/message.rs +++ b/wallet/core/src/wasm/message.rs @@ -1,36 +1,72 @@ use crate::imports::*; use crate::message::*; -use kaspa_consensus_wasm::{PrivateKey, PublicKey}; +use kaspa_wallet_keys::privatekey::PrivateKey; +use kaspa_wallet_keys::publickey::PublicKey; +use kaspa_wasm_core::types::HexString; + +#[wasm_bindgen(typescript_custom_section)] +const TS_MESSAGE_TYPES: &'static str = r#" +/** + * Interface declaration for {@link signMessage} function arguments. + * + * @category Message Signing + */ +export interface ISignMessage { + message: string; + privateKey: PrivateKey | string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Object, typescript_type = "ISignMessage")] + pub type ISignMessage; +} /// Signs a message with the given private key -/// @param {object} value - an object containing { message: String, privateKey: String|PrivateKey } -/// @returns {String} the signature, in hex string format -#[wasm_bindgen(js_name = signMessage, skip_jsdoc)] -pub fn js_sign_message(value: JsValue) -> Result { +/// @category Message Signing +#[wasm_bindgen(js_name = signMessage)] +pub fn js_sign_message(value: ISignMessage) -> Result { if let Some(object) = Object::try_from(&value) { - let private_key = object.get::("privateKey")?; + let private_key = object.get_cast::("privateKey")?; let raw_msg = object.get_string("message")?; let mut privkey_bytes = [0u8; 32]; - - privkey_bytes.copy_from_slice(&private_key.secret_bytes()); - + privkey_bytes.copy_from_slice(&private_key.as_ref().secret_bytes()); let pm = PersonalMessage(&raw_msg); - let sig_vec = sign_message(&pm, &privkey_bytes)?; - - Ok(faster_hex::hex_string(sig_vec.as_slice())) + privkey_bytes.zeroize(); + Ok(faster_hex::hex_string(sig_vec.as_slice()).into()) } else { Err(Error::custom("Failed to parse input")) } } +#[wasm_bindgen(typescript_custom_section)] +const TS_MESSAGE_TYPES: &'static str = r#" +/** + * Interface declaration for {@link verifyMessage} function arguments. + * + * @category Message Signing + */ +export interface IVerifyMessage { + message: string; + signature: HexString; + publicKey: PublicKey | string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Object, typescript_type = "IVerifyMessage")] + pub type IVerifyMessage; +} + /// Verifies with a public key the signature of the given message -/// @param {object} value - an object containing { message: String, signature: String, publicKey: String|PublicKey } -/// @returns {bool} true if the signature can be verified with the given public key and message, false otherwise +/// @category Message Signing #[wasm_bindgen(js_name = verifyMessage, skip_jsdoc)] -pub fn js_verify_message(value: JsValue) -> Result { +pub fn js_verify_message(value: IVerifyMessage) -> Result { if let Some(object) = Object::try_from(&value) { - let public_key = object.get::("publicKey")?; + let public_key = object.get_cast::("publicKey")?; let raw_msg = object.get_string("message")?; let signature = object.get_string("signature")?; @@ -38,7 +74,7 @@ pub fn js_verify_message(value: JsValue) -> Result { let mut signature_bytes = [0u8; 64]; faster_hex::hex_decode(signature.as_bytes(), &mut signature_bytes)?; - Ok(verify_message(&pm, &signature_bytes.to_vec(), &public_key.into()).is_ok()) + Ok(verify_message(&pm, &signature_bytes.to_vec(), &public_key.as_ref().xonly_public_key).is_ok()) } else { Err(Error::custom("Failed to parse input")) } diff --git a/wallet/core/src/wasm/mod.rs b/wallet/core/src/wasm/mod.rs index cdd359824..074f67962 100644 --- a/wallet/core/src/wasm/mod.rs +++ b/wallet/core/src/wasm/mod.rs @@ -2,20 +2,36 @@ //! WASM32 bindings for the wallet framework components. //! -pub mod balance; -pub mod message; -pub mod tx; -pub mod utils; -pub mod utxo; -pub mod wallet; -pub mod xprivatekey; -pub mod xpublickey; +use cfg_if::cfg_if; -pub use balance::*; -pub use message::*; -pub use tx::*; -pub use utils::*; -pub use utxo::*; -pub use wallet::*; -pub use xprivatekey::*; -pub use xpublickey::*; +cfg_if! { + if #[cfg(any(feature = "wasm32-sdk", feature = "wasm32-core"))] { + pub mod balance; + pub mod message; + pub mod notify; + pub mod signer; + pub mod tx; + pub mod utils; + pub mod utxo; + pub mod encryption; + pub mod cryptobox; + + pub use self::balance::*; + pub use self::message::*; + pub use self::notify::*; + pub use self::signer::*; + pub use self::tx::*; + pub use self::utils::*; + pub use self::utxo::*; + pub use self::encryption::*; + pub use self::cryptobox::*; + } +} + +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + pub mod api; + pub mod wallet; + pub use self::wallet::*; + } +} diff --git a/wallet/core/src/wasm/notify.rs b/wallet/core/src/wasm/notify.rs new file mode 100644 index 000000000..61d1d1687 --- /dev/null +++ b/wallet/core/src/wasm/notify.rs @@ -0,0 +1,733 @@ +#![allow(non_snake_case)] +use cfg_if::cfg_if; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; +use wasm_bindgen::prelude::*; + +cfg_if! { + if #[cfg(any(feature = "wasm32-core", feature = "wasm32-sdk"))] { + + #[wasm_bindgen(typescript_custom_section)] + const TS_NOTIFY: &'static str = r#" + + /** + * Events emitted by the {@link UtxoProcessor}. + * @category Wallet SDK + */ + export enum UtxoProcessorEventType { + Connect = "connect", + Disconnect = "disconnect", + UtxoIndexNotEnabled = "utxo-index-not-enabled", + SyncState = "sync-state", + UtxoProcStart = "utxo-proc-start", + UtxoProcStop = "utxo-proc-stop", + UtxoProcError = "utxo-proc-error", + DaaScoreChange = "daa-score-change", + Pending = "pending", + Reorg = "reorg", + Stasis = "stasis", + Maturity = "maturity", + Discovery = "discovery", + Balance = "balance", + Error = "error", + } + + /** + * {@link UtxoProcessor} notification event data. + * @category Wallet SDK + */ + export type UtxoProcessorEventData = IConnectEvent + | IDisconnectEvent + | IUtxoIndexNotEnabledEvent + | ISyncStateEvent + | IServerStatusEvent + | IUtxoProcErrorEvent + | IDaaScoreChangeEvent + | IPendingEvent + | IReorgEvent + | IStasisEvent + | IMaturityEvent + | IDiscoveryEvent + | IBalanceEvent + | IErrorEvent + | undefined + ; + + /** + * UtxoProcessor notification event data map. + * + * @category Wallet API + */ + export type UtxoProcessorEventMap = { + "connect":IConnectEvent, + "disconnect": IDisconnectEvent, + "utxo-index-not-enabled": IUtxoIndexNotEnabledEvent, + "sync-state": ISyncStateEvent, + "server-status": IServerStatusEvent, + "utxo-proc-start": undefined, + "utxo-proc-stop": undefined, + "utxo-proc-error": IUtxoProcErrorEvent, + "daa-score-change": IDaaScoreChangeEvent, + "pending": IPendingEvent, + "reorg": IReorgEvent, + "stasis": IStasisEvent, + "maturity": IMaturityEvent, + "discovery": IDiscoveryEvent, + "balance": IBalanceEvent, + "error": IErrorEvent + } + + /** + * + * @category Wallet API + */ + export type IUtxoProcessorEvent = { + [K in keyof UtxoProcessorEventMap]: { event: K, data: UtxoProcessorEventMap[K] } + }[keyof UtxoProcessorEventMap]; + + + /** + * {@link UtxoProcessor} notification callback type. + * + * This type declares the callback function that is called when notification is emitted + * from the UtxoProcessor or UtxoContext subsystems. + * + * @see {@link UtxoProcessor}, {@link UtxoContext}, + * + * @category Wallet SDK + */ + export type UtxoProcessorNotificationCallback = (event: IUtxoProcessorEvent) => void; + "#; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = "UtxoProcessorEventType | UtxoProcessorEventType[] | string | string[]")] + pub type UtxoProcessorEventTarget; + #[wasm_bindgen(extends = js_sys::Function, typescript_type = "UtxoProcessorNotificationCallback")] + pub type UtxoProcessorNotificationCallback; + #[wasm_bindgen(extends = js_sys::Function, typescript_type = "string | UtxoProcessorNotificationCallback")] + pub type UtxoProcessorNotificationTypeOrCallback; + } + } +} + +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + #[wasm_bindgen(typescript_custom_section)] + const TS_NOTIFY: &'static str = r#" + + /** + * Events emitted by the {@link Wallet}. + * @category Wallet API + */ + export enum WalletEventType { + Connect = "connect", + Disconnect = "disconnect", + UtxoIndexNotEnabled = "utxo-index-not-enabled", + SyncState = "sync-state", + WalletHint = "wallet-hint", + WalletOpen = "wallet-open", + WalletCreate = "wallet-create", + WalletReload = "wallet-reload", + WalletError = "wallet-error", + WalletClose = "wallet-close", + PrvKeyDataCreate = "prv-key-data-create", + AccountActivation = "account-activation", + AccountDeactivation = "account-deactivation", + AccountSelection = "account-selection", + AccountCreate = "account-create", + AccountUpdate = "account-update", + ServerStatus = "server-status", + UtxoProcStart = "utxo-proc-start", + UtxoProcStop = "utxo-proc-stop", + UtxoProcError = "utxo-proc-error", + DaaScoreChange = "daa-score-change", + Pending = "pending", + Reorg = "reorg", + Stasis = "stasis", + Maturity = "maturity", + Discovery = "discovery", + Balance = "balance", + Error = "error", + } + + + /** + * {@link Wallet} notification event data payload. + * @category Wallet API + */ + export type WalletEventData = IConnectEvent + | IDisconnectEvent + | IUtxoIndexNotEnabledEvent + | ISyncStateEvent + | IWalletHintEvent + | IWalletOpenEvent + | IWalletCreateEvent + | IWalletReloadEvent + | IWalletErrorEvent + // | IWalletCloseEvent + | IPrvKeyDataCreateEvent + | IAccountActivationEvent + | IAccountDeactivationEvent + | IAccountSelectionEvent + | IAccountCreateEvent + | IAccountUpdateEvent + | IServerStatusEvent + // | IUtxoProcStartEvent + // | IUtxoProcStopEvent + | IUtxoProcErrorEvent + | IDaaScoreChangeEvent + | IPendingEvent + | IReorgEvent + | IStasisEvent + | IMaturityEvent + | IDiscoveryEvent + | IBalanceEvent + | IErrorEvent + | undefined + ; + + /** + * Wallet notification event data map. + * @see {@link Wallet.addEventListener} + * @category Wallet API + */ + export type WalletEventMap = { + "connect": IConnectEvent, + "disconnect": IDisconnectEvent, + "utxo-index-not-enabled": IUtxoIndexNotEnabledEvent, + "sync-state": ISyncStateEvent, + "wallet-hint": IWalletHintEvent, + "wallet-open": IWalletOpenEvent, + "wallet-create": IWalletCreateEvent, + "wallet-reload": IWalletReloadEvent, + "wallet-error": IWalletErrorEvent, + "wallet-close": undefined, + "prv-key-data-create": IPrvKeyDataCreateEvent, + "account-activation": IAccountActivationEvent, + "account-deactivation": IAccountDeactivationEvent, + "account-selection": IAccountSelectionEvent, + "account-create": IAccountCreateEvent, + "account-update": IAccountUpdateEvent, + "server-status": IServerStatusEvent, + "utxo-proc-start": undefined, + "utxo-proc-stop": undefined, + "utxo-proc-error": IUtxoProcErrorEvent, + "daa-score-change": IDaaScoreChangeEvent, + "pending": IPendingEvent, + "reorg": IReorgEvent, + "stasis": IStasisEvent, + "maturity": IMaturityEvent, + "discovery": IDiscoveryEvent, + "balance": IBalanceEvent, + "error": IErrorEvent, + } + + /** + * {@link Wallet} notification event interface. + * @category Wallet API + */ + export type IWalletEvent = { + [K in keyof WalletEventMap]: { type: K, data: WalletEventMap[K] } + }[keyof WalletEventMap]; + + /** + * Wallet notification callback type. + * + * This type declares the callback function that is called when notification is emitted + * from the Wallet (and the underlying UtxoProcessor or UtxoContext subsystems). + * + * @see {@link Wallet} + * + * @category Wallet API + */ + export type WalletNotificationCallback = (event: IWalletEvent) => void; + "#; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = "WalletEventType | WalletEventType[] | string | string[]")] + pub type WalletEventTarget; + #[wasm_bindgen(extends = js_sys::Function, typescript_type = "WalletNotificationCallback")] + pub type WalletNotificationCallback; + #[wasm_bindgen(extends = js_sys::Function, typescript_type = "string | WalletNotificationCallback")] + pub type WalletNotificationTypeOrCallback; + } + } +} + +declare! { + IConnectEvent, + r#" + /** + * Emitted by {@link UtxoProcessor} when it negotiates a successful RPC connection. + * + * @category Wallet Events + */ + export interface IConnectEvent { + networkId : string; + url? : string; + } + "#, +} + +declare! { + IDisconnectEvent, + r#" + /** + * Emitted by {@link UtxoProcessor} when it disconnects from RPC. + * + * @category Wallet Events + */ + export interface IDisconnectEvent { + networkId : string; + url? : string; + } + "#, +} + +declare! { + IUtxoIndexNotEnabledEvent, + r#" + /** + * Emitted by {@link UtxoProcessor} when it detects that connected node does not have UTXO index enabled. + * + * @category Wallet Events + */ + export interface IUtxoIndexNotEnabledEvent { + url? : string; + } + "#, +} + +declare! { + ISyncStateEvent, + r#" + + /** + * + * @category Wallet Events + */ + export interface ISyncState { + event : string; + data? : ISyncProofEvent | ISyncHeadersEvent | ISyncBlocksEvent | ISyncUtxoSyncEvent | ISyncTrustSyncEvent; + } + + /** + * + * @category Wallet Events + */ + export interface ISyncStateEvent { + syncState : ISyncState; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IWalletHintEvent, + r#" + /** + * Emitted by {@link Wallet} when it opens and contains an optional anti-phishing 'hint' set by the user. + * + * @category Wallet Events + */ + export interface IWalletHintEvent { + hint? : string; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IWalletOpenEvent, + r#" + /** + * Emitted by {@link Wallet} when the wallet is successfully opened. + * + * @category Wallet Events + */ + export interface IWalletOpenEvent { + walletDescriptor : IWalletDescriptor; + accountDescriptors : IAccountDescriptor[]; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IWalletCreateEvent, + r#" + /** + * Emitted by {@link Wallet} when the wallet data storage has been successfully created. + * + * @category Wallet Events + */ + export interface IWalletCreateEvent { + walletDescriptor : IWalletDescriptor; + storageDescriptor : IStorageDescriptor; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IWalletReloadEvent, + r#" + /** + * Emitted by {@link Wallet} when the wallet is successfully reloaded. + * + * @category Wallet Events + */ + export interface IWalletReloadEvent { + walletDescriptor : IWalletDescriptor; + accountDescriptors : IAccountDescriptor[]; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IWalletErrorEvent, + r#" + /** + * Emitted by {@link Wallet} when an error occurs (for example, the wallet has failed to open). + * + * @category Wallet Events + */ + export interface IWalletErrorEvent { + message : string; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IPrvKeyDataCreateEvent, + r#" + /** + * Emitted by {@link Wallet} when the wallet has created a private key. + * + * @category Wallet Events + */ + export interface IPrvKeyDataCreateEvent { + prvKeyDataInfo : IPrvKeyDataInfo; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IAccountActivationEvent, + r#" + /** + * Emitted by {@link Wallet} when an account has been activated. + * + * @category Wallet Events + */ + export interface IAccountActivationEvent { + ids : HexString[]; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IAccountDeactivationEvent, + r#" + /** + * Emitted by {@link Wallet} when an account has been deactivated. + * + * @category Wallet Events + */ + export interface IAccountDeactivationEvent { + ids : HexString[]; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IAccountSelectionEvent, + r#" + /** + * Emitted by {@link Wallet} when an account has been selected. + * This event is used internally in Rust SDK to track currently + * selected account in the Rust CLI wallet. + * + * @category Wallet Events + */ + export interface IAccountSelectionEvent { + id? : HexString; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IAccountCreateEvent, + r#" + /** + * Emitted by {@link Wallet} when an account has been created. + * + * @category Wallet Events + */ + export interface IAccountCreateEvent { + accountDescriptor : IAccountDescriptor; + } + "#, +} + +#[cfg(feature = "wasm32-sdk")] +declare! { + IAccountUpdateEvent, + r#" + /** + * Emitted by {@link Wallet} when an account data has been updated. + * This event signifies a chance in the internal account state that + * includes new address generation. + * + * @category Wallet Events + */ + export interface IAccountUpdateEvent { + accountDescriptor : IAccountDescriptor; + } + "#, +} + +declare! { + IServerStatusEvent, + r#" + /** + * Emitted by {@link UtxoProcessor} after successfully opening an RPC + * connection to the Kaspa node. This event contains general information + * about the Kaspa node. + * + * @category Wallet Events + */ + export interface IServerStatusEvent { + networkId : string; + serverVersion : string; + isSynced : boolean; + url? : string; + } + "#, +} + +declare! { + IUtxoProcErrorEvent, + r#" + /** + * Emitted by {@link UtxoProcessor} indicating a non-recoverable internal error. + * If such event is emitted, the application should stop the UtxoProcessor + * and restart all related subsystem. This event is emitted when the UtxoProcessor + * encounters a critical condition such as "out of memory". + * + * @category Wallet Events + */ + export interface IUtxoProcErrorEvent { + message : string; + } + "#, +} + +declare! { + IDaaScoreChangeEvent, + r#" + /** + * Emitted by {@link UtxoProcessor} on DAA score change. + * + * @category Wallet Events + */ + export interface IDaaScoreChangeEvent { + currentDaaScore : number; + } + "#, +} + +declare! { + IPendingEvent, + r#" + /** + * Emitted by {@link UtxoContext} when detecting a pending transaction. + * This notification will be followed by the "balance" event. + * + * @category Wallet Events + */ + export type IPendingEvent = TransactionRecord; + "#, +} + +declare! { + IReorgEvent, + r#" + /** + * Emitted by {@link UtxoContext} when detecting a reorg transaction condition. + * A transaction is considered reorg if it has been removed from the UTXO set + * as a part of the network reorg process. Transactions notified with this event + * should be considered as invalid and should be removed from the application state. + * Associated UTXOs will be automatically removed from the UtxoContext state. + * + * @category Wallet Events + */ + export type IReorgEvent = TransactionRecord; + "#, +} + +declare! { + IStasisEvent, + r#" + /** + * Emitted by {@link UtxoContext} when detecting a new coinbase transaction. + * Transactions are kept in "stasis" for the half of the coinbase maturity DAA period. + * A wallet should ignore these transactions until they are re-broadcasted + * via the "pending" event. + * + * @category Wallet Events + */ + export type IStasisEvent = TransactionRecord; + "#, +} + +declare! { + IMaturityEvent, + r#" + /** + * Emitted by {@link UtxoContext} when transaction is considered to be confirmed. + * This notification will be followed by the "balance" event. + * + * @category Wallet Events + */ + export type IMaturityEvent = TransactionRecord; + "#, +} + +declare! { + IDiscoveryEvent, + r#" + /** + * Emitted by {@link UtxoContext} when detecting a new transaction during + * the initialization phase. Discovery transactions indicate that UTXOs + * have been discovered during the initial UTXO scan. + * + * When receiving such notifications, the application should check its + * internal storage to see if the transaction already exists. If it doesn't, + * it should create a correspond in record and notify the user of a new + * transaction. + * + * This event is emitted when an address has existing UTXO entries that + * may have been received during previous sessions or while the wallet + * was offline. + * + * @category Wallet Events + */ + export type IDiscoveryEvent = TransactionRecord; + "#, +} + +declare! { + IBalanceEvent, + r#" + /** + * Emitted by {@link UtxoContext} when detecting a balance change. + * This notification is produced during the UTXO scan, when UtxoContext + * detects incoming or outgoing transactions or when transactions + * change their state (e.g. from pending to confirmed). + * + * @category Wallet Events + */ + export interface IBalanceEvent { + id : HexString; + balance? : IBalance; + } + "#, +} + +declare! { + IErrorEvent, + r#" + /** + * Emitted when detecting a general error condition. + * + * @category Wallet Events + */ + export interface IErrorEvent { + message : string; + } + "#, +} + +// --- + +declare! { + ISyncProof, + r#" + /** + * Emitted by {@link UtxoProcessor} when node is syncing and processing cryptographic proofs. + * + * @category Wallet Events + */ + export interface ISyncProofEvent { + level : number; + } + "#, +} + +declare! { + ISyncHeaders, + r#" + /** + * Emitted by {@link UtxoProcessor} when node is syncing headers as a part of the IBD (Initial Block Download) process. + * + * @category Wallet Events + */ + export interface ISyncHeadersEvent { + headers : number; + progress : number; + } + "#, +} + +declare! { + ISyncBlocks, + r#" + /** + * Emitted by {@link UtxoProcessor} when node is syncing blocks as a part of the IBD (Initial Block Download) process. + * + * @category Wallet Events + */ + export interface ISyncBlocksEvent { + blocks : number; + progress : number; + } + "#, +} + +declare! { + ISyncUtxoSync, + r#" + /** + * Emitted by {@link UtxoProcessor} when node is syncing the UTXO set as a part of the IBD (Initial Block Download) process. + * + * @category Wallet Events + */ + export interface ISyncUtxoSyncEvent { + chunks : number; + total : number; + } + "#, +} + +declare! { + ISyncTrustSync, + r#" + /** + * Emitted by {@link UtxoProcessor} when node is syncing cryptographic trust data as a part of the IBD (Initial Block Download) process. + * + * @category Wallet Events + */ + export interface ISyncTrustSyncEvent { + processed : number; + total : number; + } + "#, +} diff --git a/wallet/core/src/wasm/signer.rs b/wallet/core/src/wasm/signer.rs new file mode 100644 index 000000000..e2ff8e6fb --- /dev/null +++ b/wallet/core/src/wasm/signer.rs @@ -0,0 +1,81 @@ +use crate::imports::*; +use crate::result::Result; +use js_sys::Array; +use kaspa_consensus_client::{sign_with_multiple_v3, Transaction}; +use kaspa_consensus_core::tx::PopulatedTransaction; +use kaspa_consensus_core::{hashing::sighash_type::SIG_HASH_ALL, sign::verify}; +use kaspa_hashes::Hash; +use kaspa_wallet_keys::privatekey::PrivateKey; +use serde_wasm_bindgen::from_value; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Array, is_type_of = Array::is_array, typescript_type = "(PrivateKey | HexString | Uint8Array)[]")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type PrivateKeyArrayT; +} + +impl TryFrom for Vec { + type Error = crate::error::Error; + fn try_from(keys: PrivateKeyArrayT) -> std::result::Result { + let mut private_keys: Vec = vec![]; + for key in keys.iter() { + private_keys + .push(PrivateKey::try_owned_from(key).map_err(|_| Self::Error::Custom("Unable to cast PrivateKey".to_string()))?); + } + + Ok(private_keys) + } +} + +/// `signTransaction()` is a helper function to sign a transaction using a private key array or a signer array. +/// @category Wallet SDK +#[wasm_bindgen(js_name = "signTransaction")] +pub fn js_sign_transaction(tx: Transaction, signer: PrivateKeyArrayT, verify_sig: bool) -> Result { + if signer.is_array() { + let mut private_keys: Vec<[u8; 32]> = vec![]; + for key in Array::from(&signer).iter() { + let key = PrivateKey::try_cast_from(key).map_err(|_| Error::Custom("Unable to cast PrivateKey".to_string()))?; + private_keys.push(key.as_ref().secret_bytes()); + } + + let tx = sign_transaction(tx, &private_keys, verify_sig).map_err(|err| Error::Custom(format!("Unable to sign: {err:?}")))?; + private_keys.zeroize(); + Ok(tx) + } else { + Err(Error::custom("signTransaction() requires an array of signatures")) + } +} + +pub fn sign_transaction(tx: Transaction, private_keys: &[[u8; 32]], verify_sig: bool) -> Result { + let tx = sign(tx, private_keys)?; + if verify_sig { + let (cctx, utxos) = tx.tx_and_utxos(); + let populated_transaction = PopulatedTransaction::new(&cctx, utxos); + verify(&populated_transaction)?; + } + Ok(tx) +} + +/// 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(tx: Transaction, privkeys: &[[u8; 32]]) -> Result { + Ok(sign_with_multiple_v3(tx, privkeys)?.unwrap()) +} + +/// @category Wallet SDK +#[wasm_bindgen(js_name=signScriptHash)] +pub fn sign_script_hash(script_hash: JsValue, privkey: &PrivateKey) -> Result { + let script_hash = from_value(script_hash)?; + let result = sign_hash(script_hash, &privkey.into())?; + Ok(result.to_hex()) +} + +pub fn sign_hash(sig_hash: Hash, privkey: &[u8; 32]) -> Result> { + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice())?; + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; + let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref(); + let signature = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect(); + Ok(signature) +} diff --git a/wallet/core/src/wasm/tx/consensus.rs b/wallet/core/src/wasm/tx/consensus.rs index 62e285279..f1400d091 100644 --- a/wallet/core/src/wasm/tx/consensus.rs +++ b/wallet/core/src/wasm/tx/consensus.rs @@ -3,6 +3,7 @@ use kaspa_addresses::Address; use kaspa_consensus_core::{config::params::Params, network::NetworkType}; use wasm_bindgen::prelude::*; +/// @category Wallet SDK #[wasm_bindgen] pub struct ConsensusParams { params: Params, @@ -21,12 +22,14 @@ impl From for Params { } /// find Consensus parameters for given Address +/// @category Wallet SDK #[wasm_bindgen(js_name = getConsensusParametersByAddress)] pub fn get_consensus_params_by_address(address: &Address) -> ConsensusParams { core::get_consensus_params_by_address(address).into() } /// find Consensus parameters for given NetworkType +/// @category Wallet SDK #[wasm_bindgen(js_name = getConsensusParametersByNetwork)] pub fn get_consensus_params_by_network(network: NetworkType) -> ConsensusParams { core::get_consensus_params_by_network(&network).into() diff --git a/wallet/core/src/wasm/tx/fees.rs b/wallet/core/src/wasm/tx/fees.rs new file mode 100644 index 000000000..ea38c6ebf --- /dev/null +++ b/wallet/core/src/wasm/tx/fees.rs @@ -0,0 +1,54 @@ +use crate::imports::*; +use crate::tx::fees::Fees; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; +use workflow_wasm::convert::CastFromJs; + +/// +/// @see {@link IFees}, {@link IGeneratorSettingsObject}, {@link Generator}, {@link estimateTransactions}, {@link createTransactions} +/// @category Wallet SDK +/// +#[wasm_bindgen] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, CastFromJs)] +pub enum FeeSource { + SenderPays, + ReceiverPays, +} + +declare! { + IFees, + "IFees | bigint", + r#" + /** + * + * @category Wallet SDK + */ + export interface IFees { + amount: bigint; + source?: FeeSource; + } + "#, +} + +impl TryFrom for Fees { + type Error = Error; + fn try_from(args: IFees) -> Result { + if args.is_undefined() || args.is_null() { + Ok(Fees::None) + } else if let Ok(fee) = args.try_as_u64() { + Ok(Fees::SenderPays(fee)) + } else if let Ok(object) = args.dyn_into::() { + let amount = object.get_u64("amount")?; + if let Some(source) = object.try_get_value("source")? { + let source = FeeSource::try_cast_from(&source)?; + match source { + FeeSource::SenderPays => Ok(Fees::SenderPays(amount)), + FeeSource::ReceiverPays => Ok(Fees::ReceiverPays(amount)), + } + } else { + Ok(Fees::SenderPays(amount)) + } + } else { + Err(crate::error::Error::custom("Invalid fee")) + } + } +} diff --git a/wallet/core/src/wasm/tx/generator/generator.rs b/wallet/core/src/wasm/tx/generator/generator.rs index a45d54bc6..5303b4d3e 100644 --- a/wallet/core/src/wasm/tx/generator/generator.rs +++ b/wallet/core/src/wasm/tx/generator/generator.rs @@ -3,39 +3,96 @@ use crate::result::Result; use crate::tx::{generator as native, Fees, PaymentDestination, PaymentOutputs}; use crate::utxo::{TryIntoUtxoEntryReferences, UtxoEntryReference}; use crate::wasm::tx::generator::*; -use crate::wasm::wallet::Account; +use crate::wasm::tx::IFees; +// use crate::wasm::wallet::Account; use crate::wasm::UtxoContext; +// TODO-WASM fix outputs #[wasm_bindgen(typescript_custom_section)] -const IGeneratorSettingsObject: &'static str = r#" +const TS_GENERATOR_SETTINGS_OBJECT: &'static str = r#" +/** + * Configuration for the transaction {@link Generator}. This interface + * allows you to specify UTXO sources, transaction outputs, change address, + * priority fee, and other transaction parameters. + * + * If the total number of UTXOs needed to satisfy the transaction outputs + * exceeds maximum allowed number of UTXOs per transaction (limited by + * the maximum transaction mass), the {@link Generator} will produce + * multiple chained transactions to the change address and then used these + * transactions as a source for the "final" transaction. + * + * @see + * {@link kaspaToSompi}, + * {@link Generator}, + * {@link PendingTransaction}, + * {@link UtxoContext}, + * {@link UtxoEntry}, + * {@link createTransactions}, + * {@link estimateTransactions} + * @category Wallet SDK + */ interface IGeneratorSettingsObject { - outputs: PaymentOutputs | Array>; + /** + * Final transaction outputs (do not supply change transaction). + * + * Typical usage: { address: "kaspa:...", amount: 1000n } + */ + outputs: PaymentOutput | IPaymentOutput[]; + /** + * Address to be used for change, if any. + */ changeAddress: Address | string; - priorityFee: bigint; - utxoEntries: Array; - sigOpCount: Uint8Array; - minimumSignatures: Uint16Array; - payload: Uint8Array | string; + /** + * Priority fee in SOMPI. + * + * If supplying `bigint` value, it will be interpreted as a sender-pays fee. + * Alternatively you can supply an object with `amount` and `source` properties + * where `source` contains the {@link FeeSource} enum. + * + * **IMPORTANT:* When sending an outbound transaction (transaction that + * contains outputs), the `priorityFee` must be set, even if it is zero. + * However, if the transaction is missing outputs (and thus you are + * creating a compound transaction against your change address), + * `priorityFee` should not be set (i.e. it should be `undefined`). + * + * @see {@link IFees}, {@link FeeSource} + */ + priorityFee?: IFees | bigint; + /** + * UTXO entries to be used for the transaction. This can be an + * array of UtxoEntry instances, objects matching {@link IUtxoEntry} + * interface, or a {@link UtxoContext} instance. + */ + entries: IUtxoEntry[] | UtxoEntryReference[] | UtxoContext; + /** + * Optional number of signature operations in the transaction. + */ + sigOpCount?: number; + /** + * Optional minimum number of signatures required for the transaction. + */ + minimumSignatures?: number; + /** + * Optional data payload to be included in the transaction. + */ + payload?: Uint8Array | HexString; + + /** + * Optional NetworkId or network id as string (i.e. `mainnet` or `testnet-11`). Required when {@link IGeneratorSettingsObject.entries} is array + */ + networkId?: NetworkId | string } "#; #[wasm_bindgen] extern "C" { - /// Supports the following properties (all values must be supplied in SOMPI): - /// - `outputs`: instance of [`PaymentOutputs`] or `[ [address, amount], [address, amount], ... ]` - /// - `changeAddress`: [`Address`] or String representation of an address - /// - `priorityFee`: BigInt - /// - `utxoEntries`: Array of [`UtxoEntryReference`] - /// - `sigOpCount`: `u8` - /// - `minimumSignatures`: `u16` - /// - `payload`: [`Uint8Array`] or hex String representation of a payload #[wasm_bindgen(extends = Object, typescript_type = "IGeneratorSettingsObject")] #[derive(Clone, Debug, PartialEq, Eq)] - pub type GeneratorSettingsObject; + pub type IGeneratorSettingsObject; } -/// [`Generator`] is a type capable of generating transactions based on a supplied -/// set of UTXO entries or a UTXO entry producer (such as `UtxoContext`). The [`Generator`] +/// Generator is a type capable of generating transactions based on a supplied +/// set of UTXO entries or a UTXO entry producer (such as {@link UtxoContext}). The Generator /// accumulates UTXO entries until it can generate a transaction that meets the /// requested amount or until the total mass of created inputs exceeds the allowed /// transaction mass, at which point it will produce a compound transaction by forwarding @@ -44,25 +101,38 @@ extern "C" { /// Each compound transaction results in a new UTXO, which is immediately reused in the /// subsequent transaction. /// +/// The Generator constructor accepts a single {@link IGeneratorSettingsObject} object. +/// /// ```javascript /// /// let generator = new Generator({ /// utxoEntries : [...], /// changeAddress : "kaspa:...", -/// outputs : [[1000, "kaspa:..."], [2000, "kaspa:..."], ...], +/// outputs : [ +/// { amount : kaspaToSompi(10.0), address: "kaspa:..."}, +/// { amount : kaspaToSompi(20.0), address: "kaspa:..."}, +/// ... +/// ], /// priorityFee : 1000n, /// }); /// -/// while(transaction = await generator.next()) { -/// await transaction.sign(privateKeys); -/// await transaction.submit(rpc); +/// let pendingTransaction; +/// while(pendingTransaction = await generator.next()) { +/// await pendingTransaction.sign(privateKeys); +/// await pendingTransaction.submit(rpc); /// } /// /// let summary = generator.summary(); /// console.log(summary); /// /// ``` -/// +/// @see +/// {@link IGeneratorSettingsObject}, +/// {@link PendingTransaction}, +/// {@link UtxoContext}, +/// {@link createTransactions}, +/// {@link estimateTransactions}, +/// @category Wallet SDK #[wasm_bindgen] pub struct Generator { inner: Arc, @@ -71,7 +141,7 @@ pub struct Generator { #[wasm_bindgen] impl Generator { #[wasm_bindgen(constructor)] - pub fn ctor(args: GeneratorSettingsObject) -> Result { + pub fn ctor(args: IGeneratorSettingsObject) -> Result { let settings = GeneratorSettings::try_from(args)?; let GeneratorSettings { @@ -120,11 +190,10 @@ impl Generator { payload, multiplexer, )? - } - GeneratorSource::Account(account) => { - let account: Arc = account.into(); - native::GeneratorSettings::try_new_with_account(account, final_transaction_destination, final_priority_fee, None)? - } + } // GeneratorSource::Account(account) => { + // let account: Arc = account.into(); + // native::GeneratorSettings::try_new_with_account(account, final_transaction_destination, final_priority_fee, None)? + // } }; let abortable = Abortable::default(); @@ -167,10 +236,11 @@ impl Generator { enum GeneratorSource { UtxoEntries(Vec), UtxoContext(UtxoContext), - Account(Account), + // #[cfg(any(feature = "wasm32-sdk"), not(target_arch = "wasm32"))] + // Account(Account), } -/// Converts [`GeneratorSettingsObject`] to a series of properties intended for use by the [`Generator`]. +/// Converts [`IGeneratorSettingsObject`] to a series of properties intended for use by the [`Generator`]. struct GeneratorSettings { pub network_id: Option, pub source: GeneratorSource, @@ -183,26 +253,24 @@ struct GeneratorSettings { pub payload: Option>, } -impl TryFrom for GeneratorSettings { +impl TryFrom for GeneratorSettings { type Error = Error; - fn try_from(args: GeneratorSettingsObject) -> std::result::Result { + fn try_from(args: IGeneratorSettingsObject) -> std::result::Result { let network_id = args.try_get::("networkId")?; // lack of outputs results in a sweep transaction compounding utxos into the change address let outputs = args.get_value("outputs")?; let final_transaction_destination: PaymentDestination = - if outputs.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_from(outputs)?.into() }; + if outputs.is_undefined() { PaymentDestination::Change } else { PaymentOutputs::try_owned_from(outputs)?.into() }; - let change_address = args.try_get::
("changeAddress")?; //.ok_or(Error::custom("changeAddress is required"))?; + let change_address = args.try_get_cast::
("changeAddress")?.map(Cast::into_owned); - let final_priority_fee = args.get::("priorityFee")?; + let final_priority_fee = args.get::("priorityFee")?.try_into()?; - let generator_source = if let Some(utxo_entries) = args.try_get_value("entries")? { + let generator_source = if let Ok(Some(context)) = args.try_get_cast::("entries") { + GeneratorSource::UtxoContext(context.into_owned()) + } else if let Some(utxo_entries) = args.try_get_value("entries")? { GeneratorSource::UtxoEntries(utxo_entries.try_into_utxo_entry_references()?) - } else if let Some(context) = args.try_get::("entries")? { - GeneratorSource::UtxoContext(context) - } else if let Some(account) = args.try_get::("account")? { - GeneratorSource::Account(account) } else { return Err(Error::custom("'entries', 'context' or 'account' property is required for Generator")); }; diff --git a/wallet/core/src/wasm/tx/generator/pending.rs b/wallet/core/src/wasm/tx/generator/pending.rs index 8d39b8eb5..dfa84c9bd 100644 --- a/wallet/core/src/wasm/tx/generator/pending.rs +++ b/wallet/core/src/wasm/tx/generator/pending.rs @@ -1,9 +1,13 @@ use crate::imports::*; use crate::result::Result; use crate::tx::generator as native; -use kaspa_consensus_wasm::{PrivateKey, Transaction}; -use kaspa_wrpc_client::wasm::RpcClient; +use crate::wasm::PrivateKeyArrayT; +use kaspa_consensus_client::{numeric, string}; +use kaspa_consensus_client::{ITransaction, Transaction}; +use kaspa_wallet_keys::privatekey::PrivateKey; +use kaspa_wrpc_wasm::RpcClient; +/// @category Wallet SDK #[wasm_bindgen(inspectable)] pub struct PendingTransaction { inner: native::PendingTransaction, @@ -54,29 +58,41 @@ impl PendingTransaction { } } - #[wasm_bindgen(getter)] + /// List of unique addresses used by transaction inputs. + /// This method can be used to determine addresses used by transaction inputs + /// in order to select private keys needed for transaction signing. pub fn addresses(&self) -> Array { self.inner.addresses().iter().map(|address| JsValue::from(address.to_string())).collect() } #[wasm_bindgen(js_name = getUtxoEntries)] pub fn get_utxo_entries(&self) -> Array { - self.inner.utxo_entries().iter().map(|utxo_entry| JsValue::from(utxo_entry.clone())).collect() + self.inner.utxo_entries().values().map(|utxo_entry| JsValue::from(utxo_entry.clone())).collect() } /// Sign transaction with supplied [`Array`] or [`PrivateKey`] or an array of /// raw private key bytes (encoded as `Uint8Array` or as hex strings) - pub fn sign(&self, js_value: JsValue) -> Result<()> { + pub fn sign(&self, js_value: PrivateKeyArrayT) -> Result<()> { if let Ok(keys) = js_value.dyn_into::() { - let keys = - keys.iter().map(PrivateKey::try_from).collect::, kaspa_consensus_wasm::error::Error>>()?; - self.inner.try_sign_with_keys(keys.iter().map(|key| key.into()).collect()) + let keys = keys + .iter() + .map(PrivateKey::try_cast_from) + .collect::, kaspa_wallet_keys::error::Error>>()?; + let mut keys = keys.iter().map(|key| key.as_ref().secret_bytes()).collect::>(); + self.inner.try_sign_with_keys(&keys)?; + keys.zeroize(); + Ok(()) } else { Err(Error::custom("Please supply an array of keys")) } } /// Submit transaction to the supplied [`RpcClient`] + /// **IMPORTANT:** This method will remove UTXOs from the associated + /// {@link UtxoContext} if one was used to create the transaction + /// and will return UTXOs back to {@link UtxoContext} in case of + /// a failed submission. + /// @see {@link RpcClient.submitTransaction} pub async fn submit(&self, wasm_rpc_client: &RpcClient) -> Result { let rpc: Arc = wasm_rpc_client.client().clone(); let txid = self.inner.try_submit(&rpc).await?; @@ -86,7 +102,37 @@ impl PendingTransaction { /// Returns encapsulated network [`Transaction`] #[wasm_bindgen(getter)] pub fn transaction(&self) -> Result { - Ok(Transaction::from(self.inner.transaction())) + Ok(Transaction::from_cctx_transaction(&self.inner.transaction(), self.inner.utxo_entries())) + } + + /// Serializes the transaction to a pure JavaScript Object. + /// The schema of the JavaScript object is defined by {@link ISerializableTransaction}. + /// @see {@link ISerializableTransaction} + /// @see {@link Transaction}, {@link ISerializableTransaction} + #[wasm_bindgen(js_name = "serializeToObject")] + pub fn serialize_to_object(&self) -> Result { + Ok(numeric::SerializableTransaction::from_cctx_transaction(&self.inner.transaction(), self.inner.utxo_entries())? + .serialize_to_object()? + .into()) + } + + /// Serializes the transaction to a JSON string. + /// The schema of the JSON is defined by {@link ISerializableTransaction}. + /// Once serialized, the transaction can be deserialized using {@link Transaction.deserializeFromJSON}. + /// @see {@link Transaction}, {@link ISerializableTransaction} + #[wasm_bindgen(js_name = "serializeToJSON")] + pub fn serialize_to_json(&self) -> Result { + Ok(numeric::SerializableTransaction::from_cctx_transaction(&self.inner.transaction(), self.inner.utxo_entries())? + .serialize_to_json()?) + } + + /// Serializes the transaction to a "Safe" JSON schema where it converts all `bigint` values to `string` to avoid potential client-side precision loss. + /// Once serialized, the transaction can be deserialized using {@link Transaction.deserializeFromSafeJSON}. + /// @see {@link Transaction}, {@link ISerializableTransaction} + #[wasm_bindgen(js_name = "serializeToSafeJSON")] + pub fn serialize_to_json_safe(&self) -> Result { + Ok(string::SerializableTransaction::from_cctx_transaction(&self.inner.transaction(), self.inner.utxo_entries())? + .serialize_to_json()?) } } diff --git a/wallet/core/src/wasm/tx/generator/summary.rs b/wallet/core/src/wasm/tx/generator/summary.rs index 86cb0aff9..8d572ec1e 100644 --- a/wallet/core/src/wasm/tx/generator/summary.rs +++ b/wallet/core/src/wasm/tx/generator/summary.rs @@ -1,6 +1,15 @@ use crate::imports::*; use crate::tx::generator as core; +/// +/// A class containing a summary produced by transaction {@link Generator}. +/// This class contains the number of transactions, the aggregated fees, +/// the aggregated UTXOs and the final transaction amount that includes +/// both network and QoS (priority) fees. +/// +/// @see {@link createTransactions}, {@link IGeneratorSettingsObject}, {@link Generator} +/// @category Wallet SDK +/// #[wasm_bindgen(inspectable)] pub struct GeneratorSummary { inner: core::GeneratorSummary, diff --git a/wallet/core/src/wasm/tx/mass.rs b/wallet/core/src/wasm/tx/mass.rs index f14469bbc..cc522fd8e 100644 --- a/wallet/core/src/wasm/tx/mass.rs +++ b/wallet/core/src/wasm/tx/mass.rs @@ -1,18 +1,18 @@ use crate::imports::NetworkParams; use crate::result::Result; use crate::tx::mass; -// use crate::utxo::NetworkParams; use crate::wasm::tx::*; +use kaspa_consensus_client::*; use kaspa_consensus_core::config::params::Params; use kaspa_consensus_core::tx as cctx; -use kaspa_consensus_wasm::*; use std::sync::Arc; use wasm_bindgen::prelude::*; +use workflow_wasm::convert::*; +/// @category Wallet SDK #[wasm_bindgen] pub struct MassCalculator { mc: Arc, - // params: Arc, } #[wasm_bindgen] @@ -39,7 +39,7 @@ impl MassCalculator { /// /// 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 { + pub fn is_transaction_output_dust(transaction_output: &JsValue) -> Result { let transaction_output = TransactionOutput::try_from(transaction_output)?; let transaction_output = cctx::TransactionOutput::from(&transaction_output); Ok(mass::is_transaction_output_dust(&transaction_output)) @@ -71,9 +71,9 @@ impl MassCalculator { } #[wasm_bindgen(js_name=calcMassForTransaction)] - pub fn calc_mass_for_transaction(&self, tx: JsValue) -> Result { - let tx = Transaction::try_from(tx)?; - let tx = cctx::Transaction::from(&tx); + pub fn calc_mass_for_transaction(&self, tx: &JsValue) -> Result { + let tx = Transaction::try_cast_from(tx)?; + let tx = cctx::Transaction::from(tx.as_ref()); Ok(self.mc.calc_mass_for_transaction(&tx) as u32) } @@ -98,7 +98,7 @@ impl MassCalculator { .dyn_into::()? .iter() .map(TransactionOutput::try_from) - .collect::, kaspa_consensus_wasm::error::Error>>()?; + .collect::, kaspa_consensus_client::error::Error>>()?; let outputs = outputs.iter().map(|output| self.calc_mass_for_output(output)).collect::>>()?; Ok(outputs.iter().sum()) } @@ -108,8 +108,8 @@ impl MassCalculator { let inputs = inputs .dyn_into::()? .iter() - .map(TransactionInput::try_from) - .collect::, kaspa_consensus_wasm::error::Error>>()?; + .map(TransactionInput::try_owned_from) + .collect::, kaspa_consensus_client::error::Error>>()?; let inputs = inputs.iter().map(|input| self.calc_mass_for_input(input)).collect::>>()?; Ok(inputs.iter().sum()) } diff --git a/wallet/core/src/wasm/tx/mod.rs b/wallet/core/src/wasm/tx/mod.rs index 5340539f0..df826a997 100644 --- a/wallet/core/src/wasm/tx/mod.rs +++ b/wallet/core/src/wasm/tx/mod.rs @@ -1,9 +1,11 @@ pub mod consensus; +pub mod fees; pub mod generator; pub mod mass; pub mod utils; -pub use consensus::*; -pub use generator::*; -pub use mass::*; -pub use utils::*; +pub use self::consensus::*; +pub use self::fees::*; +pub use self::generator::*; +pub use self::mass::*; +pub use self::utils::*; diff --git a/wallet/core/src/wasm/tx/utils.rs b/wallet/core/src/wasm/tx/utils.rs index ec5f287d7..0d911af76 100644 --- a/wallet/core/src/wasm/tx/utils.rs +++ b/wallet/core/src/wasm/tx/utils.rs @@ -1,37 +1,40 @@ use crate::imports::*; use crate::result::Result; -use crate::tx::PaymentOutputs; +use crate::tx::{IPaymentOutputArray, PaymentOutputs}; use crate::wasm::tx::consensus::get_consensus_params_by_address; use crate::wasm::tx::generator::*; use crate::wasm::tx::mass::MassCalculator; -use kaspa_addresses::Address; +use kaspa_addresses::{Address, AddressT}; +use kaspa_consensus_client::*; use kaspa_consensus_core::subnets::SUBNETWORK_ID_NATIVE; -use kaspa_consensus_wasm::*; +//use kaspa_consensus_wasm::*; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; use workflow_core::runtime::is_web; /// Create a basic transaction without any mass limit checks. +/// @category Wallet SDK #[wasm_bindgen(js_name=createTransaction)] pub fn create_transaction_js( - utxo_entry_source: JsValue, - outputs: JsValue, - change_address: JsValue, + utxo_entry_source: IUtxoEntryArray, + outputs: IPaymentOutputArray, + change_address: AddressT, priority_fee: BigInt, payload: JsValue, sig_op_count: JsValue, minimum_signatures: JsValue, -) -> crate::result::Result { - let change_address = Address::try_from(change_address)?; - let params = get_consensus_params_by_address(&change_address); +) -> crate::result::Result { + let change_address = Address::try_cast_from(change_address)?; + let params = get_consensus_params_by_address(change_address.as_ref()); let mc = MassCalculator::new(params); let utxo_entries = if let Some(utxo_entries) = utxo_entry_source.dyn_ref::() { - utxo_entries.to_vec().iter().map(UtxoEntryReference::try_from).collect::, _>>()? + utxo_entries.to_vec().iter().map(UtxoEntryReference::try_cast_from).collect::, _>>()? } else { return Err(Error::custom("utxo_entries must be an array")); }; let priority_fee: u64 = priority_fee.try_into().map_err(|err| Error::custom(format!("invalid fee value: {err}")))?; let payload = payload.try_as_vec_u8().ok().unwrap_or_default(); - let outputs: PaymentOutputs = outputs.try_into()?; + let outputs = PaymentOutputs::try_owned_from(outputs)?; let sig_op_count = if !sig_op_count.is_undefined() { sig_op_count.as_f64().expect("sigOpCount should be a number") as u8 } else { 1 }; @@ -47,13 +50,13 @@ pub fn create_transaction_js( let mut entries = vec![]; let inputs = utxo_entries - .iter() + .into_iter() .enumerate() .map(|(sequence, reference)| { - let UtxoEntryReference { utxo } = reference; + let UtxoEntryReference { utxo } = reference.as_ref(); total_input_amount += utxo.amount(); - entries.push(reference.clone()); - TransactionInput::new(utxo.outpoint.clone(), vec![], sequence as u64, sig_op_count) + entries.push(reference.as_ref().clone()); + TransactionInput::new(utxo.outpoint.clone(), vec![], sequence as u64, sig_op_count, Some(reference.into_owned())) }) .collect::>(); @@ -64,16 +67,43 @@ pub fn create_transaction_js( // TODO - Calculate mass and fees let outputs: Vec = outputs.into(); - let transaction = Transaction::new(0, inputs, outputs, 0, SUBNETWORK_ID_NATIVE, 0, payload)?; + let transaction = Transaction::new(None, 0, inputs, outputs, 0, SUBNETWORK_ID_NATIVE, 0, payload)?; let _fee = mc.calc_minimum_transaction_relay_fee(&transaction, minimum_signatures); - let mtx = SignableTransaction::new(transaction, entries.into()); + //let mtx = SignableTransaction::new(transaction, entries.into()); - Ok(mtx) + Ok(transaction) } -/// Creates a set of transactions using transaction [`Generator`]. +declare! { + ICreateTransactions, + r#" + /** + * Interface defining response from the {@link createTransactions} function. + * + * @category Wallet SDK + */ + export interface ICreateTransactions { + /** + * Array of pending unsigned transactions. + */ + transactions : PendingTransaction[]; + /** + * Summary of the transaction generation process. + */ + summary : GeneratorSummary; + } + "#, +} + +#[wasm_bindgen(typescript_custom_section)] +const TS_CREATE_TRANSACTIONS: &'static str = r#" +"#; + +/// Helper function that creates a set of transactions using the transaction {@link Generator}. +/// @see {@link IGeneratorSettingsObject}, {@link Generator}, {@link estimateTransactions} +/// @category Wallet SDK #[wasm_bindgen(js_name=createTransactions)] -pub async fn create_transactions_js(settings: GeneratorSettingsObject) -> Result { +pub async fn create_transactions_js(settings: IGeneratorSettingsObject) -> Result { let generator = Generator::ctor(settings)?; if is_web() { // yield after each generated transaction if operating in the browser @@ -83,27 +113,29 @@ pub async fn create_transactions_js(settings: GeneratorSettingsObject) -> Result transactions.push(PendingTransaction::from(transaction)); yield_executor().await; } - let transactions = transactions.into_iter().map(JsValue::from).collect::(); + let transactions = Array::from_iter(transactions.into_iter().map(JsValue::from)); //.collect::(); let summary = JsValue::from(generator.summary()); - let object = Object::new(); + let object = ICreateTransactions::default(); object.set("transactions", &transactions)?; object.set("summary", &summary)?; Ok(object) } else { - // use iterator to aggregate all transactions let transactions = generator.iter().map(|r| r.map(PendingTransaction::from)).collect::>>()?; - let transactions = transactions.into_iter().map(JsValue::from).collect::(); + let transactions = Array::from_iter(transactions.into_iter().map(JsValue::from)); //.collect::(); let summary = JsValue::from(generator.summary()); - let object = Object::new(); + let object = ICreateTransactions::default(); object.set("transactions", &transactions)?; object.set("summary", &summary)?; Ok(object) } } -/// Creates a set of transactions using transaction [`Generator`]. +/// Helper function that creates an estimate using the transaction {@link Generator} +/// by producing only the {@link GeneratorSummary} containing the estimate. +/// @see {@link IGeneratorSettingsObject}, {@link Generator}, {@link createTransactions} +/// @category Wallet SDK #[wasm_bindgen(js_name=estimateTransactions)] -pub async fn estimate_js(settings: GeneratorSettingsObject) -> Result { +pub async fn estimate_transactions_js(settings: IGeneratorSettingsObject) -> 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/utils.rs b/wallet/core/src/wasm/utils.rs index 520e8946e..a06c6136a 100644 --- a/wallet/core/src/wasm/utils.rs +++ b/wallet/core/src/wasm/utils.rs @@ -1,27 +1,46 @@ use crate::result::Result; +use js_sys::BigInt; +use kaspa_consensus_core::network::{NetworkType, NetworkTypeT}; use wasm_bindgen::prelude::*; use workflow_wasm::prelude::*; -#[wasm_bindgen(js_name = "sompiToKaspa")] -pub fn sompi_to_kaspa(sompi: JsValue) -> Result { - let sompi = sompi.try_as_u64()?; - Ok(crate::utils::sompi_to_kaspa(sompi)) +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "bigint | number | HexString")] + #[derive(Clone, Debug)] + pub type ISompiToKaspa; } +/// Convert a Kaspa string to Sompi represented by bigint. +/// This function provides correct precision handling and +/// can be used to parse user input. +/// @category Wallet SDK #[wasm_bindgen(js_name = "kaspaToSompi")] -pub fn kaspa_to_sompi(kaspa: f64) -> u64 { - crate::utils::kaspa_to_sompi(kaspa) +pub fn kaspa_to_sompi(kaspa: String) -> Option { + crate::utils::try_kaspa_str_to_sompi(kaspa).ok().flatten().map(Into::into) } +/// +/// Convert Sompi to a string representation of the amount in Kaspa. +/// +/// @category Wallet SDK +/// #[wasm_bindgen(js_name = "sompiToKaspaString")] -pub fn sompi_to_kaspa_string(sompi: JsValue) -> Result { +pub fn sompi_to_kaspa_string(sompi: ISompiToKaspa) -> Result { let sompi = sompi.try_as_u64()?; Ok(crate::utils::sompi_to_kaspa_string(sompi)) } +/// +/// Format a Sompi amount to a string representation of the amount in Kaspa with a suffix +/// based on the network type (e.g. `KAS` for mainnet, `TKAS` for testnet, +/// `SKAS` for simnet, `DKAS` for devnet). +/// +/// @category Wallet SDK +/// #[wasm_bindgen(js_name = "sompiToKaspaStringWithSuffix")] -pub fn sompi_to_kaspa_string_with_suffix(sompi: JsValue, wallet: &crate::wasm::wallet::Wallet) -> Result { +pub fn sompi_to_kaspa_string_with_suffix(sompi: ISompiToKaspa, network: &NetworkTypeT) -> Result { let sompi = sompi.try_as_u64()?; - let network_type = wallet.wallet.network_id()?.network_type; + let network_type = NetworkType::try_from(network)?; Ok(crate::utils::sompi_to_kaspa_string_with_suffix(sompi, &network_type)) } diff --git a/wallet/core/src/wasm/utxo/context.rs b/wallet/core/src/wasm/utxo/context.rs index 5dfb6359c..2b69eb0c4 100644 --- a/wallet/core/src/wasm/utxo/context.rs +++ b/wallet/core/src/wasm/utxo/context.rs @@ -3,11 +3,95 @@ use crate::result::Result; use crate::utxo as native; use crate::utxo::{UtxoContextBinding, UtxoContextId}; use crate::wasm::utxo::UtxoProcessor; -use crate::wasm::Balance; -use kaspa_addresses::AddressList; +use crate::wasm::{Balance, BalanceStrings}; +use kaspa_addresses::AddressOrStringArrayT; +use kaspa_consensus_client::UtxoEntryReferenceArrayT; use kaspa_hashes::Hash; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; -#[derive(Clone)] +declare! { + IUtxoContextArgs, + r#" + /** + * UtxoContext constructor arguments. + * + * @see {@link UtxoProcessor}, {@link UtxoContext}, {@link RpcClient} + * @category Wallet SDK + */ + export interface IUtxoContextArgs { + /** + * Associated UtxoProcessor. + */ + processor: UtxoProcessor; + /** + * Optional id for the UtxoContext. + * **The id must be a valid 32-byte hex string.** + * You can use {@link sha256FromBinary} or {@link sha256FromText} to generate a valid id. + * + * If not provided, a random id will be generated. + * The IDs are deterministic, based on the order UtxoContexts are created. + */ + id?: HexString; + } + "#, +} + +/// +/// UtxoContext is a class that provides a way to track addresses activity +/// on the Kaspa network. When an address is registered with UtxoContext +/// it aggregates all UTXO entries for that address and emits events when +/// any activity against these addresses occurs. +/// +/// UtxoContext constructor accepts {@link IUtxoContextArgs} interface that +/// can contain an optional id parameter. If supplied, this `id` parameter +/// will be included in all notifications emitted by the UtxoContext as +/// well as included as a part of {@link ITransactionRecord} emitted when +/// transactions occur. If not provided, a random id will be generated. This id +/// typically represents an account id in the context of a wallet application. +/// The integrated Wallet API uses UtxoContext to represent wallet accounts. +/// +/// **Exchanges:** if you are building an exchange wallet, it is recommended +/// to use UtxoContext for each user account. This way you can track and isolate +/// each user activity (use address set, balances, transaction records). +/// +/// UtxoContext maintains a real-time cumulative balance of all addresses +/// registered against it and provides balance update notification events +/// when the balance changes. +/// +/// The UtxoContext balance is comprised of 3 values: +/// - `mature`: amount of funds available for spending. +/// - `pending`: amount of funds that are being received. +/// - `outgoing`: amount of funds that are being sent but are not yet accepted by the network. +/// +/// Please see {@link IBalance} interface for more details. +/// +/// UtxoContext can be supplied as a UTXO source to the transaction {@link Generator} +/// allowing the {@link Generator} to create transactions using the +/// UTXO entries it manages. +/// +/// **IMPORTANT:** UtxoContext is meant to represent a single account. It is not +/// designed to be used as a global UTXO manager for all addresses in a very large +/// wallet (such as an exchange wallet). For such use cases, it is recommended to +/// perform manual UTXO management by subscribing to UTXO notifications using +/// {@link RpcClient.subscribeUtxosChanged} and {@link RpcClient.getUtxosByAddresses}. +/// +/// @see {@link IUtxoContextArgs}, +/// {@link UtxoProcessor}, +/// {@link Generator}, +/// {@link createTransactions}, +/// {@link IBalance}, +/// {@link IBalanceEvent}, +/// {@link IPendingEvent}, +/// {@link IReorgEvent}, +/// {@link IStasisEvent}, +/// {@link IMaturityEvent}, +/// {@link IDiscoveryEvent}, +/// {@link IBalanceEvent}, +/// {@link ITransactionRecord} +/// +/// @category Wallet SDK +/// +#[derive(Clone, CastFromJs)] #[wasm_bindgen(inspectable)] pub struct UtxoContext { inner: native::UtxoContext, @@ -21,67 +105,135 @@ impl UtxoContext { pub fn context(&self) -> MutexGuard { self.inner.context() } + + pub fn processor(&self) -> &native::UtxoProcessor { + self.inner.processor() + } } #[wasm_bindgen] impl UtxoContext { #[wasm_bindgen(constructor)] - pub async fn ctor(js_value: JsValue) -> Result { + pub fn ctor(js_value: IUtxoContextArgs) -> Result { let UtxoContextCreateArgs { processor, binding } = js_value.try_into()?; - let inner = native::UtxoContext::new(processor.inner(), binding); + let inner = native::UtxoContext::new(processor.processor(), binding); Ok(UtxoContext { inner }) } /// Performs a scan of the given addresses and registers them in the context for event notifications. #[wasm_bindgen(js_name = "trackAddresses")] - pub async fn track_addresses(&self, addresses: JsValue, optional_current_daa_score: JsValue) -> Result<()> { - let current_daa_score = - if optional_current_daa_score.is_falsy() { None } else { optional_current_daa_score.try_as_u64().ok() }; - - let addresses: Vec
= AddressList::try_from(addresses)?.into(); - self.inner().scan_and_register_addresses(addresses, current_daa_score).await + pub async fn track_addresses(&self, addresses: AddressOrStringArrayT, optional_current_daa_score: Option) -> Result<()> { + let current_daa_score = if let Some(big_int) = optional_current_daa_score { + Some(big_int.try_into().map_err(|v| Error::custom(format!("Unable to convert BigInt value {v:?}")))?) + } else { + None + }; + let addresses: Vec
= addresses.try_into()?; + self.inner().scan_and_register_addresses(addresses, current_daa_score).await?; + Ok(()) } /// Unregister a list of addresses from the context. This will stop tracking of these addresses. #[wasm_bindgen(js_name = "unregisterAddresses")] - pub async fn unregister_addresses(&self, addresses: JsValue) -> Result<()> { - let addresses: Vec
= AddressList::try_from(addresses)?.into(); + pub async fn unregister_addresses(&self, addresses: AddressOrStringArrayT) -> Result<()> { + let addresses: Vec
= addresses.try_into()?; self.inner().unregister_addresses(addresses).await } - /// Clear the UtxoContext. Unregisters all addresses and clears all UTXO entries. + /// Clear the UtxoContext. Unregister all addresses and clear all UTXO entries. + /// IMPORTANT: This function must be manually called when disconnecting or re-connecting to the node + /// (followed by address re-registration). pub async fn clear(&self) -> Result<()> { self.inner().clear().await } - /// Returns the mature UTXO entries that are currently in the context. - pub fn mature(&self) -> Result { + pub fn active(&self) -> bool { + let processor = self.inner().processor(); + processor.try_rpc_ctl().map(|ctl| ctl.is_connected()).unwrap_or(false) && processor.is_connected() && processor.is_running() + } + + // Returns all mature UTXO entries that are currently managed by the UtxoContext and are available for spending. + // This function is for informational purposes only. + // pub fn mature(&self) -> Result { + // let context = self.context(); + // let array = Array::new(); + // for entry in context.mature.iter() { + // array.push(&JsValue::from(entry.clone())); + // } + // Ok(array.unchecked_into()) + // } + + /// + /// Returns a range of mature UTXO entries that are currently + /// managed by the UtxoContext and are available for spending. + /// + /// NOTE: This function is provided for informational purposes only. + /// **You should not manage UTXO entries manually if they are owned by UtxoContext.** + /// + /// The resulting range may be less than requested if UTXO entries + /// have been spent asynchronously by UtxoContext or by other means + /// (i.e. UtxoContext has received notification from the network that + /// UtxoEntries have been spent externally). + /// + /// UtxoEntries are kept in in the ascending sorted order by their amount. + /// + #[wasm_bindgen(js_name = "getMatureRange")] + pub fn mature_range(&self, mut from: usize, mut to: usize) -> Result { let context = self.context(); + if from > to { + return Err(Error::custom("'from' must be less than or equal to 'to'")); + } + if from > context.mature.len() { + from = context.mature.len(); + } + if to > context.mature.len() { + to = context.mature.len(); + } + if from == to { + return Ok(Array::new().unchecked_into()); + } + let slice = context.mature.get(from..to).unwrap(); let array = Array::new(); - for entry in context.mature.iter() { + for entry in slice.iter() { array.push(&JsValue::from(entry.clone())); } - Ok(array) + Ok(array.unchecked_into()) } - /// Returns the mature UTXO entries that are currently in the context. - pub fn pending(&self) -> Result { + /// Obtain the length of the mature UTXO entries that are currently + /// managed by the UtxoContext. + #[wasm_bindgen(getter, js_name = "matureLength")] + pub fn mature_length(&self) -> usize { + self.context().mature.len() + } + + /// Returns pending UTXO entries that are currently managed by the UtxoContext. + #[wasm_bindgen(js_name = "getPending")] + pub fn pending(&self) -> Result { let context = self.context(); let array = Array::new(); for (_, entry) in context.pending.iter() { array.push(&JsValue::from(entry.clone())); } - Ok(array) + Ok(array.unchecked_into()) } - #[wasm_bindgen(getter)] - pub fn balance(&self) -> JsValue { - self.inner().balance().map(Balance::from).map(JsValue::from).unwrap_or(JsValue::UNDEFINED) + /// Current {@link Balance} of the UtxoContext. + #[wasm_bindgen(getter, js_name = "balance")] + pub fn balance(&self) -> Option { + self.inner().balance().map(Balance::from) } - #[wasm_bindgen(js_name=updateBalance)] - pub async fn calculate_balance(&self) -> crate::wasm::Balance { - self.inner.calculate_balance().await.into() + /// Current {@link BalanceStrings} of the UtxoContext. + #[wasm_bindgen(getter, js_name = "balanceStrings")] + pub fn balance_strings(&self) -> Result> { + let network_id = self.inner.processor().network_id().ok(); + if let (Some(network_id), Some(balance)) = (network_id, self.inner().balance()) { + let balance_strings = balance.to_balance_strings(&network_id.into(), None); + Ok(Some(BalanceStrings::from(balance_strings))) + } else { + Ok(None) + } } } @@ -97,10 +249,10 @@ impl From for native::UtxoContext { } } -impl TryFrom for UtxoContext { +impl TryCastFromJs for UtxoContext { type Error = Error; - fn try_from(value: JsValue) -> std::result::Result { - Ok(ref_from_abi!(UtxoContext, &value)?) + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Ok(Self::try_ref_from_js_value_as_cast(value)?) } } @@ -109,21 +261,21 @@ pub struct UtxoContextCreateArgs { binding: UtxoContextBinding, } -impl TryFrom for UtxoContextCreateArgs { +impl TryFrom for UtxoContextCreateArgs { type Error = Error; - fn try_from(value: JsValue) -> std::result::Result { + fn try_from(value: IUtxoContextArgs) -> std::result::Result { if let Some(object) = Object::try_from(&value) { - let processor = object.get::("processor")?; + let processor = object.get_cast::("processor")?; - let binding = if let Some(id) = object.try_get::("id")? { - UtxoContextBinding::Id(UtxoContextId::new(id)) + let binding = if let Some(id) = object.try_get_cast::("id")? { + UtxoContextBinding::Id(UtxoContextId::new(id.into_owned())) } else { UtxoContextBinding::default() }; - Ok(UtxoContextCreateArgs { binding, processor }) + Ok(UtxoContextCreateArgs { binding, processor: processor.into_owned() }) } else { - Err(Error::custom("UtxoProcessor: suppliedd value must be an object")) + Err(Error::custom("UtxoProcessor: supplied value must be an object")) } } } diff --git a/wallet/core/src/wasm/utxo/processor.rs b/wallet/core/src/wasm/utxo/processor.rs index 051dc587e..0e41d8f77 100644 --- a/wallet/core/src/wasm/utxo/processor.rs +++ b/wallet/core/src/wasm/utxo/processor.rs @@ -1,56 +1,164 @@ use crate::error::Error; +use crate::events::{EventKind, Events}; use crate::imports::*; use crate::result::Result; use crate::utxo as native; -use kaspa_wrpc_client::wasm::RpcClient; -use workflow_wasm::channel::EventDispatcher; +use crate::wasm::notify::{UtxoProcessorEventTarget, UtxoProcessorNotificationCallback, UtxoProcessorNotificationTypeOrCallback}; +use kaspa_consensus_core::network::NetworkIdT; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; +use kaspa_wasm_core::events::{get_event_targets, Sink}; +use kaspa_wrpc_wasm::RpcClient; +use workflow_log::log_error; -#[derive(Clone)] -#[wasm_bindgen(inspectable)] -pub struct UtxoProcessor { - inner: native::UtxoProcessor, - #[wasm_bindgen(getter_with_clone)] - pub rpc: RpcClient, - #[wasm_bindgen(getter_with_clone)] - pub events: EventDispatcher, +declare! { + IUtxoProcessorArgs, + r#" + /** + * UtxoProcessor constructor arguments. + * + * @see {@link UtxoProcessor}, {@link UtxoContext}, {@link RpcClient}, {@link NetworkId} + * @category Wallet SDK + */ + export interface IUtxoProcessorArgs { + /** + * The RPC client to use for network communication. + */ + rpc : RpcClient; + networkId : NetworkId | string; + } + "#, } -impl UtxoProcessor { - pub fn inner(&self) -> &native::UtxoProcessor { - &self.inner +pub struct Inner { + processor: native::UtxoProcessor, + rpc: RpcClient, + + callbacks: Mutex>>, + task_running: AtomicBool, + task_ctl: DuplexChannel, +} + +impl Inner { + fn callbacks(&self, event: EventKind) -> Option> { + let callbacks = self.callbacks.lock().unwrap(); + let all = callbacks.get(&EventKind::All).cloned(); + let target = callbacks.get(&event).cloned(); + match (all, target) { + (Some(mut vec_all), Some(vec_target)) => { + vec_all.extend(vec_target); + Some(vec_all) + } + (Some(vec_all), None) => Some(vec_all), + (None, Some(vec_target)) => Some(vec_target), + (None, None) => None, + } } } +cfg_if! { + if #[cfg(any(feature = "wasm32-core", feature = "wasm32-sdk"))] { + #[wasm_bindgen(typescript_custom_section)] + const TS_NOTIFY: &'static str = r#" + interface UtxoProcessor { + /** + * @param {UtxoProcessorNotificationCallback} callback + */ + addEventListener(callback:UtxoProcessorNotificationCallback): void; + /** + * @param {UtxoProcessorEventType} event + * @param {UtxoProcessorNotificationCallback} [callback] + */ + addEventListener( + event: M, + callback: (eventData: UtxoProcessorEventMap[M]) => void + ) + }"#; + } +} + +/// +/// UtxoProcessor class is the main coordinator that manages UTXO processing +/// between multiple UtxoContext instances. It acts as a bridge between the +/// Kaspa node RPC connection, address subscriptions and UtxoContext instances. +/// +/// @see {@link IUtxoProcessorArgs}, +/// {@link UtxoContext}, +/// {@link RpcClient}, +/// {@link NetworkId}, +/// {@link IConnectEvent} +/// {@link IDisconnectEvent} +/// @category Wallet SDK +/// +#[derive(Clone, CastFromJs)] +#[wasm_bindgen(inspectable)] +pub struct UtxoProcessor { + inner: Arc, +} + #[wasm_bindgen] impl UtxoProcessor { + /// UtxoProcessor constructor. + /// + /// + /// + /// @see {@link IUtxoProcessorArgs} #[wasm_bindgen(constructor)] - pub async fn ctor(js_value: JsValue) -> Result { + pub fn ctor(js_value: IUtxoProcessorArgs) -> Result { let UtxoProcessorCreateArgs { rpc, network_id } = js_value.try_into()?; 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, None); - let events = EventDispatcher::new(); + let processor = native::UtxoProcessor::new(Some(rpc_binding), Some(network_id), None, None); + + let this = UtxoProcessor { + inner: Arc::new(Inner { + processor: processor.clone(), + rpc, + callbacks: Mutex::new(AHashMap::new()), + task_running: AtomicBool::new(false), + task_ctl: DuplexChannel::oneshot(), + }), + }; + + Ok(this) + } + + /// Starts the UtxoProcessor and begins processing UTXO and other notifications. + pub async fn start(&self) -> Result<()> { + self.start_notification_task(self.inner.processor.multiplexer()).await?; + self.inner.processor.start().await?; + Ok(()) + } - inner.start().await?; + /// Stops the UtxoProcessor and ends processing UTXO and other notifications. + pub async fn stop(&self) -> Result<()> { + self.inner.processor.stop().await?; + self.stop_notification_task().await?; + Ok(()) + } - Ok(UtxoProcessor { inner, rpc, events }) + #[wasm_bindgen(getter)] + pub fn rpc(&self) -> RpcClient { + self.inner.rpc.clone() } - // TODO - discuss async ctor interface - // pub async fn start(&self) -> Result<()> { - // self.inner().start().await - // } + #[wasm_bindgen(getter, js_name = "networkId")] + pub fn network_id(&self) -> Option { + self.inner.processor.network_id().ok().map(|network_id| network_id.to_string()) + } - pub async fn shutdown(&self) -> Result<()> { - self.inner().stop().await + #[wasm_bindgen(js_name = "setNetworkId")] + pub fn set_network_id(&self, network_id: &NetworkIdT) -> Result<()> { + let network_id = NetworkId::try_cast_from(network_id)?; + self.inner.processor.set_network_id(network_id.as_ref()); + Ok(()) } } -impl TryFrom for UtxoProcessor { - type Error = Error; - fn try_from(value: JsValue) -> std::result::Result { - Ok(ref_from_abi!(UtxoProcessor, &value)?) +impl TryCastFromJs for UtxoProcessor { + type Error = workflow_wasm::error::Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::try_ref_from_js_value_as_cast(value) } } @@ -59,16 +167,133 @@ pub struct UtxoProcessorCreateArgs { network_id: NetworkId, } -impl TryFrom for UtxoProcessorCreateArgs { +impl TryFrom for UtxoProcessorCreateArgs { type Error = Error; - fn try_from(value: JsValue) -> std::result::Result { + fn try_from(value: IUtxoProcessorArgs) -> std::result::Result { if let Some(object) = Object::try_from(&value) { let rpc = object.get_value("rpc")?; - let rpc = ref_from_abi!(RpcClient, &rpc)?; + let rpc = RpcClient::try_ref_from_js_value(&rpc)?.clone(); let network_id = object.get::("networkId")?; Ok(UtxoProcessorCreateArgs { rpc, network_id }) } else { - Err(Error::custom("UtxoProcessor: suppliedd value must be an object")) + Err(Error::custom("UtxoProcessor: supplied value must be an object")) + } + } +} + +impl UtxoProcessor { + pub fn inner(&self) -> &Arc { + &self.inner + } + + pub fn processor(&self) -> &native::UtxoProcessor { + &self.inner.processor + } + + pub async fn start_notification_task(&self, multiplexer: &Multiplexer>) -> Result<()> { + let inner = self.inner.clone(); + + if inner.task_running.load(Ordering::SeqCst) { + log_error!("You are calling `UtxoProcessor.start()` twice without calling `UtxoProcessor.stop()`!"); + panic!("UtxoProcessor background task is already running"); + } else { + inner.task_running.store(true, Ordering::SeqCst); + } + + let ctl_receiver = inner.task_ctl.request.receiver.clone(); + let ctl_sender = inner.task_ctl.response.sender.clone(); + let channel = multiplexer.channel(); + + spawn(async move { + loop { + select! { + _ = ctl_receiver.recv().fuse() => { + break; + }, + msg = channel.receiver.recv().fuse() => { + if let Ok(notification) = &msg { + let event_type = EventKind::from(notification.as_ref()); + let callbacks = inner.callbacks(event_type); + if let Some(handlers) = callbacks { + for handler in handlers.into_iter() { + let value = notification.as_ref().to_js_value(); + if let Err(err) = handler.call(&value) { + log_error!("Error while executing notification callback: {:?}", err); + } + } + } + } + } + } + } + + channel.close(); + inner.task_running.store(false, Ordering::SeqCst); + ctl_sender.send(()).await.ok(); + }); + + Ok(()) + } + + pub async fn stop_notification_task(&self) -> Result<()> { + let inner = &self.inner; + if inner.task_running.load(Ordering::SeqCst) { + inner.task_ctl.signal(()).await.map_err(|err| JsValue::from_str(&err.to_string()))?; + } + Ok(()) + } +} + +#[wasm_bindgen] +impl UtxoProcessor { + #[wasm_bindgen(js_name = "addEventListener", skip_typescript)] + pub fn add_event_listener( + &self, + event: UtxoProcessorNotificationTypeOrCallback, + callback: Option, + ) -> Result<()> { + if let Ok(sink) = Sink::try_from(&event) { + let event = EventKind::All; + self.inner.callbacks.lock().unwrap().entry(event).or_default().push(sink); + Ok(()) + } else if let Some(Ok(sink)) = callback.map(Sink::try_from) { + let targets: Vec = get_event_targets(event)?; + for event in targets { + self.inner.callbacks.lock().unwrap().entry(event).or_default().push(sink.clone()); + } + Ok(()) + } else { + Err(Error::custom("Invalid event listener callback")) + } + } + + #[wasm_bindgen(js_name = "removeEventListener")] + pub fn remove_event_listener( + &self, + event: UtxoProcessorEventTarget, + callback: Option, + ) -> Result<()> { + let mut callbacks = self.inner.callbacks.lock().unwrap(); + if let Ok(sink) = Sink::try_from(&event) { + // remove callback from all events + for (_, handlers) in callbacks.iter_mut() { + handlers.retain(|handler| handler != &sink); + } + } else if let Some(Ok(sink)) = callback.map(Sink::try_from) { + // remove callback from specific events + let targets: Vec = get_event_targets(event)?; + for target in targets.into_iter() { + callbacks.entry(target).and_modify(|handlers| { + handlers.retain(|handler| handler != &sink); + }); + } + } else { + // remove all callbacks for the event + let targets: Vec = get_event_targets(event)?; + for event in targets { + callbacks.remove(&event); + } } + Ok(()) } } diff --git a/wallet/core/src/wasm/wallet/account.rs b/wallet/core/src/wasm/wallet/account.rs index d38f008c2..976e1df62 100644 --- a/wallet/core/src/wasm/wallet/account.rs +++ b/wallet/core/src/wasm/wallet/account.rs @@ -1,15 +1,18 @@ use crate::account as native; use crate::imports::*; -use crate::result::Result; -use crate::secret::Secret; use crate::tx::PaymentOutputs; use crate::wasm::utxo::UtxoContext; -use kaspa_consensus_wasm::Keypair; +use kaspa_consensus_core::network::NetworkTypeT; +use kaspa_wallet_keys::keypair::Keypair; use workflow_core::abortable::Abortable; -use workflow_wasm::abi::ref_from_abi; +/// +/// The `Account` class is a wallet account that can be used to send and receive payments. +/// +/// +/// @category Wallet API #[wasm_bindgen(inspectable)] -#[derive(Clone)] +#[derive(Clone, CastFromJs)] pub struct Account { inner: Arc, #[wasm_bindgen(getter_with_clone)] @@ -47,9 +50,9 @@ impl Account { } #[wasm_bindgen(js_name = balanceStrings)] - pub fn balance_strings(&self, network_type: JsValue) -> Result { + pub fn balance_strings(&self, network_type: &NetworkTypeT) -> Result { match self.inner.balance() { - Some(balance) => Ok(crate::wasm::Balance::from(balance).as_strings(network_type)?.into()), + Some(balance) => Ok(crate::wasm::Balance::from(balance).to_balance_strings(network_type)?.into()), None => Ok(JsValue::UNDEFINED), } } @@ -95,10 +98,10 @@ impl From for Arc { } } -impl TryFrom for Account { +impl TryFrom<&JsValue> for Account { type Error = Error; - fn try_from(js_value: JsValue) -> std::result::Result { - Ok(ref_from_abi!(Account, &js_value)?) + fn try_from(js_value: &JsValue) -> std::result::Result { + Ok(Account::try_ref_from_js_value(js_value)?.clone()) } } @@ -116,11 +119,11 @@ impl TryFrom for AccountSendArgs { type Error = Error; fn try_from(js_value: JsValue) -> std::result::Result { if let Some(object) = Object::try_from(&js_value) { - let outputs = object.get::("outputs")?; + let outputs = object.get_cast::("outputs")?.into_owned(); let priority_fee_sompi = object.get_u64("priorityFee").ok(); let include_fees_in_amount = object.get_bool("includeFeesInAmount").unwrap_or(false); - let abortable = object.get("abortable").ok().and_then(|v| ref_from_abi!(Abortable, &v).ok()).unwrap_or_default(); + let abortable = object.get("abortable").ok().and_then(|v| Abortable::try_from(&v).ok()).unwrap_or_default(); let wallet_secret = object.get_string("walletSecret")?.into(); let payment_secret = object.get_value("paymentSecret")?.as_string().map(|s| s.into()); @@ -141,12 +144,12 @@ impl TryFrom for AccountCreateArgs { type Error = Error; fn try_from(value: JsValue) -> std::result::Result { if let Some(object) = Object::try_from(&value) { - let _keypair = object.try_get::("keypair")?; - let _public_key = object.try_get::("keypair")?; + let _keypair = object.try_get_cast::("keypair")?; + let _public_key = object.try_get_cast::("keypair")?; Ok(AccountCreateArgs {}) } else { - Err(Error::custom("Account: suppliedd value must be an object")) + Err(Error::custom("Account: supplied value must be an object")) } } } diff --git a/wallet/core/src/wasm/wallet/keydata.rs b/wallet/core/src/wasm/wallet/keydata.rs index c689cfbf6..e24b06e46 100644 --- a/wallet/core/src/wasm/wallet/keydata.rs +++ b/wallet/core/src/wasm/wallet/keydata.rs @@ -2,6 +2,7 @@ use crate::imports::*; use crate::result::Result; use crate::storage::keydata; +/// @category Wallet SDK #[wasm_bindgen] pub struct PrvKeyDataInfo { inner: Arc, diff --git a/wallet/core/src/wasm/wallet/mod.rs b/wallet/core/src/wasm/wallet/mod.rs index 7def5405b..cacd73628 100644 --- a/wallet/core/src/wasm/wallet/mod.rs +++ b/wallet/core/src/wasm/wallet/mod.rs @@ -3,6 +3,7 @@ pub mod keydata; #[allow(clippy::module_inception)] pub mod wallet; -pub use account::Account; +// Account class is disabled for now but is kept for potential future re-integration. +// pub use account::Account; pub use keydata::PrvKeyDataInfo; pub use wallet::Wallet; diff --git a/wallet/core/src/wasm/wallet/storage.rs b/wallet/core/src/wasm/wallet/storage.rs index c0a068a13..5e4546692 100644 --- a/wallet/core/src/wasm/wallet/storage.rs +++ b/wallet/core/src/wasm/wallet/storage.rs @@ -6,7 +6,7 @@ use wasm_bindgen::JsValue; use web_sys::Element; // TODO - finish WASM storage implementation bindings - +/// @alpha #[wasm_bindgen] extern "C" { diff --git a/wallet/core/src/wasm/wallet/wallet.rs b/wallet/core/src/wasm/wallet/wallet.rs index 1aa9ac018..bd91bedf2 100644 --- a/wallet/core/src/wasm/wallet/wallet.rs +++ b/wallet/core/src/wasm/wallet/wallet.rs @@ -1,354 +1,326 @@ use crate::imports::*; -use crate::result::Result; use crate::storage::local::interface::LocalStore; -use crate::storage::{PrvKeyDataId, WalletDescriptor}; +use crate::storage::WalletDescriptor; use crate::wallet as native; -use crate::wasm::wallet::account::Account; -use crate::wasm::wallet::keydata::PrvKeyDataInfo; -use kaspa_wrpc_client::wasm::RpcClient; -use kaspa_wrpc_client::WrpcEncoding; -use workflow_core::sendable::Sendable; -use workflow_wasm::channel::EventDispatcher; +use crate::wasm::notify::{WalletEventTarget, WalletNotificationCallback, WalletNotificationTypeOrCallback}; +use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; +use kaspa_wasm_core::events::{get_event_targets, Sink}; +use kaspa_wrpc_wasm::{IConnectOptions, Resolver, RpcClient, RpcConfig, WrpcEncoding}; + +declare! { + IWalletConfig, + r#" + /** + * + * + * @category Wallet API + */ + export interface IWalletConfig { + /** + * `resident` is a boolean indicating if the wallet should not be stored on the permanent medium. + */ + resident?: boolean; + networkId?: NetworkId | string; + encoding?: Encoding | string; + url?: string; + resolver?: Resolver; + } + "#, +} + +#[derive(Default)] +struct WalletCtorArgs { + resident: bool, + network_id: Option, + encoding: Option, + url: Option, + resolver: Option, +} + +impl TryFrom for WalletCtorArgs { + type Error = Error; + fn try_from(js_value: JsValue) -> Result { + if let Some(object) = Object::try_from(&js_value) { + let resident = object.get_value("resident")?.as_bool().unwrap_or(false); + let network_id = object.try_get::("networkId")?; + let encoding = object.try_get::("encoding")?; + let url = object.get_value("url")?.as_string(); + let resolver = object.try_get("resolver")?; + + Ok(Self { resident, network_id, encoding, url, resolver }) + } else { + Ok(WalletCtorArgs::default()) + } + } +} + +struct Inner { + wallet: Arc, + rpc: RpcClient, + callbacks: Mutex>>, + task_running: AtomicBool, + task_ctl: DuplexChannel, +} + +impl Inner { + fn callbacks(&self, event: EventKind) -> Option> { + let callbacks = self.callbacks.lock().unwrap(); + let all = callbacks.get(&EventKind::All).cloned(); + let target = callbacks.get(&event).cloned(); + match (all, target) { + (Some(mut vec_all), Some(vec_target)) => { + vec_all.extend(vec_target); + Some(vec_all) + } + (Some(vec_all), None) => Some(vec_all), + (None, Some(vec_target)) => Some(vec_target), + (None, None) => None, + } + } +} +/// +/// Wallet class is the main coordinator that manages integrated wallet operations. +/// +/// The Wallet class encapsulates {@link UtxoProcessor} and provides internal +/// account management using {@link UtxoContext} instances. It acts as a bridge +/// between the integrated Wallet subsystem providing a high-level interface +/// for wallet key and account management. +/// +/// The Rusty Kaspa is developed in Rust, and the Wallet class is a Rust implementation +/// exposed to the JavaScript/TypeScript environment using the WebAssembly (WASM32) interface. +/// As such, the Wallet implementation can be powered up using native Rust or built +/// as a WebAssembly module and used in the browser or Node.js environment. +/// +/// When using Rust native or NodeJS environment, all wallet data is stored on the local +/// filesystem. When using WASM32 build in the web browser, the wallet data is stored +/// in the browser's `localStorage` and transaction records are stored in the `IndexedDB`. +/// +/// The Wallet API can create multiple wallet instances, however, only one wallet instance +/// can be active at a time. +/// +/// The wallet implementation is designed to be efficient and support a large number +/// of accounts. Accounts reside in storage and can be loaded and activated as needed. +/// A `loaded` account contains all account information loaded from the permanent storage +/// whereas an `active` account monitors the UTXO set and provides notifications for +/// incoming and outgoing transactions as well as balance updates. +/// +/// The Wallet API communicates with the client using resource identifiers. These include +/// account IDs, private key IDs, transaction IDs, etc. It is the responsibility of the +/// client to track these resource identifiers at runtime. +/// +/// @see {@link IWalletConfig}, +/// +/// @category Wallet API +/// #[wasm_bindgen(inspectable)] #[derive(Clone)] pub struct Wallet { - pub(crate) wallet: Arc, - #[wasm_bindgen(getter_with_clone)] - pub rpc: RpcClient, - #[wasm_bindgen(getter_with_clone)] - pub events: EventDispatcher, + inner: Arc, +} + +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { + #[wasm_bindgen(typescript_custom_section)] + const TS_NOTIFY: &'static str = r#" + interface Wallet { + /** + * @param {WalletNotificationCallback} callback + */ + addEventListener(callback:WalletNotificationCallback): void; + /** + * @param {WalletEventType} event + * @param {WalletNotificationCallback} [callback] + */ + addEventListener( + event: M, + callback: (eventData: WalletEventMap[M]) => void + ) + }"#; + } } #[wasm_bindgen] impl Wallet { #[wasm_bindgen(constructor)] - pub fn constructor(js_value: JsValue) -> Result { - let WalletCtorArgs { resident, network_id, encoding, url } = WalletCtorArgs::try_from(js_value)?; + pub fn constructor(config: IWalletConfig) -> Result { + let WalletCtorArgs { resident, network_id, encoding, url, resolver } = WalletCtorArgs::try_from(JsValue::from(config))?; let store = Arc::new(LocalStore::try_new(resident)?); - let rpc = RpcClient::new( - url.unwrap_or("wrpc://127.0.0.1:17110".to_string()).as_str(), - encoding.unwrap_or(WrpcEncoding::Borsh), - None, - )?; + + let rpc_config = RpcConfig { url, resolver, encoding, network_id }; + + let rpc = RpcClient::new(Some(rpc_config))?; 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(native::Wallet::try_with_rpc(Some(rpc_binding), store, network_id)?); - let events = EventDispatcher::default(); - Ok(Self { wallet, events, rpc }) + Ok(Self { + inner: Arc::new(Inner { + wallet, + rpc, + callbacks: Mutex::new(AHashMap::new()), + task_running: AtomicBool::new(false), + task_ctl: DuplexChannel::oneshot(), + }), + }) } - pub async fn keys(&self) -> JsValue { - let wallet = self.wallet.clone(); - let keys = self.wallet.keys().await.expect("Unable to access Wallet::keys iterator").then(move |item| { - let wallet = wallet.clone(); - async move { - match item { - Ok(prv_key_data_info) => Sendable::new(PrvKeyDataInfo::new(wallet, prv_key_data_info).into()), - Err(err) => Sendable::new(JsValue::from(err)), - } - } - }); - - AsyncStream::new(keys).into() - } - - pub async fn accounts(&self) -> Result { - self.account_iterator(JsValue::NULL).await - } - - #[wasm_bindgen(js_name = "accountIterator")] - pub async fn account_iterator(&self, prv_key_data_id_filter: JsValue) -> Result { - let prv_key_data_id_filter = if prv_key_data_id_filter.is_falsy() { - None - } else { - Some(PrvKeyDataId::from_hex( - &prv_key_data_id_filter - .as_string() - .ok_or(Error::Custom("private key data id account filter must be a hex string or falsy".to_string()))?, - )?) - }; - - let accounts = self - .wallet - .accounts(prv_key_data_id_filter) - .await - .unwrap_or_else(|err| panic!("Unable to access Wallet::account iterator: {err}")) - .then(|item| async move { - match item { - Ok(account) => Sendable::new( - Account::try_new(account).await.unwrap_or_else(|err| panic!("accountIterator (account): {err}")).into(), - ), - Err(err) => Sendable::new(JsValue::from(err)), - } - }); - - Ok(AsyncStream::new(accounts).into()) + #[wasm_bindgen(getter, js_name = "rpc")] + pub fn rpc(&self) -> RpcClient { + self.inner.rpc.clone() } - #[wasm_bindgen(js_name = "isOpen")] + /// @remarks This is a local property indicating + /// if the wallet is currently open. + #[wasm_bindgen(getter, js_name = "isOpen")] pub fn is_open(&self) -> bool { - self.wallet.is_open() + self.wallet().is_open() } - #[wasm_bindgen(js_name = "isSynced")] + /// @remarks This is a local property indicating + /// if the node is currently synced. + #[wasm_bindgen(getter, js_name = "isSynced")] pub fn is_synced(&self) -> bool { - self.wallet.is_synced() + self.wallet().is_synced() } - #[wasm_bindgen(js_name = "descriptor")] + #[wasm_bindgen(getter, js_name = "descriptor")] pub fn descriptor(&self) -> Option { - self.wallet.descriptor() + self.wallet().descriptor() } - pub async fn exists(&self, name: JsValue) -> Result { - let name = - if name.is_falsy() { - None - } else { - Some(name.as_string().ok_or(Error::Custom( - "Wallet::exists(): Wallet name must be a string (or falsy for default `kaspa`)".to_string(), - ))?) - }; - - self.wallet.exists(name.as_deref()).await + /// Check if a wallet with a given name exists. + pub async fn exists(&self, name: Option) -> Result { + 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 (_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?; - self.wallet.start().await?; + self.start_notification_task(self.wallet().multiplexer()).await?; + self.wallet().start().await?; Ok(()) } pub async fn stop(&self) -> Result<()> { - self.wallet.stop().await?; - self.events.stop_notification_task().await?; + self.wallet().stop().await?; + self.stop_notification_task().await?; Ok(()) } - pub async fn connect(&self, args: JsValue) -> Result<()> { - self.rpc.connect(args).await?; + pub async fn connect(&self, args: Option) -> Result<()> { + self.inner.rpc.connect(args).await?; Ok(()) } pub async fn disconnect(&self) -> Result<()> { - self.rpc.client.disconnect().await?; + self.inner.rpc.client().disconnect().await?; Ok(()) } -} -#[derive(Default)] -struct WalletCtorArgs { - resident: bool, - network_id: Option, - encoding: Option, - url: Option, -} + #[wasm_bindgen(js_name = "addEventListener", skip_typescript)] + pub fn add_event_listener( + &self, + event: WalletNotificationTypeOrCallback, + callback: Option, + ) -> Result<()> { + if let Ok(sink) = Sink::try_from(&event) { + let event = EventKind::All; + self.inner.callbacks.lock().unwrap().entry(event).or_default().push(sink); + Ok(()) + } else if let Some(Ok(sink)) = callback.map(Sink::try_from) { + let targets: Vec = get_event_targets(event)?; + for event in targets { + self.inner.callbacks.lock().unwrap().entry(event).or_default().push(sink.clone()); + } + Ok(()) + } else { + Err(Error::custom("Invalid event listener callback")) + } + } -impl TryFrom for WalletCtorArgs { - type Error = Error; - fn try_from(js_value: JsValue) -> Result { - if let Some(object) = Object::try_from(&js_value) { - let resident = object.get_value("resident")?.as_bool().unwrap_or(false); + #[wasm_bindgen(js_name = "removeEventListener")] + pub fn remove_event_listener(&self, event: WalletEventTarget, callback: Option) -> Result<()> { + let mut callbacks = self.inner.callbacks.lock().unwrap(); + if let Ok(sink) = Sink::try_from(&event) { + // remove callback from all events + for (_, handlers) in callbacks.iter_mut() { + handlers.retain(|handler| handler != &sink); + } + } else if let Some(Ok(sink)) = callback.map(Sink::try_from) { + // remove callback from specific events + let targets: Vec = get_event_targets(event)?; + for target in targets.into_iter() { + callbacks.entry(target).and_modify(|handlers| { + handlers.retain(|handler| handler != &sink); + }); + } + } else { + // remove all callbacks for the event + let targets: Vec = get_event_targets(event)?; + for event in targets { + callbacks.remove(&event); + } + } + Ok(()) + } +} - let network_id = object.try_get::("networkId")?; - let encoding = object.try_get::("encoding")?; +impl Wallet { + pub fn wallet(&self) -> &Arc { + &self.inner.wallet + } - let url = object.get_value("url")?.as_string(); + pub async fn start_notification_task(&self, multiplexer: &Multiplexer>) -> Result<()> { + let inner = self.inner.clone(); - Ok(Self { resident, network_id, encoding, url }) + if inner.task_running.load(Ordering::SeqCst) { + panic!("ReflectorClient task is already running"); } else { - Ok(WalletCtorArgs::default()) + inner.task_running.store(true, Ordering::SeqCst); } + + let ctl_receiver = inner.task_ctl.request.receiver.clone(); + let ctl_sender = inner.task_ctl.response.sender.clone(); + + let channel = multiplexer.channel(); + + spawn(async move { + loop { + select! { + _ = ctl_receiver.recv().fuse() => { + break; + }, + msg = channel.receiver.recv().fuse() => { + if let Ok(notification) = &msg { + let event_type = EventKind::from(notification.as_ref()); + let callbacks = inner.callbacks(event_type); + if let Some(handlers) = callbacks { + for handler in handlers.into_iter() { + let value = notification.as_ref().to_js_value(); + if let Err(err) = handler.call(&value) { + log_error!("Error while executing notification callback: {:?}", err); + } + } + } + } + } + } + } + + channel.close(); + ctl_sender.send(()).await.ok(); + }); + + Ok(()) } -} -// 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 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: 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) { -// 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 Drop for PrvKeyDataCreateArgs { -// fn drop(&mut self) { -// self.wallet_secret.clear(); -// self.payment_secret.clear(); -// self.mnemonic.zeroize(); -// } -// } - -// 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")?, -// 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::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()), -// } -// } -// } + pub async fn stop_notification_task(&self) -> Result<()> { + let inner = &self.inner; + if inner.task_running.load(Ordering::SeqCst) { + inner.task_running.store(false, Ordering::SeqCst); + inner.task_ctl.signal(()).await.map_err(|err| JsValue::from_str(&err.to_string()))?; + } + Ok(()) + } +} diff --git a/wallet/core/src/wasm/xpublickey.rs b/wallet/core/src/wasm/xpublickey.rs deleted file mode 100644 index c4deb8e06..000000000 --- a/wallet/core/src/wasm/xpublickey.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::derivation::gen1::WalletDerivationManager; -use crate::derivation::traits::WalletDerivationManagerTrait; -use crate::result::Result; -use kaspa_bip32::ExtendedPublicKey; -use kaspa_bip32::{ExtendedPrivateKey, SecretKey}; -use std::str::FromStr; -use wasm_bindgen::prelude::*; -use workflow_wasm::serde::to_value; - -#[wasm_bindgen] -pub struct XPublicKey { - hd_wallet: WalletDerivationManager, -} -#[wasm_bindgen] -impl XPublicKey { - #[wasm_bindgen(js_name=fromXPub)] - pub async fn from_xpub(kpub: &str, cosigner_index: Option) -> Result { - let xpub = ExtendedPublicKey::::from_str(kpub)?; - let hd_wallet = WalletDerivationManager::from_extended_public_key(xpub, cosigner_index)?; - Ok(Self { hd_wallet }) - } - - #[wasm_bindgen(js_name=fromMasterXPrv)] - pub async fn from_master_xprv( - xprv: &str, - is_multisig: bool, - account_index: u64, - cosigner_index: Option, - ) -> Result { - let xprv = ExtendedPrivateKey::::from_str(xprv)?; - let path = WalletDerivationManager::build_derivate_path(is_multisig, account_index, None, None)?; - let xprv = xprv.derive_path(path)?; - let xpub = xprv.public_key(); - let hd_wallet = WalletDerivationManager::from_extended_public_key(xpub, cosigner_index)?; - Ok(Self { hd_wallet }) - } - - #[wasm_bindgen(js_name=receivePubkeys)] - pub async fn receive_pubkeys(&self, mut start: u32, mut end: u32) -> Result { - if start > end { - (start, end) = (end, start); - } - let pubkeys = self.hd_wallet.receive_pubkey_manager().derive_pubkey_range(start..end)?; - let pubkeys = to_value(&pubkeys)?; - Ok(pubkeys) - } - - #[wasm_bindgen(js_name=changePubkeys)] - pub async fn change_pubkeys(&self, mut start: u32, mut end: u32) -> Result { - if start > end { - (start, end) = (end, start); - } - let pubkeys = self.hd_wallet.change_pubkey_manager().derive_pubkey_range(start..end)?; - let pubkeys = to_value(&pubkeys)?; - - Ok(pubkeys) - } -} diff --git a/wallet/keys/Cargo.toml b/wallet/keys/Cargo.toml new file mode 100644 index 000000000..7300c1de5 --- /dev/null +++ b/wallet/keys/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "kaspa-wallet-keys" +description = "Kaspa wallet key primitives" +keywords = ["kaspa", "wallet"] +version.workspace = true +edition.workspace = true +authors.workspace = true +include.workspace = true +license.workspace = true +repository.workspace = true + +[features] +default = [] + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +async-trait.workspace = true +borsh.workspace = true +downcast.workspace = true +faster-hex.workspace = true +hmac.workspace = true +js-sys.workspace = true +kaspa-addresses.workspace = true +kaspa-bip32.workspace = true +kaspa-consensus-core.workspace = true +kaspa-txscript-errors.workspace = true +kaspa-txscript.workspace = true +kaspa-utils.workspace = true +kaspa-wasm-core.workspace = true +rand.workspace = true +ripemd.workspace = true +secp256k1.workspace = true +serde_json.workspace = true +serde-wasm-bindgen.workspace = true +serde.workspace = true +sha2.workspace = true +thiserror.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen.workspace = true +workflow-core.workspace = true +workflow-wasm.workspace = true +zeroize.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio.workspace = true + +[lints.clippy] +empty_docs = "allow" diff --git a/wallet/core/src/derivation/gen0/hd.rs b/wallet/keys/src/derivation/gen0/hd.rs similarity index 98% rename from wallet/core/src/derivation/gen0/hd.rs rename to wallet/keys/src/derivation/gen0/hd.rs index 595ee55dc..dd85f8fe5 100644 --- a/wallet/core/src/derivation/gen0/hd.rs +++ b/wallet/keys/src/derivation/gen0/hd.rs @@ -10,7 +10,6 @@ use kaspa_bip32::{ use ripemd::Ripemd160; use sha2::{Digest, Sha256}; use std::fmt::Debug; -use wasm_bindgen::prelude::*; fn get_fingerprint(private_key: &K) -> KeyFingerprint where @@ -39,7 +38,6 @@ impl Inner { } #[derive(Clone)] -#[wasm_bindgen(inspectable)] pub struct PubkeyDerivationManagerV0 { inner: Arc>>, index: Arc>, @@ -215,9 +213,9 @@ impl PubkeyDerivationManagerV0 { } } -#[wasm_bindgen] +// #[wasm_bindgen] impl PubkeyDerivationManagerV0 { - #[wasm_bindgen(getter, js_name = publicKey)] + // #[wasm_bindgen(getter, js_name = publicKey)] pub fn get_public_key(&self) -> String { self.public_key().to_string(None) } @@ -363,16 +361,7 @@ impl WalletDerivationManagerV0 { address_type: AddressType, attrs: &ExtendedKeyAttrs, ) -> Result<(secp256k1::SecretKey, ExtendedKeyAttrs, HmacSha512)> { - // if let Some(cosigner_index) = cosigner_index { - // public_key = public_key.derive_child(ChildNumber::new(cosigner_index, false)?)?; - // } - //let attrs = private_key.attrs().clone(); - // let (public_key, attrs) = - // Self::derive_public_key(&public_key.public_key, public_key.attrs(), ChildNumber::new(address_type.index(), true)?).await?; //public_key.derive_child(ChildNumber::new(address_type.index(), false)?)?; let (private_key, attrs) = Self::derive_private_key(private_key, attrs, ChildNumber::new(address_type.index(), true)?)?; - // let mut hmac = HmacSha512::new_from_slice(&attrs.chain_code).map_err(Error::Hmac)?; - // hmac.update(&[0]); - // hmac.update(&private_key.to_bytes()); let hmac = Self::create_hmac(&private_key, &attrs, true)?; Ok((private_key, attrs, hmac)) diff --git a/wallet/core/src/derivation/gen0/mod.rs b/wallet/keys/src/derivation/gen0/mod.rs similarity index 90% rename from wallet/core/src/derivation/gen0/mod.rs rename to wallet/keys/src/derivation/gen0/mod.rs index 7c6b74312..d25032626 100644 --- a/wallet/core/src/derivation/gen0/mod.rs +++ b/wallet/keys/src/derivation/gen0/mod.rs @@ -2,4 +2,3 @@ mod hd; pub use hd::{PubkeyDerivationManagerV0, WalletDerivationManagerV0}; -pub mod import; diff --git a/wallet/core/src/derivation/gen1/hd.rs b/wallet/keys/src/derivation/gen1/hd.rs similarity index 97% rename from wallet/core/src/derivation/gen1/hd.rs rename to wallet/keys/src/derivation/gen1/hd.rs index bef09109a..15467c3df 100644 --- a/wallet/core/src/derivation/gen1/hd.rs +++ b/wallet/keys/src/derivation/gen1/hd.rs @@ -10,7 +10,7 @@ use kaspa_bip32::{ use ripemd::Ripemd160; use sha2::{Digest, Sha256}; use std::fmt::Debug; -use wasm_bindgen::prelude::*; +// use wasm_bindgen::prelude::*; fn get_fingerprint(private_key: &K) -> KeyFingerprint where @@ -23,7 +23,7 @@ where } #[derive(Clone)] -#[wasm_bindgen(inspectable)] +// #[wasm_bindgen(inspectable)] pub struct PubkeyDerivationManager { /// Derived public key public_key: secp256k1::PublicKey, @@ -50,24 +50,23 @@ impl PubkeyDerivationManager { pub fn derive_pubkey_range(&self, indexes: std::ops::Range) -> Result> { let list = indexes.map(|index| self.derive_pubkey(index)).collect::>(); - // let keys = join_all(list).into_iter().collect::>>()?; let keys = list.into_iter().collect::>>()?; Ok(keys) } pub fn derive_pubkey(&self, index: u32) -> Result { let (key, _chain_code) = WalletDerivationManager::derive_public_key_child(&self.public_key, index, self.hmac.clone())?; - - // let pubkey = &key.to_bytes()[1..]; - // // - TODO - where should the address prefix come from? - // let address = Address::new(self.address_prefix, self.address_version, pubkey); - Ok(key) } - pub fn create_address(key: &secp256k1::PublicKey, prefix: AddressPrefix, _ecdsa: bool) -> Result
{ - let payload = &key.to_bytes()[1..]; - let address = Address::new(prefix, AddressVersion::PubKey, payload); + pub fn create_address(key: &secp256k1::PublicKey, prefix: AddressPrefix, ecdsa: bool) -> Result
{ + let address = if ecdsa { + let payload = &key.serialize(); + Address::new(prefix, AddressVersion::PubKeyECDSA, payload) + } else { + let payload = &key.x_only_public_key().0.serialize(); + Address::new(prefix, AddressVersion::PubKey, payload) + }; Ok(address) } @@ -97,9 +96,9 @@ impl PubkeyDerivationManager { } } -#[wasm_bindgen] +// #[wasm_bindgen] impl PubkeyDerivationManager { - #[wasm_bindgen(getter, js_name = publicKey)] + // #[wasm_bindgen(getter, js_name = publicKey)] pub fn get_public_key(&self) -> String { self.public_key().to_string(None) } diff --git a/wallet/core/src/derivation/gen1/mod.rs b/wallet/keys/src/derivation/gen1/mod.rs similarity index 90% rename from wallet/core/src/derivation/gen1/mod.rs rename to wallet/keys/src/derivation/gen1/mod.rs index 5853f3673..1822c4d7a 100644 --- a/wallet/core/src/derivation/gen1/mod.rs +++ b/wallet/keys/src/derivation/gen1/mod.rs @@ -1,4 +1,3 @@ /// Derivation management for the Kaspa standard derivation scheme `111111'` mod hd; pub use hd::{PubkeyDerivationManager, WalletDerivationManager}; -pub mod import; diff --git a/wallet/keys/src/derivation/mod.rs b/wallet/keys/src/derivation/mod.rs new file mode 100644 index 000000000..a63201194 --- /dev/null +++ b/wallet/keys/src/derivation/mod.rs @@ -0,0 +1,3 @@ +pub mod gen0; +pub mod gen1; +pub mod traits; diff --git a/wallet/core/src/derivation/traits.rs b/wallet/keys/src/derivation/traits.rs similarity index 100% rename from wallet/core/src/derivation/traits.rs rename to wallet/keys/src/derivation/traits.rs diff --git a/wallet/bip32/src/wasm/derivation_path.rs b/wallet/keys/src/derivation_path.rs similarity index 62% rename from wallet/bip32/src/wasm/derivation_path.rs rename to wallet/keys/src/derivation_path.rs index 1b89eac6d..a4c3efe69 100644 --- a/wallet/bip32/src/wasm/derivation_path.rs +++ b/wallet/keys/src/derivation_path.rs @@ -1,19 +1,19 @@ -use crate::{ChildNumber, Error, Result}; -use std::str::FromStr; -use wasm_bindgen::prelude::*; +use crate::imports::*; use workflow_wasm::prelude::*; -#[derive(Clone)] +/// Key derivation path +/// @category Wallet SDK +#[derive(Clone, CastFromJs)] #[wasm_bindgen] pub struct DerivationPath { - inner: crate::DerivationPath, + inner: kaspa_bip32::DerivationPath, } #[wasm_bindgen] impl DerivationPath { #[wasm_bindgen(constructor)] pub fn new(path: &str) -> Result { - let inner = crate::DerivationPath::from_str(path)?; + let inner = kaspa_bip32::DerivationPath::from_str(path)?; Ok(Self { inner }) } @@ -49,19 +49,22 @@ impl DerivationPath { } } -impl TryFrom for DerivationPath { +impl TryCastFromJs for DerivationPath { type Error = Error; - fn try_from(value: JsValue) -> std::result::Result { - if let Some(path) = value.as_string() { - return Self::new(&path); - } - - Ok(ref_from_abi!(DerivationPath, &value)?) + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + let value = value.as_ref(); + if let Some(path) = value.as_string() { + Ok(DerivationPath::new(&path)?) + } else { + Err(Error::custom("Invalid derivation path")) + } + }) } } -impl From for crate::DerivationPath { - fn from(value: DerivationPath) -> Self { - value.inner +impl<'a> From<&'a DerivationPath> for &'a kaspa_bip32::DerivationPath { + fn from(value: &'a DerivationPath) -> Self { + &value.inner } } diff --git a/wallet/keys/src/error.rs b/wallet/keys/src/error.rs new file mode 100644 index 000000000..7a868b13a --- /dev/null +++ b/wallet/keys/src/error.rs @@ -0,0 +1,117 @@ +//! +//! Error types used by the wallet framework. +//! + +use kaspa_bip32::Error as BIP32Error; +use std::sync::PoisonError; +use thiserror::Error; +use wasm_bindgen::JsValue; +use workflow_core::sendable::*; +use workflow_wasm::jserror::*; +use workflow_wasm::printable::*; + +/// [`Error`](enum@Error) variants emitted by the wallet framework. +#[derive(Debug, Error)] +pub enum Error { + #[error("{0}")] + Custom(String), + + #[error("Bip32 -> {0}")] + BIP32Error(#[from] BIP32Error), + + #[error("Decoding -> {0}")] + Decode(#[from] core::array::TryFromSliceError), + + #[error("Poison error -> {0}")] + PoisonError(String), + + #[error("Secp256k1 -> {0}")] + Secp256k1Error(#[from] secp256k1::Error), + + #[error("{0}")] + JsValue(JsErrorData), + + #[error(transparent)] + WorkflowWasm(#[from] workflow_wasm::error::Error), + + #[error("Serde WASM bindgen -> {0}")] + SerdeWasmBindgen(Sendable), + + #[error("Invalid account type (must be one of: bip32|multisig|legacy")] + InvalidAccountKind, + + #[error("Invalid XPrv (must be a string or an instance of XPrv)")] + InvalidXPrv, + + #[error("Invalid XPub (must be a string or an instance of XPub)")] + InvalidXPub, + + #[error("Invalid PrivateKey (must be a string or an instance of PrivateKey)")] + InvalidPrivateKey, + + #[error("Invalid PublicKey (must be a string or an instance of PrivateKey)")] + InvalidPublicKey, + + #[error("Invalid PublicKey Array (must be string[] or PrivateKey[])")] + InvalidPublicKeyArray, + + #[error(transparent)] + NetworkId(#[from] kaspa_consensus_core::network::NetworkIdError), + + #[error(transparent)] + NetworkType(#[from] kaspa_consensus_core::network::NetworkTypeError), + + #[error("Invalid UTF-8 sequence")] + Utf8(#[from] std::str::Utf8Error), +} + +impl Error { + pub fn custom>(msg: T) -> Self { + Error::Custom(msg.into()) + } +} + +impl From for JsValue { + fn from(value: Error) -> Self { + match value { + Error::JsValue(js_error_data) => js_error_data.into(), + _ => JsValue::from(value.to_string()), + } + } +} + +impl From> for Error { + fn from(err: PoisonError) -> Self { + Self::PoisonError(format!("{err:?}")) + } +} + +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()) + } +} + +impl From for Error { + fn from(err: wasm_bindgen::JsValue) -> Self { + Self::JsValue(err.into()) + } +} + +impl From for Error { + fn from(err: wasm_bindgen::JsError) -> Self { + Self::JsValue(err.into()) + } +} + +impl From for Error { + fn from(err: serde_wasm_bindgen::Error) -> Self { + Self::SerdeWasmBindgen(Sendable(Printable::new(err.into()))) + } +} diff --git a/wallet/keys/src/imports.rs b/wallet/keys/src/imports.rs new file mode 100644 index 000000000..6597aad3c --- /dev/null +++ b/wallet/keys/src/imports.rs @@ -0,0 +1,28 @@ +// //! +// //! This file contains most common imports that +// //! are used internally in this crate. +// //! + +pub use crate::derivation_path::DerivationPath; +pub use crate::error::Error; +pub use crate::privatekey::PrivateKey; +pub use crate::publickey::{PublicKey, PublicKeyArrayT}; +pub use crate::result::Result; +pub use crate::xprv::{XPrv, XPrvT}; +pub use crate::xpub::{XPub, XPubT}; +pub use async_trait::async_trait; +pub use borsh::{BorshDeserialize, BorshSerialize}; +pub use js_sys::Array; +pub use kaspa_addresses::{Address, Version as AddressVersion}; +pub use kaspa_bip32::{ChildNumber, ExtendedPrivateKey, ExtendedPublicKey, SecretKey}; +pub use kaspa_consensus_core::network::NetworkTypeT; +pub use kaspa_utils::hex::*; +pub use kaspa_wasm_core::types::*; +pub use serde::{Deserialize, Serialize}; +pub use std::collections::HashMap; +pub use std::str::FromStr; +pub use std::sync::atomic::{AtomicBool, Ordering}; +pub use std::sync::{Arc, Mutex, MutexGuard}; +pub use wasm_bindgen::prelude::*; +pub use workflow_wasm::convert::*; +pub use zeroize::*; diff --git a/wallet/keys/src/keypair.rs b/wallet/keys/src/keypair.rs new file mode 100644 index 000000000..3bed0152b --- /dev/null +++ b/wallet/keys/src/keypair.rs @@ -0,0 +1,106 @@ +//! +//! [`keypair`](mod@self) module encapsulates [`Keypair`] and [`PrivateKey`]. +//! The [`Keypair`] provides access to the secret and public keys. +//! +//! ```javascript +//! +//! let keypair = Keypair.random(); +//! let privateKey = keypair.privateKey; +//! let publicKey = keypair.publicKey; +//! +//! // to obtain an address from a keypair +//! let address = keypair.toAddress(NetworkType.Mainnnet); +//! +//! // to obtain a keypair from a private key +//! let keypair = privateKey.toKeypair(); +//! +//! ``` +//! + +use crate::imports::*; +use secp256k1::{Secp256k1, XOnlyPublicKey}; +use serde_wasm_bindgen::to_value; + +/// Data structure that contains a secret and public keys. +/// @category Wallet SDK +#[derive(Debug, Clone, CastFromJs)] +#[wasm_bindgen(inspectable)] +pub struct Keypair { + secret_key: secp256k1::SecretKey, + public_key: secp256k1::PublicKey, + xonly_public_key: XOnlyPublicKey, +} + +#[wasm_bindgen] +impl Keypair { + fn new(secret_key: secp256k1::SecretKey, public_key: secp256k1::PublicKey, xonly_public_key: XOnlyPublicKey) -> Self { + Self { secret_key, public_key, xonly_public_key } + } + + /// Get the [`PublicKey`] of this [`Keypair`]. + #[wasm_bindgen(getter = publicKey)] + pub fn get_public_key(&self) -> String { + PublicKey::from(&self.public_key).to_string() + } + + /// Get the [`PrivateKey`] of this [`Keypair`]. + #[wasm_bindgen(getter = privateKey)] + pub fn get_private_key(&self) -> String { + PrivateKey::from(&self.secret_key).to_hex() + } + + /// Get the `XOnlyPublicKey` of this [`Keypair`]. + #[wasm_bindgen(getter = xOnlyPublicKey)] + pub fn get_xonly_public_key(&self) -> JsValue { + to_value(&self.xonly_public_key).unwrap() + } + + /// Get the [`Address`] of this Keypair's [`PublicKey`]. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = keypair.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddress)] + // pub fn to_address(&self, network_type: NetworkType) -> Result
{ + pub fn to_address(&self, network: &NetworkTypeT) -> Result
{ + let payload = &self.xonly_public_key.serialize(); + let address = Address::new(network.try_into()?, AddressVersion::PubKey, payload); + Ok(address) + } + + /// Get `ECDSA` [`Address`] of this Keypair's [`PublicKey`]. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = keypair.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddressECDSA)] + pub fn to_address_ecdsa(&self, network: &NetworkTypeT) -> Result
{ + let payload = &self.public_key.serialize(); + let address = Address::new(network.try_into()?, AddressVersion::PubKeyECDSA, payload); + Ok(address) + } + + /// Create a new random [`Keypair`]. + /// JavaScript: `let keypair = Keypair::random();`. + #[wasm_bindgen] + pub fn random() -> Result { + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng()); + let (xonly_public_key, _) = public_key.x_only_public_key(); + Ok(Keypair::new(secret_key, public_key, xonly_public_key)) + } + + /// Create a new [`Keypair`] from a [`PrivateKey`]. + /// JavaScript: `let privkey = new PrivateKey(hexString); let keypair = privkey.toKeypair();`. + #[wasm_bindgen(js_name = "fromPrivateKey")] + pub fn from_private_key(secret_key: &PrivateKey) -> Result { + let secp = Secp256k1::new(); + let secret_key = secp256k1::SecretKey::from_slice(&secret_key.secret_bytes())?; + let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + let (xonly_public_key, _) = public_key.x_only_public_key(); + Ok(Keypair::new(secret_key, public_key, xonly_public_key)) + } +} + +impl TryCastFromJs for Keypair { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Ok(Self::try_ref_from_js_value_as_cast(value)?) + } +} diff --git a/wallet/keys/src/lib.rs b/wallet/keys/src/lib.rs new file mode 100644 index 000000000..bec8747d0 --- /dev/null +++ b/wallet/keys/src/lib.rs @@ -0,0 +1,15 @@ +pub mod derivation; +pub mod derivation_path; +pub mod error; +mod imports; +pub mod keypair; +pub mod prelude; +pub mod privatekey; +pub mod privkeygen; +pub mod pubkeygen; +pub mod publickey; +pub mod result; +pub mod secret; +pub mod types; +pub mod xprv; +pub mod xpub; diff --git a/wallet/keys/src/prelude.rs b/wallet/keys/src/prelude.rs new file mode 100644 index 000000000..1aed7c535 --- /dev/null +++ b/wallet/keys/src/prelude.rs @@ -0,0 +1,10 @@ +pub use crate::derivation_path::*; +pub use crate::keypair::*; +pub use crate::privatekey::*; +pub use crate::privkeygen::*; +pub use crate::pubkeygen::*; +pub use crate::publickey::*; +pub use crate::secret::*; +pub use crate::types::*; +pub use crate::xprv::*; +pub use crate::xpub::*; diff --git a/wallet/keys/src/privatekey.rs b/wallet/keys/src/privatekey.rs new file mode 100644 index 000000000..825bf395f --- /dev/null +++ b/wallet/keys/src/privatekey.rs @@ -0,0 +1,108 @@ +//! +//! Private Key +//! + +use crate::imports::*; +use crate::keypair::Keypair; +use js_sys::{Array, Uint8Array}; + +/// Data structure that envelops a Private Key. +/// @category Wallet SDK +#[derive(Clone, Debug, CastFromJs)] +#[wasm_bindgen] +pub struct PrivateKey { + inner: secp256k1::SecretKey, +} + +impl PrivateKey { + pub fn secret_bytes(&self) -> [u8; 32] { + self.inner.secret_bytes() + } +} + +impl From<&secp256k1::SecretKey> for PrivateKey { + fn from(value: &secp256k1::SecretKey) -> Self { + Self { inner: *value } + } +} + +impl From<&PrivateKey> for [u8; 32] { + fn from(key: &PrivateKey) -> Self { + key.secret_bytes() + } +} + +#[wasm_bindgen] +impl PrivateKey { + /// Create a new [`PrivateKey`] from a hex-encoded string. + #[wasm_bindgen(constructor)] + pub fn try_new(key: &str) -> Result { + Ok(Self { inner: secp256k1::SecretKey::from_str(key)? }) + } +} + +impl PrivateKey { + pub fn try_from_slice(data: &[u8]) -> Result { + Ok(Self { inner: secp256k1::SecretKey::from_slice(data)? }) + } +} + +#[wasm_bindgen] +impl PrivateKey { + /// Returns the [`PrivateKey`] key encoded as a hex string. + #[wasm_bindgen(js_name = toString)] + pub fn to_hex(&self) -> String { + use kaspa_utils::hex::ToHex; + self.secret_bytes().to_vec().to_hex() + } + + /// Generate a [`Keypair`] from this [`PrivateKey`]. + #[wasm_bindgen(js_name = toKeypair)] + pub fn to_keypair(&self) -> Result { + Keypair::from_private_key(self) + } + + #[wasm_bindgen(js_name = toPublicKey)] + pub fn to_public_key(&self) -> Result { + Ok(PublicKey::from(secp256k1::PublicKey::from_secret_key_global(&self.inner))) + } + + /// Get the [`Address`] of the PublicKey generated from this PrivateKey. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = privateKey.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddress)] + pub fn to_address(&self, network: &NetworkTypeT) -> Result
{ + let public_key = secp256k1::PublicKey::from_secret_key_global(&self.inner); + let (x_only_public_key, _) = public_key.x_only_public_key(); + let payload = x_only_public_key.serialize(); + let address = Address::new(network.try_into()?, AddressVersion::PubKey, &payload); + Ok(address) + } + + /// Get `ECDSA` [`Address`] of the PublicKey generated from this PrivateKey. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = privateKey.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddressECDSA)] + pub fn to_address_ecdsa(&self, network: &NetworkTypeT) -> Result
{ + let public_key = secp256k1::PublicKey::from_secret_key_global(&self.inner); + let payload = public_key.serialize(); + let address = Address::new(network.try_into()?, AddressVersion::PubKeyECDSA, &payload); + Ok(address) + } +} + +impl TryCastFromJs for PrivateKey { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(hex_str) = value.as_ref().as_string() { + Self::try_new(hex_str.as_str()) + } else if Array::is_array(value.as_ref()) { + let array = Uint8Array::new(value.as_ref()); + Self::try_from_slice(array.to_vec().as_slice()) + } else { + Err(Error::InvalidPrivateKey) + } + }) + } +} diff --git a/wallet/core/src/wasm/xprivatekey.rs b/wallet/keys/src/privkeygen.rs similarity index 52% rename from wallet/core/src/wasm/xprivatekey.rs rename to wallet/keys/src/privkeygen.rs index 82a2c3b9b..ff2f3bd8f 100644 --- a/wallet/core/src/wasm/xprivatekey.rs +++ b/wallet/keys/src/privkeygen.rs @@ -1,27 +1,35 @@ use crate::derivation::gen1::WalletDerivationManager; -use crate::result::Result; -use kaspa_bip32::{ChildNumber, ExtendedPrivateKey, SecretKey}; -use kaspa_consensus_wasm::PrivateKey; -use std::str::FromStr; -use wasm_bindgen::prelude::*; +use crate::imports::*; +/// +/// Helper class to generate private keys from an extended private key (XPrv). +/// This class accepts the master Kaspa XPrv string (e.g. `xprv1...`) and generates +/// private keys for the receive and change paths given the pre-set parameters +/// such as account index, multisig purpose and cosigner index. +/// +/// Please note that in Kaspa master private keys use `kprv` prefix. +/// +/// @see {@link PublicKeyGenerator}, {@link XPub}, {@link XPrv}, {@link Mnemonic} +/// @category Wallet SDK +/// #[wasm_bindgen] -pub struct XPrivateKey { +pub struct PrivateKeyGenerator { receive: ExtendedPrivateKey, change: ExtendedPrivateKey, } #[wasm_bindgen] -impl XPrivateKey { +impl PrivateKeyGenerator { #[wasm_bindgen(constructor)] - pub fn new(xprv: &str, is_multisig: bool, account_index: u64, cosigner_index: Option) -> Result { - let xkey = ExtendedPrivateKey::::from_str(xprv)?; - let receive = xkey.clone().derive_path(WalletDerivationManager::build_derivate_path( + pub fn new(xprv: &XPrvT, is_multisig: bool, account_index: u64, cosigner_index: Option) -> Result { + let xprv = XPrv::try_cast_from(xprv)?; + let xprv = xprv.as_ref().inner(); + let receive = xprv.clone().derive_path(&WalletDerivationManager::build_derivate_path( is_multisig, account_index, cosigner_index, Some(kaspa_bip32::AddressType::Receive), )?)?; - let change = xkey.derive_path(WalletDerivationManager::build_derivate_path( + let change = xprv.clone().derive_path(&WalletDerivationManager::build_derivate_path( is_multisig, account_index, cosigner_index, diff --git a/wallet/keys/src/pubkeygen.rs b/wallet/keys/src/pubkeygen.rs new file mode 100644 index 000000000..272e63845 --- /dev/null +++ b/wallet/keys/src/pubkeygen.rs @@ -0,0 +1,219 @@ +use crate::derivation::gen1::WalletDerivationManager; +use crate::derivation::traits::WalletDerivationManagerTrait; +use crate::imports::*; +use kaspa_addresses::AddressArrayT; +use kaspa_consensus_core::network::NetworkType; +// use crate::xprv::XPrv; + +/// +/// Helper class to generate public keys from an extended public key (XPub) +/// that has been derived up to the co-signer index. +/// +/// Please note that in Kaspa master public keys use `kpub` prefix. +/// +/// @see {@link PrivateKeyGenerator}, {@link XPub}, {@link XPrv}, {@link Mnemonic} +/// @category Wallet SDK +/// +#[wasm_bindgen] +pub struct PublicKeyGenerator { + hd_wallet: WalletDerivationManager, +} +#[wasm_bindgen] +impl PublicKeyGenerator { + #[wasm_bindgen(js_name=fromXPub)] + pub fn from_xpub(kpub: XPubT, cosigner_index: Option) -> Result { + let kpub = XPub::try_cast_from(kpub)?; + let xpub = kpub.as_ref().inner(); + let hd_wallet = WalletDerivationManager::from_extended_public_key(xpub.clone(), cosigner_index)?; + Ok(Self { hd_wallet }) + } + + #[wasm_bindgen(js_name=fromMasterXPrv)] + pub fn from_master_xprv( + xprv: &XPrvT, + is_multisig: bool, + account_index: u64, + cosigner_index: Option, + ) -> Result { + let path = WalletDerivationManager::build_derivate_path(is_multisig, account_index, None, None)?; + let xprv = XPrv::try_owned_from(xprv)?.inner().clone().derive_path(&path)?; + let xpub = xprv.public_key(); + let hd_wallet = WalletDerivationManager::from_extended_public_key(xpub, cosigner_index)?; + Ok(Self { hd_wallet }) + } + + // --- + + /// Generate Receive Public Key derivations for a given range. + #[wasm_bindgen(js_name=receivePubkeys)] + pub fn receive_pubkeys(&self, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let pubkeys = self.hd_wallet.receive_pubkey_manager().derive_pubkey_range(start..end)?; + Ok(Array::from_iter(pubkeys.into_iter().map(|pk| JsValue::from(PublicKey::from(pk)))).unchecked_into()) + } + + /// Generate a single Receive Public Key derivation at a given index. + #[wasm_bindgen(js_name=receivePubkey)] + pub fn receive_pubkey(&self, index: u32) -> Result { + Ok(self.hd_wallet.receive_pubkey_manager().derive_pubkey(index)?.into()) + } + + /// Generate a range of Receive Public Key derivations and return them as strings. + #[wasm_bindgen(js_name=receivePubkeysAsStrings)] + pub fn receive_pubkeys_as_strings(&self, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let pubkeys = self.hd_wallet.receive_pubkey_manager().derive_pubkey_range(start..end)?; + Ok(Array::from_iter(pubkeys.into_iter().map(|pk| JsValue::from(PublicKey::from(pk).to_string()))).unchecked_into()) + } + + /// Generate a single Receive Public Key derivation at a given index and return it as a string. + #[wasm_bindgen(js_name=receivePubkeyAsString)] + pub fn receive_pubkey_as_string(&self, index: u32) -> Result { + Ok(self.hd_wallet.receive_pubkey_manager().derive_pubkey(index)?.to_string()) + } + + /// Generate Receive Address derivations for a given range. + #[wasm_bindgen(js_name=receiveAddresses)] + #[allow(non_snake_case)] + pub fn receive_addresses(&self, networkType: &NetworkTypeT, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let network_type = NetworkType::try_from(networkType)?; + let pubkeys = self.hd_wallet.receive_pubkey_manager().derive_pubkey_range(start..end)?; + let addresses = + pubkeys.into_iter().map(|pk| PublicKey::from(pk).to_address(network_type)).collect::>>()?; + Ok(Array::from_iter(addresses.into_iter().map(JsValue::from)).unchecked_into()) + } + + /// Generate a single Receive Address derivation at a given index. + #[wasm_bindgen(js_name=receiveAddress)] + #[allow(non_snake_case)] + pub fn receive_address(&self, networkType: &NetworkTypeT, index: u32) -> Result
{ + PublicKey::from(self.hd_wallet.receive_pubkey_manager().derive_pubkey(index)?).to_address(networkType.try_into()?) + } + + /// Generate a range of Receive Address derivations and return them as strings. + #[wasm_bindgen(js_name=receiveAddressAsStrings)] + #[allow(non_snake_case)] + pub fn receive_addresses_as_strings(&self, networkType: &NetworkTypeT, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let network_type = NetworkType::try_from(networkType)?; + let pubkeys = self.hd_wallet.receive_pubkey_manager().derive_pubkey_range(start..end)?; + let addresses = + pubkeys.into_iter().map(|pk| PublicKey::from(pk).to_address(network_type)).collect::>>()?; + Ok(Array::from_iter(addresses.into_iter().map(String::from).map(JsValue::from)).unchecked_into()) + } + + /// Generate a single Receive Address derivation at a given index and return it as a string. + #[wasm_bindgen(js_name=receiveAddressAsString)] + #[allow(non_snake_case)] + pub fn receive_address_as_string(&self, networkType: &NetworkTypeT, index: u32) -> Result { + Ok(PublicKey::from(self.hd_wallet.receive_pubkey_manager().derive_pubkey(index)?) + .to_address(networkType.try_into()?)? + .to_string()) + } + + // --- + + /// Generate Change Public Key derivations for a given range. + #[wasm_bindgen(js_name=changePubkeys)] + pub fn change_pubkeys(&self, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let pubkeys = self.hd_wallet.change_pubkey_manager().derive_pubkey_range(start..end)?; + Ok(Array::from_iter(pubkeys.into_iter().map(|pk| JsValue::from(PublicKey::from(pk)))).unchecked_into()) + } + + /// Generate a single Change Public Key derivation at a given index. + #[wasm_bindgen(js_name=changePubkey)] + pub fn change_pubkey(&self, index: u32) -> Result { + Ok(self.hd_wallet.change_pubkey_manager().derive_pubkey(index)?.into()) + } + + /// Generate a range of Change Public Key derivations and return them as strings. + #[wasm_bindgen(js_name=changePubkeysAsStrings)] + pub fn change_pubkeys_as_strings(&self, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let pubkeys = self.hd_wallet.change_pubkey_manager().derive_pubkey_range(start..end)?; + Ok(Array::from_iter(pubkeys.into_iter().map(|pk| JsValue::from(PublicKey::from(pk).to_string()))).unchecked_into()) + } + + /// Generate a single Change Public Key derivation at a given index and return it as a string. + #[wasm_bindgen(js_name=changePubkeyAsString)] + pub fn change_pubkey_as_string(&self, index: u32) -> Result { + Ok(self.hd_wallet.change_pubkey_manager().derive_pubkey(index)?.to_string()) + } + + /// Generate Change Address derivations for a given range. + #[wasm_bindgen(js_name=changeAddresses)] + #[allow(non_snake_case)] + pub fn change_addresses(&self, networkType: &NetworkTypeT, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let network_type = NetworkType::try_from(networkType)?; + let pubkeys = self.hd_wallet.change_pubkey_manager().derive_pubkey_range(start..end)?; + let addresses = + pubkeys.into_iter().map(|pk| PublicKey::from(pk).to_address(network_type)).collect::>>()?; + Ok(Array::from_iter(addresses.into_iter().map(JsValue::from)).unchecked_into()) + } + + /// Generate a single Change Address derivation at a given index. + #[wasm_bindgen(js_name=changeAddress)] + #[allow(non_snake_case)] + pub fn change_address(&self, networkType: &NetworkTypeT, index: u32) -> Result
{ + PublicKey::from(self.hd_wallet.change_pubkey_manager().derive_pubkey(index)?).to_address(networkType.try_into()?) + } + + /// Generate a range of Change Address derivations and return them as strings. + #[wasm_bindgen(js_name=changeAddressAsStrings)] + #[allow(non_snake_case)] + pub fn change_addresses_as_strings(&self, networkType: &NetworkTypeT, mut start: u32, mut end: u32) -> Result { + if start > end { + (start, end) = (end, start); + } + let network_type = NetworkType::try_from(networkType)?; + let pubkeys = self.hd_wallet.change_pubkey_manager().derive_pubkey_range(start..end)?; + let addresses = + pubkeys.into_iter().map(|pk| PublicKey::from(pk).to_address(network_type)).collect::>>()?; + Ok(Array::from_iter(addresses.into_iter().map(String::from).map(JsValue::from)).unchecked_into()) + } + + /// Generate a single Change Address derivation at a given index and return it as a string. + #[wasm_bindgen(js_name=changeAddressAsString)] + #[allow(non_snake_case)] + pub fn change_address_as_string(&self, networkType: &NetworkTypeT, index: u32) -> Result { + Ok(PublicKey::from(self.hd_wallet.receive_pubkey_manager().derive_pubkey(index)?) + .to_address(networkType.try_into()?)? + .to_string()) + } + + // #[wasm_bindgen(js_name=changePubkeys)] + // pub fn change_pubkeys(&self, mut start: u32, mut end: u32) -> Result { + // if start > end { + // (start, end) = (end, start); + // } + // let pubkeys = self.hd_wallet.change_pubkey_manager().derive_pubkey_range(start..end)?; + // Ok(Array::from_iter(pubkeys.into_iter().map(|pk| JsValue::from(PublicKey::from(pk)))).unchecked_into()) + // } + + // #[wasm_bindgen(js_name=changePubkey)] + // pub fn change_pubkey(&self, index: u32) -> Result { + // Ok(self.hd_wallet.change_pubkey_manager().derive_pubkey(index)?.into()) + // } + + #[wasm_bindgen(js_name=toString)] + pub fn to_string(&self) -> Result { + Ok(self.hd_wallet.to_string(None).to_string()) + } +} diff --git a/wallet/keys/src/publickey.rs b/wallet/keys/src/publickey.rs new file mode 100644 index 000000000..f262a12f4 --- /dev/null +++ b/wallet/keys/src/publickey.rs @@ -0,0 +1,251 @@ +//! +//! [`keypair`](mod@self) module encapsulates [`Keypair`] and [`PrivateKey`]. +//! The [`Keypair`] provides access to the secret and public keys. +//! +//! ```javascript +//! +//! let keypair = Keypair.random(); +//! let privateKey = keypair.privateKey; +//! let publicKey = keypair.publicKey; +//! +//! // to obtain an address from a keypair +//! let address = keypair.toAddress(NetworkType.Mainnnet); +//! +//! // to obtain a keypair from a private key +//! let keypair = privateKey.toKeypair(); +//! +//! ``` +//! + +use kaspa_consensus_core::network::NetworkType; + +use crate::imports::*; + +/// Data structure that envelopes a PublicKey. +/// Only supports Schnorr-based addresses. +/// @category Wallet SDK +#[derive(Clone, Debug, CastFromJs)] +#[wasm_bindgen(js_name = PublicKey)] +pub struct PublicKey { + #[wasm_bindgen(skip)] + pub xonly_public_key: secp256k1::XOnlyPublicKey, + #[wasm_bindgen(skip)] + pub public_key: Option, +} + +#[wasm_bindgen(js_class = PublicKey)] +impl PublicKey { + /// Create a new [`PublicKey`] from a hex-encoded string. + #[wasm_bindgen(constructor)] + pub fn try_new(key: &str) -> Result { + match secp256k1::PublicKey::from_str(key) { + Ok(public_key) => Ok((&public_key).into()), + Err(_e) => Ok(Self { xonly_public_key: secp256k1::XOnlyPublicKey::from_str(key)?, public_key: None }), + } + } + + #[wasm_bindgen(js_name = "toString")] + pub fn to_string_impl(&self) -> String { + self.public_key.as_ref().map(|pk| pk.to_string()).unwrap_or_else(|| self.xonly_public_key.to_string()) + } + + /// Get the [`Address`] of this PublicKey. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = publicKey.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddress)] + pub fn to_address_js(&self, network: &NetworkTypeT) -> Result
{ + self.to_address(network.try_into()?) + } + + /// Get `ECDSA` [`Address`] of this PublicKey. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = publicKey.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddressECDSA)] + pub fn to_address_ecdsa_js(&self, network: &NetworkTypeT) -> Result
{ + self.to_address_ecdsa(network.try_into()?) + } + + #[wasm_bindgen(js_name = toXOnlyPublicKey)] + pub fn to_x_only_public_key(&self) -> XOnlyPublicKey { + self.xonly_public_key.into() + } +} + +impl PublicKey { + #[inline] + pub fn to_address(&self, network_type: NetworkType) -> Result
{ + let payload = &self.xonly_public_key.serialize(); + let address = Address::new(network_type.into(), AddressVersion::PubKey, payload); + Ok(address) + } + + #[inline] + pub fn to_address_ecdsa(&self, network_type: NetworkType) -> Result
{ + let payload = &self.xonly_public_key.serialize(); + let address = Address::new(network_type.into(), AddressVersion::PubKeyECDSA, payload); + Ok(address) + } +} + +impl std::fmt::Display for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_string_impl()) + } +} + +impl From for secp256k1::XOnlyPublicKey { + fn from(value: PublicKey) -> Self { + value.xonly_public_key + } +} + +impl TryFrom for secp256k1::PublicKey { + type Error = Error; + fn try_from(value: PublicKey) -> std::result::Result { + value.public_key.ok_or(Error::InvalidPublicKey) + } +} + +impl TryFrom<&PublicKey> for secp256k1::PublicKey { + type Error = Error; + fn try_from(value: &PublicKey) -> std::result::Result { + value.public_key.ok_or(Error::InvalidPublicKey) + } +} + +impl From<&secp256k1::PublicKey> for PublicKey { + fn from(public_key: &secp256k1::PublicKey) -> Self { + let (xonly_public_key, _) = public_key.x_only_public_key(); + Self { xonly_public_key, public_key: Some(*public_key) } + } +} + +impl From for PublicKey { + fn from(public_key: secp256k1::PublicKey) -> Self { + let (xonly_public_key, _) = public_key.x_only_public_key(); + Self { xonly_public_key, public_key: Some(public_key) } + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "PublicKey | string")] + pub type PublicKeyT; + + #[wasm_bindgen(extends = Array, typescript_type = "(PublicKey | string)[]")] + pub type PublicKeyArrayT; +} + +impl TryCastFromJs for PublicKey { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + let value = value.as_ref(); + if let Some(hex_str) = value.as_string() { + Ok(PublicKey::try_new(hex_str.as_str())?) + } else { + Err(Error::custom("Invalid PublicKey")) + } + }) + } +} + +impl TryFrom for Vec { + type Error = Error; + fn try_from(value: PublicKeyArrayT) -> Result { + if value.is_array() { + let array = Array::from(&value); + let pubkeys = array.iter().map(PublicKey::try_cast_from).collect::>>()?; + Ok(pubkeys.iter().map(|pk| pk.as_ref().try_into()).collect::>>()?) + } else { + Err(Error::InvalidPublicKeyArray) + } + } +} + +/// +/// Data structure that envelopes a XOnlyPublicKey. +/// +/// XOnlyPublicKey is used as a payload part of the {@link Address}. +/// +/// @see {@link PublicKey} +/// @category Wallet SDK +#[wasm_bindgen] +#[derive(Clone, Debug, CastFromJs)] +pub struct XOnlyPublicKey { + #[wasm_bindgen(skip)] + pub inner: secp256k1::XOnlyPublicKey, +} + +impl XOnlyPublicKey { + pub fn new(inner: secp256k1::XOnlyPublicKey) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl XOnlyPublicKey { + #[wasm_bindgen(constructor)] + pub fn try_new(key: &str) -> Result { + Ok(secp256k1::XOnlyPublicKey::from_str(key)?.into()) + } + + #[wasm_bindgen(js_name = "toString")] + pub fn to_string_impl(&self) -> String { + self.inner.to_string() + } + + /// Get the [`Address`] of this XOnlyPublicKey. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = xOnlyPublicKey.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddress)] + pub fn to_address(&self, network: &NetworkTypeT) -> Result
{ + let payload = &self.inner.serialize(); + let address = Address::new(network.try_into()?, AddressVersion::PubKey, payload); + Ok(address) + } + + /// Get `ECDSA` [`Address`] of this XOnlyPublicKey. + /// Receives a [`NetworkType`] to determine the prefix of the address. + /// JavaScript: `let address = xOnlyPublicKey.toAddress(NetworkType.MAINNET);`. + #[wasm_bindgen(js_name = toAddressECDSA)] + pub fn to_address_ecdsa(&self, network: &NetworkTypeT) -> Result
{ + let payload = &self.inner.serialize(); + let address = Address::new(network.try_into()?, AddressVersion::PubKeyECDSA, payload); + Ok(address) + } + + #[wasm_bindgen(js_name = fromAddress)] + pub fn from_address(address: &Address) -> Result { + Ok(secp256k1::XOnlyPublicKey::from_slice(&address.payload)?.into()) + } +} + +impl std::fmt::Display for XOnlyPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_string_impl()) + } +} + +impl From for XOnlyPublicKey { + fn from(inner: secp256k1::XOnlyPublicKey) -> Self { + Self { inner } + } +} + +impl From for secp256k1::XOnlyPublicKey { + fn from(xonly_public_key: XOnlyPublicKey) -> Self { + xonly_public_key.inner + } +} + +impl TryFrom for XOnlyPublicKey { + type Error = Error; + fn try_from(js_value: JsValue) -> std::result::Result { + if let Some(hex_str) = js_value.as_string() { + Ok(secp256k1::XOnlyPublicKey::from_str(hex_str.as_str())?.into()) + } else { + Ok(XOnlyPublicKey::try_ref_from_js_value(js_value.as_ref())?.clone()) + } + } +} diff --git a/wallet/keys/src/result.rs b/wallet/keys/src/result.rs new file mode 100644 index 000000000..da660af6b --- /dev/null +++ b/wallet/keys/src/result.rs @@ -0,0 +1,6 @@ +//! +//! [`Result`] type alias bound to the framework [`Error`](crate::error::Error) enum. +//! + +/// [`Result`] type alias bound to the framework [`Error`](crate::error::Error) enum. +pub type Result = std::result::Result; diff --git a/wallet/core/src/secret.rs b/wallet/keys/src/secret.rs similarity index 91% rename from wallet/core/src/secret.rs rename to wallet/keys/src/secret.rs index 7a83c85a7..99d94f5be 100644 --- a/wallet/core/src/secret.rs +++ b/wallet/keys/src/secret.rs @@ -2,9 +2,7 @@ //! Secret container for sensitive data. Performs zeroization on drop. //! -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; -use zeroize::Zeroize; +use crate::imports::*; /// Secret container for sensitive data. Performs memory zeroization on drop. #[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -14,6 +12,10 @@ impl Secret { pub fn new(data: Vec) -> Self { Self(data) } + + pub fn as_str(&self) -> Result<&str> { + Ok(std::str::from_utf8(&self.0)?) + } } impl AsRef<[u8]> for Secret { diff --git a/wallet/core/src/types.rs b/wallet/keys/src/types.rs similarity index 100% rename from wallet/core/src/types.rs rename to wallet/keys/src/types.rs diff --git a/wallet/keys/src/xprv.rs b/wallet/keys/src/xprv.rs new file mode 100644 index 000000000..bc4b681cf --- /dev/null +++ b/wallet/keys/src/xprv.rs @@ -0,0 +1,98 @@ +use crate::imports::*; + +/// +/// Extended private key (XPrv). +/// +/// This class allows accepts a master seed and provides +/// functions for derivation of dependent child private keys. +/// +/// Please note that Kaspa extended private keys use `kprv` prefix. +/// +/// @see {@link PrivateKeyGenerator}, {@link PublicKeyGenerator}, {@link XPub}, {@link Mnemonic} +/// @category Wallet SDK +/// + +#[derive(Clone, CastFromJs)] +#[wasm_bindgen] +pub struct XPrv { + inner: ExtendedPrivateKey, +} + +impl XPrv { + pub fn inner(&self) -> &ExtendedPrivateKey { + &self.inner + } +} + +#[wasm_bindgen] +impl XPrv { + #[wasm_bindgen(constructor)] + pub fn try_new(seed: HexString) -> Result { + let seed_bytes = Vec::::from_hex(String::try_from(seed)?.as_str()).map_err(|_| Error::custom("Invalid seed"))?; + + let inner = ExtendedPrivateKey::::new(seed_bytes)?; + Ok(Self { inner }) + } + + /// Create {@link XPrv} from `xprvxxxx..` string + #[wasm_bindgen(js_name=fromXPrv)] + pub fn from_xprv_str(xprv: String) -> Result { + Ok(Self { inner: ExtendedPrivateKey::::from_str(&xprv)? }) + } + + #[wasm_bindgen(js_name=deriveChild)] + pub fn derive_child(&self, chile_number: u32, hardened: Option) -> Result { + let chile_number = ChildNumber::new(chile_number, hardened.unwrap_or(false))?; + let inner = self.inner.derive_child(chile_number)?; + Ok(Self { inner }) + } + + #[wasm_bindgen(js_name=derivePath)] + pub fn derive_path(&self, path: &JsValue) -> Result { + let path = DerivationPath::try_cast_from(path)?; + let inner = self.inner.clone().derive_path(path.as_ref().into())?; + Ok(Self { inner }) + } + + #[wasm_bindgen(js_name = intoString)] + pub fn into_string(&self, prefix: &str) -> Result { + let str = self.inner.to_extended_key(prefix.try_into()?).to_string(); + Ok(str) + } + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> Result { + let str = self.inner.to_extended_key("kprv".try_into()?).to_string(); + Ok(str) + } + + #[wasm_bindgen(js_name = toXPub)] + pub fn to_xpub(&self) -> Result { + let public_key = self.inner.public_key(); + Ok(public_key.into()) + } +} + +impl<'a> From<&'a XPrv> for &'a ExtendedPrivateKey { + fn from(xprv: &'a XPrv) -> Self { + &xprv.inner + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "XPrv | string")] + pub type XPrvT; +} + +impl TryCastFromJs for XPrv { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(xprv) = value.as_ref().as_string() { + Ok(XPrv::from_xprv_str(xprv)?) + } else { + Err(Error::InvalidXPrv) + } + }) + } +} diff --git a/wallet/keys/src/xpub.rs b/wallet/keys/src/xpub.rs new file mode 100644 index 000000000..ded247edf --- /dev/null +++ b/wallet/keys/src/xpub.rs @@ -0,0 +1,83 @@ +use crate::imports::*; + +/// +/// Extended public key (XPub). +/// +/// This class allows accepts another XPub and and provides +/// functions for derivation of dependent child public keys. +/// +/// Please note that Kaspa extended public keys use `kpub` prefix. +/// +/// @see {@link PrivateKeyGenerator}, {@link PublicKeyGenerator}, {@link XPrv}, {@link Mnemonic} +/// @category Wallet SDK +/// +#[derive(Clone, CastFromJs)] +#[wasm_bindgen] +pub struct XPub { + inner: ExtendedPublicKey, +} + +impl XPub { + pub fn inner(&self) -> &ExtendedPublicKey { + &self.inner + } +} + +#[wasm_bindgen] +impl XPub { + #[wasm_bindgen(constructor)] + pub fn try_new(xpub: &str) -> Result { + let inner = ExtendedPublicKey::::from_str(xpub)?; + Ok(Self { inner }) + } + + #[wasm_bindgen(js_name=deriveChild)] + pub fn derive_child(&self, chile_number: u32, hardened: Option) -> Result { + let chile_number = ChildNumber::new(chile_number, hardened.unwrap_or(false))?; + let inner = self.inner.derive_child(chile_number)?; + Ok(Self { inner }) + } + + #[wasm_bindgen(js_name=derivePath)] + pub fn derive_path(&self, path: &JsValue) -> Result { + let path = DerivationPath::try_cast_from(path)?; + let inner = self.inner.clone().derive_path(path.as_ref().into())?; + Ok(Self { inner }) + } + + //#[wasm_bindgen(js_name = toString)] + #[wasm_bindgen(js_name = intoString)] + pub fn to_str(&self, prefix: &str) -> Result { + Ok(self.inner.to_string(Some(prefix.try_into()?))) + } + + #[wasm_bindgen(js_name = toPublicKey)] + pub fn public_key(&self) -> PublicKey { + self.inner.public_key().into() + } +} + +impl From> for XPub { + fn from(inner: ExtendedPublicKey) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "XPub | string")] + pub type XPubT; +} + +impl TryCastFromJs for XPub { + type Error = Error; + fn try_cast_from(value: impl AsRef) -> Result, Self::Error> { + Self::resolve(&value, || { + if let Some(xpub) = value.as_ref().as_string() { + Ok(XPub::try_new(xpub.as_str())?) + } else { + Err(Error::InvalidXPub) + } + }) + } +} diff --git a/wallet/macros/src/handler.rs b/wallet/macros/src/handler.rs index 9fb6b0f1c..961fbad3f 100644 --- a/wallet/macros/src/handler.rs +++ b/wallet/macros/src/handler.rs @@ -2,10 +2,12 @@ use convert_case::{Case, Casing}; // use proc_macro::Literal; use proc_macro2::{Ident, Literal, Span}; use quote::ToTokens; +use syn::Attribute; use syn::{Error, Expr, ExprArray, Result}; use xxhash_rust::xxh3::xxh3_64; use xxhash_rust::xxh32::xxh32; +#[derive(Debug)] pub struct Handler { pub name: String, pub hash_32: Literal, @@ -17,6 +19,11 @@ pub struct Handler { pub fn_camel: Ident, pub request_type: Ident, pub response_type: Ident, + pub typename: Ident, + pub ts_request_type: Ident, + pub ts_response_type: Ident, + pub ts_custom_section_ident: Ident, + pub docs: Vec, } impl Handler { @@ -25,7 +32,10 @@ impl Handler { } pub fn new_with_args(handler: &Expr, fn_suffix: Option<&str>) -> Handler { - let name = handler.to_token_stream().to_string(); + let (name, docs) = match handler { + syn::Expr::Path(expr_path) => (expr_path.path.to_token_stream().to_string(), expr_path.attrs.clone()), + _ => (handler.to_token_stream().to_string(), vec![]), + }; 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()); @@ -35,7 +45,27 @@ impl Handler { 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 } + let typename = Ident::new(&name.to_string(), Span::call_site()); + let ts_request_type = Ident::new(&format!("I{name}Request"), Span::call_site()); + let ts_response_type = Ident::new(&format!("I{name}Response"), Span::call_site()); + let ts_custom_section_ident = Ident::new(&format!("TS_{}", name.to_uppercase()), Span::call_site()); + Handler { + name, + hash_32, + hash_64, + ident, + fn_call, + fn_with_suffix, + fn_no_suffix, + fn_camel, + request_type, + response_type, + typename, + ts_request_type, + ts_response_type, + ts_custom_section_ident, + docs, + } } } diff --git a/wallet/macros/src/lib.rs b/wallet/macros/src/lib.rs index 4434b4cc1..750738902 100644 --- a/wallet/macros/src/lib.rs +++ b/wallet/macros/src/lib.rs @@ -15,11 +15,17 @@ pub fn build_wallet_server_transport_interface(input: TokenStream) -> TokenStrea 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 declare_wasm_handlers(input: TokenStream) -> TokenStream { + wallet::wasm::declare_wasm_handlers(input) +} + +#[proc_macro] +#[proc_macro_error] +pub fn declare_typescript_wasm_interface(input: TokenStream) -> TokenStream { + wallet::wasm::declare_typescript_wasm_interface(input) +} // #[proc_macro] // #[proc_macro_error] diff --git a/wallet/macros/src/wallet/client.rs b/wallet/macros/src/wallet/client.rs index 440c163a4..8bbc51b8a 100644 --- a/wallet/macros/src/wallet/client.rs +++ b/wallet/macros/src/wallet/client.rs @@ -59,17 +59,17 @@ impl ToTokens for RpcTable { 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?)?) + match __self.codec { + Codec::Borsh(ref codec) => { + Ok(#response_type::try_from_slice(&codec.call(op, request.try_to_vec()?).await?)?) }, - Transport::Serde(ref transport) => { + Codec::Serde(ref codec) => { let request = serde_json::to_string(&request)?; - let response = transport.call(#ident, request.as_str()).await?; + let response = codec.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?)?) + // Ok(#response_type::try_from_slice(&__self.codec.call(op, &request.try_to_vec()?).await?)?) }; #[allow(unreachable_code)] __ret diff --git a/wallet/macros/src/wallet/mod.rs b/wallet/macros/src/wallet/mod.rs index b3ebc3984..1a15b0664 100644 --- a/wallet/macros/src/wallet/mod.rs +++ b/wallet/macros/src/wallet/mod.rs @@ -1,4 +1,3 @@ pub mod client; pub mod server; -// TODO @aspect (placeholder for wasm bindings) -// pub mod wasm; +pub mod wasm; diff --git a/wallet/macros/src/wallet/server.rs b/wallet/macros/src/wallet/server.rs index 6a25ffb15..a1e7bf13e 100644 --- a/wallet/macros/src/wallet/server.rs +++ b/wallet/macros/src/wallet/server.rs @@ -38,14 +38,14 @@ impl ToTokens for RpcTable { targets_borsh.push(quote! { #hash_64 => { - Ok(self.wallet_api.clone().#fn_call(#request_type::try_from_slice(&request)?).await?.try_to_vec()?) + Ok(self.wallet_api().#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?; + let response = self.wallet_api().#fn_call(request).await?; Ok(serde_json::to_string(&response)?) } }); diff --git a/wallet/macros/src/wallet/wasm.rs b/wallet/macros/src/wallet/wasm.rs index 89802ad87..3fd71d71c 100644 --- a/wallet/macros/src/wallet/wasm.rs +++ b/wallet/macros/src/wallet/wasm.rs @@ -1,111 +1,113 @@ use crate::handler::*; use convert_case::{Case, Casing}; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Literal, 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, + Error, Expr, ExprArray, ExprLit, Lit, Result, Token, }; #[derive(Debug)] -struct RpcHandlers { - handlers_no_args: ExprArray, - handlers_with_args: ExprArray, +struct TsInterface { + handler: Handler, + alias: Literal, + declaration: String, } -impl Parse for RpcHandlers { +impl Parse for TsInterface { fn parse(input: ParseStream) -> Result { let parsed = Punctuated::::parse_terminated(input).unwrap(); - if parsed.len() != 2 { + + if parsed.len() == 2 { + let mut iter = parsed.iter(); + let handler = Handler::new(iter.next().unwrap()); + let alias = Literal::string(&handler.name); + let declaration = extract_literal(&iter.next().unwrap().clone())?; + Ok(TsInterface { handler, alias, declaration }) + } else if parsed.len() == 3 { + let mut iter = parsed.iter(); + let handler = Handler::new(iter.next().unwrap()); + let alias = match iter.next().unwrap().clone() { + Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => Literal::string(&lit_str.value()), + _ => return Err(Error::new_spanned(parsed, "type spec must be a string literal".to_string())), + }; + let declaration = extract_literal(&iter.next().unwrap().clone())?; + Ok(TsInterface { handler, alias, declaration }) + } else { return Err(Error::new_spanned( parsed, - "usage: build_wrpc_wasm_bindgen_interface!([fn no args, ..],[fn with args, ..])".to_string(), + "usage: declare_wasm_interface!(typescript_type, [alias], typescript declaration)".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 { +impl ToTokens for TsInterface { 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); + let Self { handler, alias, declaration } = self; + let Handler { name, typename, ts_custom_section_ident, .. } = handler; + + let declaration = if name.ends_with("Request") { + let method = (&name.trim_end_matches("Request")[1..]).to_case(Case::Camel); + insert_typedoc( + declaration, + &format!( + r#" + Argument interface for the {{@link Wallet.{method}}} method. + "# + ), + ) + } else if name.ends_with("Response") { + let method = (&name.trim_end_matches("Response")[1..]).to_case(Case::Camel); + insert_typedoc( + declaration, + &format!( + r#" + Return interface for the {{@link Wallet.{method}}} method. + "# + ), + ) + } else { + declaration.to_owned() + }; - 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(typescript_custom_section)] + const #ts_custom_section_ident: &'static str = #declaration; - quote! { #[wasm_bindgen] - impl RpcClient { - #(#targets_no_args)* - #(#targets_with_args)* + extern "C" { + #[wasm_bindgen(extends = js_sys::Object, typescript_type = #alias)] + #[derive(Default)] + pub type #typename; } + + } .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(); +pub fn declare_typescript_wasm_interface(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let declaration = parse_macro_input!(input as TsInterface); + let ts = declaration.to_token_stream(); // println!("MACRO: {}", ts.to_string()); ts.into() } -// ##################################################################### - #[derive(Debug)] -struct RpcSubscriptions { +struct ApiHandlers { handlers: ExprArray, } -impl Parse for RpcSubscriptions { +impl Parse for ApiHandlers { fn parse(input: ParseStream) -> Result { let parsed = Punctuated::::parse_terminated(input).unwrap(); + if parsed.len() != 1 { return Err(Error::new_spanned( parsed, @@ -116,47 +118,35 @@ impl Parse for RpcSubscriptions { let mut iter = parsed.iter(); let handlers = get_handlers(iter.next().unwrap().clone())?; - Ok(RpcSubscriptions { handlers }) + Ok(ApiHandlers { handlers }) } } -impl ToTokens for RpcSubscriptions { +impl ToTokens for ApiHandlers { 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()); - + let Handler { fn_call, fn_camel, fn_no_suffix, request_type, ts_request_type, ts_response_type, docs, .. } = + Handler::new(handler); + let links = format! {"@see {{@link {ts_request_type}}} {{@link {ts_response_type}}}"}; + let throws = "@throws `string` in case of an error."; 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(()) + #(#docs)* + #[doc=#links] + #[doc=#throws] + #[wasm_bindgen(js_name = #fn_camel)] + pub async fn #fn_no_suffix(&self, request : #ts_request_type) -> Result<#ts_response_type> { + let request = #request_type::try_from(request)?; + let response = self.wallet().clone().#fn_call(request).await?; + #ts_response_type::try_from(response) } }); } - quote! { #[wasm_bindgen] - impl RpcClient { + impl Wallet { #(#targets)* } } @@ -164,9 +154,53 @@ impl ToTokens for RpcSubscriptions { } } -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()); +pub fn declare_wasm_handlers(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let declaration = parse_macro_input!(input as ApiHandlers); + let ts = declaration.to_token_stream(); + // println!("MACRO: {}", ts); ts.into() } + +fn extract_literal(expr: &Expr) -> Result { + match expr { + Expr::Lit(expr_lit) => { + if let Lit::Str(lit_str) = &expr_lit.lit { + Ok(lit_str.value()) + } else { + Err(Error::new_spanned(expr, "argument must be a string literal".to_string())) + } + } + _ => Err(Error::new_spanned(expr, "argument must be a string literal".to_string())), + } +} + +fn insert_typedoc(text: &str, insertion: &str) -> String { + if let Some(mut index) = text.find("/**") { + index += 3; + let insertion = insertion + .split('\n') + .filter_map(|line| (!line.trim().is_empty()).then_some(format!("\n\t* {}", line.trim()))) + .collect::(); + let mut result = String::with_capacity(text.len() + insertion.len()); + result.push_str(&text[..index]); + result.push_str(&insertion); + result.push_str(&text[index..]); + + let lines = result + .split('\n') + .map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with("/**") || trimmed.starts_with('*') { + trimmed + } else { + line + } + }) + .collect::>() + .join("\n"); + + lines + } else { + text.to_string() + } +} diff --git a/wasm/.gitignore b/wasm/.gitignore index f62ba336d..00f7991fd 100644 --- a/wasm/.gitignore +++ b/wasm/.gitignore @@ -1,11 +1,7 @@ /target Cargo.lock analyzer-target -/web/node_modules +/web/* package-lock.json -/nodejs/kaspa-wallet -/nodejs/node_modules -/nodejs/package-lock.json -/setup -/docs - +/nodejs/* +/docs/* \ No newline at end of file diff --git a/wasm/CHANGELOG.md b/wasm/CHANGELOG.md index 0967d2b25..5882d9cbf 100644 --- a/wasm/CHANGELOG.md +++ b/wasm/CHANGELOG.md @@ -1,4 +1,96 @@ +Latest online documentation available at: https://kaspa.aspectron.org/docs/ +### Release 2024-04-27 + - IAccountsCreateRequest interface simplified by flattering it and now it is union for future expansion for multisig etc. + - IWalletEvent interface updated for Events with TransactionRecord + - WIP: wallet api example under wallet/wallet.js + - Bug fixes: wallet.ensure_default_account, ECDSA address creation methods +### Release 2024-04-17 -- swapped kaspa_consensus::tx::UtxoEntry with kaspa_wallet_core::UtxoEntry +- Rename RPC "open" and "close" events to "connect" and "disconnect", TypeScript `RpcEventType.Open` and `RpcEventType.Close` enums to `RpcEventType.Connect` and `RpcEventType.Disconnect` (the renaming is done to prevent confusion in other layers of the WASM SDK where "open" and "close" event names represent the Wallet open state). +- `RpcClient.open` boolean state getter renamed to `RpcClient.connected` +- Fix examples missed during `publicKey()->toPublicKey()` rename. +### Release 2024-04-17 + +- Transaction::addresses() returns a list of unique addresses used by transaction inputs +- PendingTransaction::addresses change from getter to a function +- Address::validate(string) static helper to test address validity + +### Release 2024-04-14 + +- Merge with Rusty Kaspa master (0.13.6) + +### Release 2024-04-13 + +- Change `signTransaction()` to accept `Transaction` instead of `SignableTransaction` +- Remove `SignableTransaction` from the SDK (as `Transaction` now provides all signing functionality) +- Fix a bug in `TransactionRecord` that was returning incorrect getter for `record.data` +- Added `Transaction::addresses()` that returns address list for all UTXOs associated with transaction inputs. +- Fix declarations of events (RpcClient,UtxoProcessor) that do not carry any event data (event data is now declared as `undefined`) + +### Release 2024-03-31 + +- Rename `kaspa-beacon` app to `kaspa-resolver` +- Change RpcClient, UtxoProcessor and Wallet event handlers in typescript to receive typed event data +- UtxoProcessor and Wallet event handlers now deliver TransactionRecord events (Discovery, Pending, etc.) +as Rust or WASM objects, allowing user to call `hasAddress(
)` on the received `event.data.record` object. + +### Release 2024-03-19 + +- Fix type checks when passing arrays to transaction `Generator` entries. + +### Release 2024-03-14 + +- Introduce IWASM32BindingsConfig for configuration of class naming when using WASM32 bindings. +- Introduce serializeToJSON for `PendingTransaction` class (deserializable with `Transaction` class). +- Introduce serializeToJSON and deserializeFromJSON methods for `Transaction` class. + +### Release 2024-03-11 + +- Fix `requestAnimationFrame` use in chrome extension environment. +- Add rejection in `Generator` when `priorityFee` is `undefined` while outputs are present. +- Introduce `CryptoBox` class for encryption/decryption of data using public/private keys. +- sha256x and other hash functions now have two variants sha256FromBinary and sha256FromText +- Changed `XPub.publicKey()` to `XPub.toPublicKey()` +- Most functions returning key strings now return `PrivateKey` or `PublicKey`; this allows function chaining `xpub.deriveChild(0).toPublicKey().toAddress(networkId).toString()` +- `PrivateKey` now has `toPublicKey()`, `toAddress()`, `toAddressECDSA()` methods +- Introduce `XOnlyPublicKey` which can be obtained from `PublicKey`: `xpub.toXOnlyPublicKey()` and `xpub.toXOnlyPublicKey().toAddress(networkId)`. + +### Release 2024-02-26 + +- Add `UtxoProcessor.start()/stop()` methods for explicit start/stop of the `UtxoProcessor` event processing. +- Remove `async` markers from UtxoProcessor and UtxoContext constructors. +- Add `UtxoProcessor.setNetworkId()` method to change the network ID for existing `UtxoProcessor` (`UtxoProcessor` must be stopped before changing the network id). +- Add `UtxoProcessor.networkId` property to get the current network ID. +- Add `UtxoContext.matureLength()` and `matureRange(from,to)` for access to mature UTXO entries. + + +### Release 2024-02-25 + +#### Event Listener API updates +- Event Listener API has been refactored to mimic DOM standard (similar to `addEventListener` / `removeEventListener` available in the browser, but with additional features) +- replace `RpcClient.notify()` with `RpcClient.addEventListener()` / `RpcClient.removeEventListener()` +- `addEventListener()` calls have been standardized between RPC, UtxoProcessor, Wallet +- You can now register multiple listeners for the same event type and unregister them individually +- A single registration can accept an array of events to listen to e.g. `["open", "close"]` + +#### Other updates +- RpcClient events now support `open`, `close` events to signal the RPC connection state +- RPC events now contain `type` and `data` fields (instead of listeners receiving 2 arguments) +- Rename client-side `Beacon` class to `Resolver` +- ITransactionRecord properties now have appropriate interfaces +- ITransactionData serialization fields have changed from (`transaction = {}` to `data = {}`) + +### Release 2024-02-19 + +- Fix large RPC response deserialization errors in NodeJS caused by the default WebSocket frame size limit. +- Fix event processing in UtxoContext +- Renamed `XPrivateKey` to `PrivateKeyGenerator` and `XPublicKey` to `PublicKeyGenerator` +- Simplify conversion between different key types (`XPrv->Keypair`, `XPrv->XPub->Pubkey`, etc) +- Introduced `Beacon` class that provides connectivity to the community-operated public node infrastructure (backed by `kaspa-beacon` load balancer & node status monitor) +- Created TypeScript type definitions across the entire SDK and refactored `RpcClient` class (as well as many other components) to use TypeScript interfaces +- Changed documentation structure to use `typedoc` available as a part of redistributables or online at https://kaspa.aspectron.org/docs/ +- Project-wide documentation updates +- Additional self-contained Web Browser examples +- Modified the structure of WASM32 SDK release to include all variants of libraries (both release and dev builds), examples and documentation in a single package. diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 6b4e32258..78a747e19 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -6,14 +6,16 @@ version.workspace = true edition.workspace = true authors.workspace = true include.workspace = true +repository.workspace = true # wasm-pack requires this value to be a string # (it can not be derived from the workspace) -license = "MIT/Apache-2.0" +license = "ISC" [lib] crate-type = ["cdylib"] [dependencies] +cfg-if.workspace = true js-sys.workspace = true kaspa-addresses.workspace = true kaspa-consensus-core.workspace = true @@ -23,16 +25,34 @@ kaspa-math.workspace = true kaspa-pow.workspace = true kaspa-rpc-core.workspace = true kaspa-utils.workspace = true +kaspa-wasm-core.workspace = true kaspa-wallet-core.workspace = true +kaspa-wallet-keys.workspace = true kaspa-wrpc-client.workspace = true +kaspa-wrpc-wasm.workspace = true num.workspace = true wasm-bindgen.workspace = true workflow-log.workspace = true +workflow-core.workspace = true +workflow-wasm.workspace = true [features] -wallet = [] -full = ["wallet"] -default = [] +wasm32-sdk = [ + "kaspa-wallet-core/wasm32-sdk", + "kaspa-pow/wasm32-sdk", +] +wasm32-core = [ + "kaspa-wallet-core/wasm32-core", + "kaspa-pow/wasm32-sdk", +] +wasm32-rpc = [ + "kaspa-consensus-core/wasm32-sdk", + "kaspa-consensus-wasm/wasm32-sdk", + "kaspa-wrpc-client/wasm32-sdk", + "kaspa-wrpc-wasm/wasm32-sdk", +] +wasm32-keygen = [] +default = [] [package.metadata.docs.rs] targets = ["wasm32-unknown-unknown"] diff --git a/wasm/LICENSE b/wasm/LICENSE new file mode 100644 index 000000000..b66757abc --- /dev/null +++ b/wasm/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2022-2024 Kaspa developers + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/wasm/README.md b/wasm/README.md index e9d2900b8..42700e35d 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -1,5 +1,5 @@ -## `kaspa-wasm` WASM32 bindings for Kaspa +## WASM32 bindings for Rusty Kaspa SDK [github](https://github.com/kaspanet/rusty-kaspa/tree/master/wasm) [crates.io](https://crates.io/crates/kaspa-wasm) @@ -7,25 +7,114 @@ license Rusty-Kaspa WASM32 bindings offer direct integration of Rust code and Rusty-Kaspa -codebase within JavaScript environments such as Node.js and Web Browsers. +codebase within JavaScript and TypeScript environments such as Node.js and Web Browsers. ## Documentation - [**integrating with Kaspa** guide](https://kaspa.aspectron.org/) -- [**Rustdoc** documentation](https://docs.rs/kaspa-wasm/latest/kaspa-wasm) -- [**JSDoc** documentation](https://kaspa.aspectron.org/jsdoc/) +- [**Rust** documentation](https://docs.rs/kaspa-wasm/latest/kaspa_wasm/index.html) +- [**TypeScript** documentation](https://kaspa.aspectron.org/docs/) Please note that while WASM directly binds JavaScript and Rust resources, their names on JavaScript side are different from their name in Rust as they conform to the 'camelCase' convention in JavaScript and -to the 'snake_case' convention in Rust. +to the 'snake_case' convention in Rust. + +The WASM32 bindings can be used in both TypeScript and JavaScript environments, where in JavaScript +types will not be constrained by TypeScript type definitions. ## Interfaces -The APIs are currently separated into the following groups: +The SDK is currently separated into the following top-level categories: + +- **RPC API** — RPC API for the Kaspa node using WebSockets. +- **Wallet SDK** — Bindings for primitives related to key management and transactions. +- **Wallet API** — API for the Rusty Kaspa Wallet framework. + +## WASM32 SDK release packages + +The SDK is built as 4 packages for Web Browsers as follows: +- KeyGen - Key & Address Generation only +- RPC - RPC only +- Core - RPC + Key & Address Generation + Wallet SDK +- Full - Full SDK + Integrated Wallet +For NodeJS, the SDK is built as a single package containing all features. + +## SDK folder structure + +The following is a brief overview of the SDK folder structure (as available in the release): + +- `web/kaspa` - **full** Rusty Kaspa WASM32 SDK bindings for use in web browsers. +- `web/kaspa-rpc` - only the RPC bindings for use in web browsers (reduced WASM binary size). +- `nodejs/kaspa` - **full** Rusty Kaspa WASM32 SDK bindings for use with NodeJS. +- `docs` - Rusty Kaspa WASM32 SDK documentation. +- `examples` folders contain examples for NodeJS and web browsers. +- `examples/data` - folder user by examples for configuration and wallet data storage. +- `examples/javascript` - JavaScript examples. +- `examples/javascript/general` - General SDK examples (keys & derivation, addresses, encryption, etc.). +- `examples/javascript/transactions` - Creating, sending and receiving transactions. +- `examples/javascript/wallet` - Interfacing with the Rusty Kaspa Wallet framework. +- `examples/typescript` - TypeScript examples. + +If you are using JavaScript and Visual Studio Code, it is highly recommended you replicate +the `jsconfig.json` configuration file as is done in the SDK examples. This file allows +Visual Studio to provide TypeScript-like code completion, type checking and documentation. + +Included documentation in the release can be accessed by loading the `docs/kaspa/index.html` +file in a web browser. -- **Transaction API** — Bindings for primitives related to transactions. -- **RPC API** — [RPC interface bindings](https://docs.rs/kaspa-wasm/latest/kaspa-wasm/rpc) for the Kaspa node using WebSocket (wRPC) connections. -- **Wallet API** — API for async core wallet processing tasks. +## Building from Source + +To build the WASM32 SDK from source, you need to have the Rust environment installed. To do that, +follow instructions in the [Rusty Kaspa README](https://github.com/kaspanet/rusty-kaspa). + +Once you have Rust installed, you can build the WASM32 SDK as follows: + +- `./build-release` - build the release version of the WASM32 SDK + Docs. The release version also contains `debug` builds of the libraries. +- `./build-web` - build the web package (ES6 module) +- `./build-node` - build the NodeJS package (CommonJS module) +- `./build-docs` - runs `build-web` and then generates TypeDoc documentation from the resulting build. + +Please note that to build from source, you need to have TypeDoc installed globally via `npm install -g typedoc` (see below). + +## Running Web examples + +**IMPORTANT:** To view web examples, you need to serve them from a local web server and +serve them from the root of the SDK folder (`kaspa-wasm32-sdk` if using a redistributable or +`rusty-kaspa/wasm` if building from source). This is because examples use relative paths. +WASM32 currently can not be loaded using the `file://` protocol. + +You can use any web server of your choice. If you don't have one, you can run one as follows: +```bash +cargo install http-server +http-server +``` +Access the examples at [http://localhost:7878/examples/web/index.html](http://localhost:7878/examples/web/index.html). +(Make sure to change the port if you are using a different server. Many servers will serve on +[http://localhost:8000/examples/web/index.html](http://localhost:8000/examples/web/index.html) by default) + +If building from source, you must run `build-release` or `build-web` scripts before running the examples. + +## Running NodeJs examples + +This applies to running examples while building the project from source as some dependencies are instantiated as a part of the build process. You just need to run `node init` to initialize a local config. + +NOTES: +- `npm install` will install NodeJs types for TypeScript and W3C websocket modules +- `npm install -g typedoc` is needed for the release build to generate documentation +- `node init` creates a local `examples/data/config.json` that contains a private key (mnemonic) use across NodeJS examples. You can override address used in some examples by specifying the address as a command line argument. +- Majority of examples will accept following arguments: `node @@ -60,21 +147,35 @@ WebSocket implementation. Two of such modules are [WebSocket](https://www.npmjs. ## Loading in a Node.js App ```javascript +// // W3C WebSocket module shim // this is provided by NPM `kaspa` module and is only needed // if you are building WASM libraries for NodeJS from source +// +// @ts-ignore // globalThis.WebSocket = require('websocket').w3cwebsocket; +// -let {RpcClient,Encoding,initConsolePanicHook} = require('./kaspa-rpc'); +let { + RpcClient, + Encoding, + initConsolePanicHook +} = require('./kaspa'); // enabling console panic hooks allows WASM to print panic details to console // initConsolePanicHook(); // enabling browser panic hooks will create a full-page DIV with panic details // this is useful for mobile devices where console is not available // initBrowserPanicHook(); +``` +```javascript // if port is not specified, it will use the default port for the specified network -const rpc = new RpcClient("127.0.0.1", Encoding.Borsh, "testnet-10"); +const rpc = new RpcClient({ + url: "127.0.0.1", + encoding: Encoding.Borsh, + network : "testnet-10" +}); (async () => { try { @@ -88,3 +189,18 @@ const rpc = new RpcClient("127.0.0.1", Encoding.Borsh, "testnet-10"); ``` For more details, please follow the [**integrating with Kaspa**](https://kaspa.aspectron.org/) guide. + +## Creating Documentation + +Please note that to build documentation from source you need to have the Rust environment installed. +The build script will first build the WASM32 SDK and then generate typedoc documentation from it. + +You can build documentation from source as follows: + +```bash +npm install -g typedoc +./build-docs +``` + +The resulting documentation will be located in `docs/typedoc/` + diff --git a/wasm/build-docs b/wasm/build-docs index a90057c13..de504e8de 100755 --- a/wasm/build-docs +++ b/wasm/build-docs @@ -1,2 +1,28 @@ -wasm-pack build --target web --out-dir web/kaspa -jsdoc --destination docs/kaspa-wasm web/kaspa/kaspa_wasm.js ./README.md +#!/bin/bash +set -e + +./build-web $@ + +if [ "$1" == "--keygen" ]; then + echo "building keygen" + typedoc --name "Kaspa WASM32 SDK - Key Generation" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa-keygen ./build/docs/kaspa-keygen.ts +elif [ "$1" == "--rpc" ]; then + echo "building rpc" + typedoc --name "Kaspa WASM32 SDK - RPC" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa-rpc ./build/docs/kaspa-rpc.ts +elif [ "$1" == "--core" ]; then + echo "building core" + typedoc --name "Kaspa WASM32 SDK - Core" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa-core ./build/docs/kaspa-core.ts +elif [ "$1" == "--sdk" ]; then + echo "building full" + typedoc --name "Kaspa WASM32 SDK" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa ./build/docs/kaspa.ts +else + echo "building keygen" + typedoc --name "Kaspa WASM32 SDK - Key Generation" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa-keygen ./build/docs/kaspa-keygen.ts + echo "building rpc" + typedoc --name "Kaspa WASM32 SDK - RPC" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa-rpc ./build/docs/kaspa-rpc.ts + echo "building core" + typedoc --name "Kaspa WASM32 SDK - Core" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa-core ./build/docs/kaspa-core.ts + echo "building full" + typedoc --name "Kaspa WASM32 SDK" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out ./docs/kaspa ./build/docs/kaspa.ts + +fi \ No newline at end of file diff --git a/wasm/build-node b/wasm/build-node index 18e365701..8fdf93e3c 100755 --- a/wasm/build-node +++ b/wasm/build-node @@ -1 +1,4 @@ -wasm-pack build --target nodejs --out-dir nodejs/kaspa $@ +#!/bin/bash +set -e + +wasm-pack build --weak-refs --target nodejs --out-name kaspa --out-dir nodejs/kaspa --features wasm32-sdk diff --git a/wasm/build-node-dev b/wasm/build-node-dev new file mode 100755 index 000000000..b8de2b6ac --- /dev/null +++ b/wasm/build-node-dev @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${RED}WARNING: do not use resulting WASM binaries in production!${NC}" +wasm-pack build --weak-refs --dev --target nodejs --out-name kaspa --out-dir nodejs/kaspa --features wasm32-sdk $@ diff --git a/wasm/build-release b/wasm/build-release new file mode 100755 index 000000000..462c272a6 --- /dev/null +++ b/wasm/build-release @@ -0,0 +1,90 @@ +#!/bin/bash +# This script builds the Rusty Kaspa WASM32 SDK release. + +# make the script fail for any failed command +set -e + +rm -rf release/* +rm -rf web/* +rm -rf nodejs/* +rm -rf docs/* +rm -rf examples/nodejs/typescript/lib + +mkdir -p release/kaspa-wasm32-sdk/web +mkdir -p release/kaspa-wasm32-sdk/nodejs +mkdir -p release/kaspa-wasm32-sdk/docs + +# pushd . +# cd ../rpc/wrpc/wasm +# wasm-pack build --target web --out-name kaspa-rpc --out-dir web/kaspa-rpc --features wasm32-sdk $@ +# popd + +wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-keygen --features wasm32-keygen $@ +wasm-pack build --weak-refs --dev --target web --out-name kaspa --out-dir web/kaspa-keygen-dev --features wasm32-keygen $@ + +wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-rpc --features wasm32-rpc $@ +wasm-pack build --weak-refs --dev --target web --out-name kaspa --out-dir web/kaspa-rpc-dev --features wasm32-rpc $@ + +wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-core --features wasm32-core $@ +wasm-pack build --weak-refs --dev --target web --out-name kaspa --out-dir web/kaspa-core-dev --features wasm32-core $@ + +wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa --features wasm32-sdk $@ +wasm-pack build --weak-refs --dev --target web --out-name kaspa --out-dir web/kaspa-dev --features wasm32-sdk $@ + +wasm-pack build --weak-refs --target nodejs --out-name kaspa --out-dir nodejs/kaspa --features wasm32-sdk $@ +wasm-pack build --weak-refs --dev --target nodejs --out-name kaspa --out-dir nodejs/kaspa-dev --features wasm32-sdk $@ + +# wasm-pack build --target web --dev --out-name kaspa --out-dir build/docs/kaspa --features wasm32-sdk $@ +typedoc --name "Kaspa WASM32 SDK - Key Generation" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out docs/kaspa-keygen ./build/docs/kaspa-keygen.ts +typedoc --name "Kaspa WASM32 SDK - RPC" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out docs/kaspa-rpc ./build/docs/kaspa-rpc.ts +typedoc --name "Kaspa WASM32 SDK - Core" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out docs/kaspa-core ./build/docs/kaspa-core.ts +typedoc --name "Kaspa WASM32 SDK" --sourceLinkExternal --readme ./README.md --options ./build/docs/ --out docs/kaspa ./build/docs/kaspa.ts + +# cp -r ../rpc/wrpc/wasm/web/kaspa-rpc release/kaspa-wasm32-sdk/web/kaspa-rpc +cp -r web/kaspa-keygen release/kaspa-wasm32-sdk/web/kaspa-keygen +cp -r web/kaspa-keygen-dev release/kaspa-wasm32-sdk/web/kaspa-keygen-dev + +cp -r web/kaspa-rpc release/kaspa-wasm32-sdk/web/kaspa-rpc +cp -r web/kaspa-rpc-dev release/kaspa-wasm32-sdk/web/kaspa-rpc-dev + +cp -r web/kaspa-core release/kaspa-wasm32-sdk/web/kaspa-core +cp -r web/kaspa-core-dev release/kaspa-wasm32-sdk/web/kaspa-core-dev + +cp -r web/kaspa release/kaspa-wasm32-sdk/web/kaspa +cp -r web/kaspa-dev release/kaspa-wasm32-sdk/web/kaspa-dev + +cp -r nodejs/kaspa release/kaspa-wasm32-sdk/nodejs/kaspa +cp -r nodejs/kaspa-dev release/kaspa-wasm32-sdk/nodejs/kaspa-dev + +cp -r docs/kaspa-keygen release/kaspa-wasm32-sdk/docs/kaspa-keygen +cp -r docs/kaspa-rpc release/kaspa-wasm32-sdk/docs/kaspa-rpc +cp -r docs/kaspa-core release/kaspa-wasm32-sdk/docs/kaspa-core +cp -r docs/kaspa release/kaspa-wasm32-sdk/docs/kaspa + +mkdir -p release/kaspa-wasm32-sdk/examples/data +cp -r examples/data/.gitignore release/kaspa-wasm32-sdk/examples/data/.gitignore +cp -r examples/nodejs release/kaspa-wasm32-sdk/examples/ +cp -r examples/web release/kaspa-wasm32-sdk/examples/ +cp -r examples/init.js release/kaspa-wasm32-sdk/examples/ +cp -r examples/jsconfig.json release/kaspa-wasm32-sdk/examples/ +cp -r examples/package.json release/kaspa-wasm32-sdk/examples/ + +pushd . +cd release/kaspa-wasm32-sdk/examples +npm install +popd + +# tsc release/kaspa-wasm32-sdk/examples/nodejs/typescript/ + +cp index.html release/kaspa-wasm32-sdk/index.html +cp README.md release/kaspa-wasm32-sdk/README.md +cp CHANGELOG.md release/kaspa-wasm32-sdk/CHANGELOG.md +cp LICENSE release/kaspa-wasm32-sdk/LICENSE + +node build/package-sizes.js +cp package-sizes.js release/kaspa-wasm32-sdk/package-sizes.js + +pushd . +cd release +zip -q -r kaspa-wasm32-sdk.zip kaspa-wasm32-sdk +popd diff --git a/wasm/build-web b/wasm/build-web index 91a110541..1f1208645 100755 --- a/wasm/build-web +++ b/wasm/build-web @@ -1 +1,28 @@ -wasm-pack build --target web --out-dir web/kaspa $@ +#!/bin/bash +set -e + +if [ "$1" == "--keygen" ]; then + echo "building wasm32-keygen" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-keygen --features wasm32-keygen +elif [ "$1" == "--rpc" ]; then + echo "building wasm32-rpc" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-rpc --features wasm32-rpc +elif [ "$1" == "--core" ]; then + echo "building wasm32-core" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-core --features wasm32-core +elif [ "$1" == "--sdk" ]; then + echo "building wasm32-sdk" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa --features wasm32-sdk + +else + + echo "building wasm32-keygen" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-keygen --features wasm32-keygen + echo "building wasm32-rpc" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-rpc --features wasm32-rpc + echo "building wasm32-core" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa-core --features wasm32-core + echo "building wasm32-sdk" + wasm-pack build --weak-refs --target web --out-name kaspa --out-dir web/kaspa --features wasm32-sdk + +fi \ No newline at end of file diff --git a/wasm/build-web-dev b/wasm/build-web-dev new file mode 100755 index 000000000..05a993b16 --- /dev/null +++ b/wasm/build-web-dev @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${RED}WARNING: do not use resulting WASM binaries in production!${NC}" + +wasm-pack build --weak-refs -dev --target web --out-name kaspa --out-dir web/kaspa-dev-keygen --features wasm32-keygen $@ +wasm-pack build --weak-refs -dev --target web --out-name kaspa --out-dir web/kaspa-dev-rpc --features wasm32-rpc $@ +wasm-pack build --weak-refs -dev --target web --out-name kaspa --out-dir web/kaspa-dev-core --features wasm32-core $@ +wasm-pack build --weak-refs -dev --target web --out-name kaspa --out-dir web/kaspa-dev --features wasm32-sdk $@ diff --git a/wasm/build/docs/kaspa-core.ts b/wasm/build/docs/kaspa-core.ts new file mode 100644 index 000000000..a335e1db6 --- /dev/null +++ b/wasm/build/docs/kaspa-core.ts @@ -0,0 +1 @@ +export * from "../../web/kaspa-core"; diff --git a/wasm/build/docs/kaspa-keygen.ts b/wasm/build/docs/kaspa-keygen.ts new file mode 100644 index 000000000..2eb089f63 --- /dev/null +++ b/wasm/build/docs/kaspa-keygen.ts @@ -0,0 +1 @@ +export * from "../../web/kaspa-keygen"; diff --git a/wasm/build/docs/kaspa-rpc.ts b/wasm/build/docs/kaspa-rpc.ts new file mode 100644 index 000000000..b30043d76 --- /dev/null +++ b/wasm/build/docs/kaspa-rpc.ts @@ -0,0 +1 @@ +export * from "../../web/kaspa-rpc"; diff --git a/wasm/build/docs/kaspa.ts b/wasm/build/docs/kaspa.ts new file mode 100644 index 000000000..6b9ca6aa4 --- /dev/null +++ b/wasm/build/docs/kaspa.ts @@ -0,0 +1 @@ +export * from "../../web/kaspa"; diff --git a/wasm/build/docs/tsconfig.json b/wasm/build/docs/tsconfig.json new file mode 100644 index 000000000..3d7448b00 --- /dev/null +++ b/wasm/build/docs/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } + } \ No newline at end of file diff --git a/wasm/build/docs/typedoc.json b/wasm/build/docs/typedoc.json new file mode 100644 index 000000000..b89af0882 --- /dev/null +++ b/wasm/build/docs/typedoc.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "treatWarningsAsErrors": true, + "cleanOutputDir": true, + "disableSources": true, + "categoryOrder": ["*", "Other"], +} diff --git a/wasm/build/package-sizes.js b/wasm/build/package-sizes.js new file mode 100644 index 000000000..4d274b978 --- /dev/null +++ b/wasm/build/package-sizes.js @@ -0,0 +1,56 @@ +const fs = require('fs'); +const path = require('path'); + +// Function to calculate human readable size +function humanReadableSize(size) { + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + while (size >= 1024 && i < units.length - 1) { + size /= 1024; + i++; + } + return `${size.toFixed(2)} ${units[i]}`; +} + +// Function to calculate total size of files in a folder recursively +function calculateFolderSize(folderPath) { + let totalSize = 0; + const files = fs.readdirSync(folderPath); + + files.forEach(file => { + const filePath = path.join(folderPath, file); + const stats = fs.statSync(filePath); + if (stats.isFile()) { + totalSize += stats.size; + } else if (stats.isDirectory()) { + totalSize += calculateFolderSize(filePath); + } + }); + + return totalSize; +} + +// Function to scan folder, calculate total size, and generate JSON file +function generateFolderSizesJSON(folderPath, outputFileName) { + const folders = fs.readdirSync(folderPath); + const folderSizes = {}; + + folders.forEach(folder => { + const absoluteFolder = path.join(folderPath, folder); + if (fs.statSync(absoluteFolder).isDirectory()) { + const folderSize = calculateFolderSize(absoluteFolder); + folderSizes[folder] = humanReadableSize(folderSize); + } + }); + + const jsonContent = JSON.stringify(folderSizes, null, 2); + const jsContent = "window.packageSizes = " + jsonContent + ";"; + fs.writeFileSync(outputFileName, jsContent); + console.log(`Folder sizes JSON file generated successfully: ${outputFileName}`); +} + +// Usage example +const folderPath = path.join(__dirname,'../web/'); +const outputFileName = path.join(__dirname,'../package-sizes.js'); + +generateFolderSizesJSON(folderPath, outputFileName); diff --git a/wasm/core/Cargo.toml b/wasm/core/Cargo.toml new file mode 100644 index 000000000..a8a49e0aa --- /dev/null +++ b/wasm/core/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "kaspa-wasm-core" +description = "Kaspa core WASM32 types" +version.workspace = true +edition.workspace = true +authors.workspace = true +include.workspace = true +license.workspace = true +repository.workspace = true + +[features] +wasm32-sdk = [] + +[dependencies] +wasm-bindgen.workspace = true +js-sys.workspace = true +faster-hex.workspace = true + +[lints.clippy] +empty_docs = "allow" diff --git a/wasm/core/src/events.rs b/wasm/core/src/events.rs new file mode 100644 index 000000000..b7f7cf417 --- /dev/null +++ b/wasm/core/src/events.rs @@ -0,0 +1,65 @@ +use js_sys::{Array, Function, Object, Reflect}; +use wasm_bindgen::prelude::*; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Sink { + context: Option, + callback: Function, +} + +impl Sink { + pub fn new(callback: F) -> Self + where + F: AsRef, + { + Self { context: None, callback: callback.as_ref().clone() } + } + + pub fn with_context(mut self, context: Option) -> Self { + self.context = context; + self + } + + pub fn call(&self, args: &JsValue) -> std::result::Result { + if let Some(context) = &self.context { + self.callback.call1(context, args) + } else { + self.callback.call1(&JsValue::UNDEFINED, args) + } + } +} + +unsafe impl Send for Sink {} + +impl Sink { + pub fn try_from(value: T) -> std::result::Result + where + T: AsRef, + { + let value = value.as_ref(); + if let Some(callback) = value.dyn_ref::() { + Ok(Sink::new(callback)) + } else if let Some(context) = value.dyn_ref::() { + let callback = Reflect::get(context, &JsValue::from("handleEvent")) + .map_err(|_| JsValue::from("Object does not have 'handleEvent()' method"))? + .dyn_into::() + .map_err(|_| JsValue::from("'handleEvent()' is not a function"))?; + Ok(Sink::new(callback).with_context(Some(context.clone()))) + } else { + Err(JsValue::from(format!("Invalid event listener callback: {:?}", value))) + } + } +} + +pub fn get_event_targets(targets: T) -> std::result::Result, E> +where + T: Into, + R: TryFrom, +{ + let js_value = targets.into(); + if let Ok(array) = js_value.clone().dyn_into::() { + array.iter().map(|item| R::try_from(item)).collect() + } else { + Ok(vec![R::try_from(js_value)?]) + } +} diff --git a/wasm/core/src/lib.rs b/wasm/core/src/lib.rs new file mode 100644 index 000000000..1710c11fe --- /dev/null +++ b/wasm/core/src/lib.rs @@ -0,0 +1,4 @@ +pub mod events; +pub mod types; + +// pub use types::*; diff --git a/wasm/core/src/types.rs b/wasm/core/src/types.rs new file mode 100644 index 000000000..fc898ee8b --- /dev/null +++ b/wasm/core/src/types.rs @@ -0,0 +1,57 @@ +//! +//! General-purpose types for WASM bindings +//! + +use std::str; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_HEX_STRING: &'static str = r#" +/** + * A string containing a hexadecimal representation of the data (typically representing for IDs or Hashes). + * + * @category General + */ +export type HexString = string; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "HexString")] + pub type HexString; +} + +impl From for HexString { + fn from(s: String) -> Self { + JsValue::from(s).into() + } +} + +impl TryFrom for String { + type Error = &'static str; + + fn try_from(value: HexString) -> Result { + value.as_string().ok_or("Supplied value is not a string") + } +} + +impl From<&[u8]> for HexString { + fn from(bytes: &[u8]) -> Self { + let mut hex = vec![0u8; bytes.len() * 2]; + faster_hex::hex_encode(bytes, hex.as_mut_slice()).expect("The output is exactly twice the size of the input"); + let result = unsafe { str::from_utf8_unchecked(&hex) }; + JsValue::from(result).into() + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Array")] + pub type StringArray; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "HexString | Uint8Array")] + pub type BinaryT; +} diff --git a/wasm/examples/.gitignore b/wasm/examples/.gitignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/wasm/examples/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/wasm/examples/.nojekyll b/wasm/examples/.nojekyll new file mode 100644 index 000000000..e2ac6616a --- /dev/null +++ b/wasm/examples/.nojekyll @@ -0,0 +1 @@ +TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/wasm/examples/browser-extension/README.md b/wasm/examples/browser-extension/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/wasm/examples/browser-extension/index.html b/wasm/examples/browser-extension/index.html new file mode 100644 index 000000000..618dbbf4f --- /dev/null +++ b/wasm/examples/browser-extension/index.html @@ -0,0 +1,102 @@ + + + + + + + + + + \ No newline at end of file diff --git a/wasm/examples/browser-extension/resources/style.css b/wasm/examples/browser-extension/resources/style.css new file mode 100644 index 000000000..3289093b4 --- /dev/null +++ b/wasm/examples/browser-extension/resources/style.css @@ -0,0 +1,18 @@ +.wallet{ + padding:10px; + border:1px solid #DDD; + border-radius: 5px; + margin:2px; + display: flex; + align-items: center; + width:200px; + cursor:pointer; +} +.wallet .title{ + font-size:16px; + margin-left:10px; +} +.wallet .icon{ + width:50px; + height:50px; +} \ No newline at end of file diff --git a/wasm/examples/browser-extension/server.js b/wasm/examples/browser-extension/server.js new file mode 100644 index 000000000..d20d8e6a6 --- /dev/null +++ b/wasm/examples/browser-extension/server.js @@ -0,0 +1 @@ +// TODO - NodeJs HTTP server with Kaspa Wallet and a client-facing WebSocket (example backend that receives payments) \ No newline at end of file diff --git a/wasm/examples/data/.gitignore b/wasm/examples/data/.gitignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/wasm/examples/data/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/wasm/examples/init.js b/wasm/examples/init.js new file mode 100644 index 000000000..3dd946ecc --- /dev/null +++ b/wasm/examples/init.js @@ -0,0 +1,119 @@ + +const fs = require('fs'); +const path = require('path'); +const { Mnemonic, XPrv, PublicKeyGenerator } = require('../nodejs/kaspa'); +const { parseArgs } = require('node:util'); +const { create } = require('domain'); + +let args = process.argv.slice(2); +const { + values, + positionals, + tokens, +} = parseArgs({ + args, options: { + help: { + type: 'boolean', + }, + reset: { + type: 'boolean', + }, + network: { + type: 'string', + }, + }, tokens: true, allowPositionals: true +}); + +if (values.help) { + console.log(`Usage: node init [--reset] [--network=(mainnet|testnet-)]`); + process.exit(0); +} + +const network = values.network ?? positionals.find((positional) => positional.match(/^(testnet|mainnet|simnet|devnet)-\d+$/)) ?? null; + +const configFileName = path.join(__dirname, "data", "config.json"); +const exists = fs.existsSync(configFileName); +if (!exists || values.reset) { + createConfigFile(); + process.exit(0); +} + +if (network) { + let config = JSON.parse(fs.readFileSync(configFileName, "utf8")); + config.networkId = network; + fs.writeFileSync(configFileName, JSON.stringify(config, null, 4)); + console.log(""); + console.log(`Updating networkId to '${network}'`); +} + +if (fs.existsSync(configFileName)) { + let config = JSON.parse(fs.readFileSync(configFileName, "utf8")); +// console.log("loading mnemonic:", config.mnemonic); + let mnemonic = new Mnemonic(config.mnemonic); + let wallet = basicWallet(config.networkId, mnemonic); + + console.log(""); + console.log("networkId:", config.networkId); + console.log("mnemonic:", wallet.mnemonic.phrase); + console.log("xprv:", wallet.xprv); + console.log("receive:", wallet.receive); + console.log("change:", wallet.change); + console.log(""); + console.log("Use 'init --reset' to reset the config file"); + console.log(""); +} + +function createConfigFile() { + + if (!network) { + console.log("... '--network=' argument is not specified ...defaulting to 'testnet-11'"); + } + let networkId = network ?? "testnet-11"; + + let wallet = basicWallet(networkId, Mnemonic.random()); + + let config = { + networkId, + mnemonic: wallet.mnemonic.phrase, + }; + fs.writeFileSync(configFileName, JSON.stringify(config, null, 4)); + console.log(""); + console.log("Creating config data in './data/config.json'"); + console.log(""); + console.log("networkId:", networkId); + console.log("mnemonic:", wallet.mnemonic.phrase); + console.log("xprv:", wallet.xprv); + console.log("receive:", wallet.receive); + console.log("change:", wallet.change); + console.log(""); +} + +function basicWallet(networkId, mnemonic) { + console.log("mnemonic:", mnemonic.phrase); + let xprv = new XPrv(mnemonic.toSeed()); + let account_0_root = xprv.derivePath("m/44'/111111'/0'/0").toXPub(); + let account_0 = { + receive_xpub : account_0_root.deriveChild(0), + change_xpub : account_0_root.deriveChild(1), + }; + let receive = account_0.receive_xpub.deriveChild(0).toPublicKey().toAddress(networkId).toString(); + let change = account_0.change_xpub.deriveChild(0).toPublicKey().toAddress(networkId).toString(); + + let keygen = PublicKeyGenerator.fromMasterXPrv( + xprv.toString(), + false, + 0n,0 + ); + + // let receive_pubkeys = keygen.receivePubkeys(0,1).map((key) => key.toAddress(networkId).toString()); + // let change_pubkeys = keygen.changePubkeys(0,1).map((key) => key.toAddress(networkId).toString()); + // console.log("receive_pubkeys:", receive_pubkeys); + // console.log("change_pubkeys:", change_pubkeys); + + return { + mnemonic, + xprv: xprv.toString(), + receive, + change, + }; +} \ No newline at end of file diff --git a/wasm/examples/jsconfig.json b/wasm/examples/jsconfig.json new file mode 100644 index 000000000..33d19d4fe --- /dev/null +++ b/wasm/examples/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ES2022", + "checkJs": true, + }, + "exclude": ["**/node_modules"], + "include": ["**/*.js"], +} diff --git a/wasm/examples/nodejs/javascript/.gitignore b/wasm/examples/nodejs/javascript/.gitignore new file mode 100644 index 000000000..25c8fdbab --- /dev/null +++ b/wasm/examples/nodejs/javascript/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json \ No newline at end of file diff --git a/wasm/nodejs/addresses.js b/wasm/examples/nodejs/javascript/general/addresses.js similarity index 96% rename from wasm/nodejs/addresses.js rename to wasm/examples/nodejs/javascript/general/addresses.js index 1735ca639..9d327c761 100644 --- a/wasm/nodejs/addresses.js +++ b/wasm/examples/nodejs/javascript/general/addresses.js @@ -1,8 +1,8 @@ -let kaspa = require('./kaspa/kaspa_wasm'); +let kaspa = require('../../../../nodejs/kaspa'); let { PrivateKey, PublicKey, - XPublicKey, + PublicKeyGenerator, createAddress, NetworkType, } = kaspa; @@ -16,7 +16,7 @@ kaspa.initConsolePanicHook(); /*** Advanced ***/ // HD Wallet-style public key generation - let xpub = await XPublicKey.fromMasterXPrv( + let xpub = await PublicKeyGenerator.fromMasterXPrv( "kprv5y2qurMHCsXYrNfU3GCihuwG3vMqFji7PZXajMEqyBkNh9UZUJgoHYBLTKu1eM4MvUtomcXPQ3Sw9HZ5ebbM4byoUciHo1zrPJBQfqpLorQ", false, 0n diff --git a/wasm/examples/nodejs/javascript/general/derivation.js b/wasm/examples/nodejs/javascript/general/derivation.js new file mode 100644 index 000000000..f92508c88 --- /dev/null +++ b/wasm/examples/nodejs/javascript/general/derivation.js @@ -0,0 +1,67 @@ +const kaspa = require('../../../../nodejs/kaspa'); +const { + Mnemonic, + XPrv, + DerivationPath, + PublicKey, + NetworkType, +} = kaspa; + +kaspa.initConsolePanicHook(); + +(async () => { + + //const mnemonic = Mnemonic.random(); + const mnemonic = new Mnemonic("hunt bitter praise lift buyer topic crane leopard uniform network inquiry over grain pass match crush marine strike doll relax fortune trumpet sunny silk") + console.log("mnemonic:", mnemonic); + const seed = mnemonic.toSeed(); + console.log("seed:", seed); + + // kaspa + let xPrv = new XPrv(seed); + // derive full path upto second address of receive wallet + let pubkey1 = xPrv.derivePath("m/44'/111111'/0'/0/1").toXPub().toPublicKey(); + console.log("address", pubkey1.toAddress(NetworkType.Mainnet)); + + // create receive wallet + let receiveWalletXPub = xPrv.derivePath("m/44'/111111'/0'/0").toXPub(); + // derive receive wallet for second address + let pubkey2 = receiveWalletXPub.deriveChild(1, false).toPublicKey(); + console.log("address", pubkey2.toAddress(NetworkType.Mainnet)); + + // create change wallet + let changeWalletXPub = xPrv.derivePath("m/44'/111111'/0'/1").toXPub(); + // derive change wallet for first address + let pubkey3 = changeWalletXPub.deriveChild(0, false).toPublicKey(); + console.log("address", pubkey2.toAddress(NetworkType.Mainnet)); + + if (pubkey1.toString() != pubkey2.toString()){ + throw new Error("pubkeyes dont match") + } + // --- + + // xprv with ktrv prefix + const ktrv = xPrv.intoString("ktrv"); + console.log("ktrv", ktrv) + + //create DerivationPath + const path = new DerivationPath("m/1'"); + path.push(2, true); + path.push(3, false); + console.log(`path: ${path}`); + + // derive by path string + console.log("xPrv1", xPrv.derivePath("m/1'/2'/3").intoString("xprv")) + // derive by DerivationPath object + console.log("xPrv3", xPrv.derivePath(path).intoString("xprv")) + // create XPrv from ktrvxxx string and derive it + console.log("xPrv2", XPrv.fromXPrv(ktrv).derivePath("m/1'/2'/3").intoString("xprv")) + + + // get xpub + let xPub = xPrv.toXPub(); + // derive xPub + console.log("xPub", xPub.derivePath("m/1").intoString("xpub")); + // get publicKey from xPub + console.log("publicKey", xPub.toPublicKey().toString()); +})(); diff --git a/wasm/nodejs/encryption.js b/wasm/examples/nodejs/javascript/general/encryption.js similarity index 85% rename from wasm/nodejs/encryption.js rename to wasm/examples/nodejs/javascript/general/encryption.js index 19506b07d..67e269777 100644 --- a/wasm/nodejs/encryption.js +++ b/wasm/examples/nodejs/javascript/general/encryption.js @@ -1,4 +1,4 @@ -const kaspa = require('./kaspa/kaspa_wasm'); +const kaspa = require('../../../../nodejs/kaspa'); kaspa.initConsolePanicHook(); diff --git a/wasm/examples/nodejs/javascript/general/get-balances-by-addresses.js b/wasm/examples/nodejs/javascript/general/get-balances-by-addresses.js new file mode 100644 index 000000000..1426a5078 --- /dev/null +++ b/wasm/examples/nodejs/javascript/general/get-balances-by-addresses.js @@ -0,0 +1,36 @@ +// @ts-ignore +globalThis.WebSocket = require('websocket').w3cwebsocket; // W3C WebSocket module shim + +const kaspa = require('../../../../nodejs/kaspa'); +const { parseArgs } = require("../utils"); +const { + RpcClient, + Resolver, +} = kaspa; + +kaspa.initConsolePanicHook(); + +const { + networkId, + encoding, +} = parseArgs(); + +(async () => { + + const rpc = new RpcClient({ + // url : "127.0.0.1", + // encoding, + resolver: new Resolver(), + networkId : "mainnet" + }); + console.log(`Resolving RPC endpoint...`); + await rpc.connect(); + console.log(`Connecting to ${rpc.url}`) + + const info = await rpc.getBalancesByAddresses({ addresses : ["kaspa:qpamkvhgh0kzx50gwvvp5xs8ktmqutcy3dfs9dc3w7lm9rq0zs76vf959mmrp"]}); + // const info = await rpc.getBalancesByAddresses(["kaspa:qpamkvhgh0kzx50gwvvp5xs8ktmqutcy3dfs9dc3w7lm9rq0zs76vf959mmrp"]); + console.log("GetBalancesByAddresses response:", info); + + await rpc.disconnect(); + console.log("bye!"); +})(); diff --git a/wasm/nodejs/message_signing.js b/wasm/examples/nodejs/javascript/general/message-signing.js similarity index 74% rename from wasm/nodejs/message_signing.js rename to wasm/examples/nodejs/javascript/general/message-signing.js index d2bd0921e..ed12afd45 100644 --- a/wasm/nodejs/message_signing.js +++ b/wasm/examples/nodejs/javascript/general/message-signing.js @@ -1,4 +1,4 @@ -let kaspa = require('./kaspa/kaspa_wasm'); +let kaspa = require('../../../../nodejs/kaspa'); let { PrivateKey, PublicKey, @@ -9,8 +9,8 @@ let { kaspa.initConsolePanicHook(); let message = 'Hello Kaspa!'; -let privkey = 'B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF'; -let pubkey = 'DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659'; +let privkey = 'b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef'; +let pubkey = 'dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659'; function runDemo(message, privateKey, publicKey) { let signature = signMessage({message, privateKey}); diff --git a/wasm/nodejs/mining-header.js b/wasm/examples/nodejs/javascript/general/mining-header.js similarity index 92% rename from wasm/nodejs/mining-header.js rename to wasm/examples/nodejs/javascript/general/mining-header.js index 7ade25f21..696cb5366 100644 --- a/wasm/nodejs/mining-header.js +++ b/wasm/examples/nodejs/javascript/general/mining-header.js @@ -1,4 +1,4 @@ -const kaspa = require('../kaspa/kaspa_wasm'); +const kaspa = require('../../../../nodejs/kaspa'); const {parseArgs} = require("../utils"); kaspa.initConsolePanicHook(); @@ -21,7 +21,7 @@ kaspa.initConsolePanicHook(); nonce: 567n, daaScore: 0n, blueScore: 0n, - blueWork: "baadf00d", + blueWork: "baadf00d", // or 12345n }); console.log("initial header:", header); @@ -29,7 +29,7 @@ kaspa.initConsolePanicHook(); console.log("header (after finalize):", header); console.log("resulting hash:", hash); - header.hash = "73fec18005560d4e3654b1c563c6629d48f3a45f42e5ea772e3ad984339f1e19"; + // header.hash = "73fec18005560d4e3654b1c563c6629d48f3a45f42e5ea772e3ad984339f1e19"; // note that asJSON() returns a JSON string where each BigInt is represented by an integer value, // whereas toJSON() returns a JavaScript object containing BigInt objects. diff --git a/wasm/nodejs/mining-state.js b/wasm/examples/nodejs/javascript/general/mining-state.js similarity index 94% rename from wasm/nodejs/mining-state.js rename to wasm/examples/nodejs/javascript/general/mining-state.js index 6db5e48c8..1741fd545 100644 --- a/wasm/nodejs/mining-state.js +++ b/wasm/examples/nodejs/javascript/general/mining-state.js @@ -1,5 +1,5 @@ -const kaspa = require('./kaspa/kaspa_wasm'); -const {parseArgs} = require("./utils"); +const kaspa = require('../../../../nodejs/kaspa'); +const {parseArgs} = require("../utils"); kaspa.initConsolePanicHook(); (async () => { diff --git a/wasm/nodejs/mnemonic.js b/wasm/examples/nodejs/javascript/general/mnemonic.js similarity index 93% rename from wasm/nodejs/mnemonic.js rename to wasm/examples/nodejs/javascript/general/mnemonic.js index 9d16bd74b..1b35c9aa6 100644 --- a/wasm/nodejs/mnemonic.js +++ b/wasm/examples/nodejs/javascript/general/mnemonic.js @@ -1,4 +1,4 @@ -const kaspa = require('./kaspa/kaspa_wasm'); +const kaspa = require('../../../../nodejs/kaspa'); const { Mnemonic, } = kaspa; diff --git a/wasm/examples/nodejs/javascript/general/resolver.js b/wasm/examples/nodejs/javascript/general/resolver.js new file mode 100644 index 000000000..c0ce48d56 --- /dev/null +++ b/wasm/examples/nodejs/javascript/general/resolver.js @@ -0,0 +1,44 @@ +// @ts-ignore +globalThis.WebSocket = require('websocket').w3cwebsocket; // W3C WebSocket module shim + +const kaspa = require('../../../../nodejs/kaspa'); +const { parseArgs } = require("../utils"); +const { + Resolver, + Encoding, + RpcClient, +} = kaspa; + +kaspa.initConsolePanicHook(); + +const { + networkId, + encoding, +} = parseArgs(); + +(async () => { + + const resolver = new Resolver(); + // console.log(resolver); + // process.exit(0); + // let url = await resolver.getUrl(Encoding.Borsh, networkId); + // console.log(url); + const rpc = new RpcClient({ + // url, + // encoding, + resolver, + networkId + }); + + // const rpc = await resolver.connect(networkId); + await rpc.connect(); + console.log("Connected to", rpc.url); + console.log("RPC", rpc); + + // console.log(`Connecting to ${rpc.url}`) + + const info = await rpc.getBlockDagInfo(); + console.log("GetBlockDagInfo response:", info); + + await rpc.disconnect(); +})(); diff --git a/wasm/nodejs/rpc.js b/wasm/examples/nodejs/javascript/general/rpc.js similarity index 54% rename from wasm/nodejs/rpc.js rename to wasm/examples/nodejs/javascript/general/rpc.js index f4ff48329..22d2d0e15 100644 --- a/wasm/nodejs/rpc.js +++ b/wasm/examples/nodejs/javascript/general/rpc.js @@ -1,9 +1,11 @@ +// @ts-ignore globalThis.WebSocket = require('websocket').w3cwebsocket; // W3C WebSocket module shim -const kaspa = require('./kaspa/kaspa_wasm'); -const { parseArgs } = require("./utils"); +const kaspa = require('../../../../nodejs/kaspa'); +const { parseArgs } = require("../utils"); const { - RpcClient + RpcClient, + Resolver, } = kaspa; kaspa.initConsolePanicHook(); @@ -15,12 +17,19 @@ const { (async () => { - const rpc = new RpcClient("127.0.0.1", encoding, networkId); - console.log(`Connecting to ${rpc.url}`) + const rpc = new RpcClient({ + // url : "127.0.0.1", + // encoding, + resolver: new Resolver(), + networkId + }); + console.log(`Resolving RPC endpoint...`); await rpc.connect(); + console.log(`Connecting to ${rpc.url}`) const info = await rpc.getBlockDagInfo(); console.log("GetBlockDagInfo response:", info); await rpc.disconnect(); + console.log("bye!"); })(); diff --git a/wasm/examples/nodejs/javascript/general/subscribe-daa-score.js b/wasm/examples/nodejs/javascript/general/subscribe-daa-score.js new file mode 100644 index 000000000..404d992dc --- /dev/null +++ b/wasm/examples/nodejs/javascript/general/subscribe-daa-score.js @@ -0,0 +1,51 @@ +// @ts-ignore +globalThis.WebSocket = require('websocket').w3cwebsocket; // W3C WebSocket module shim + +const kaspa = require('../../../../nodejs/kaspa'); +const { parseArgs } = require("../utils"); +const { + RpcClient, + Resolver, + // RpcEventType +} = kaspa; + +kaspa.initConsolePanicHook(); + +const { + networkId, + encoding, +} = parseArgs(); + +(async () => { + + const rpc = new RpcClient({ + resolver: new Resolver(), + networkId + }); + + console.log("Registering for DAA notifications..."); + rpc.addEventListener("virtual-daa-score-changed", async (event) => { + console.log(event); + }); + + console.log("Registering for RPC online event..."); + rpc.addEventListener("connect", async (event) => { + console.log("Connected to", rpc.url); + console.log(event); + console.log("Subscribing to DAA score..."); + rpc.subscribeVirtualDaaScoreChanged(); + }); + + console.log(`Connecting...`); + await rpc.connect(); + + process.on('SIGINT', async () => { + console.log('SIGINT'); + console.log("Disconnecting..."); + await rpc.disconnect(); + console.log("Disconnected..."); + console.log("bye!"); + process.exit(0); + }); + +})(); diff --git a/wasm/nodejs/refactoring/storage.js b/wasm/examples/nodejs/javascript/refactoring/storage.js similarity index 100% rename from wasm/nodejs/refactoring/storage.js rename to wasm/examples/nodejs/javascript/refactoring/storage.js diff --git a/wasm/nodejs/refactoring/tx-create.js b/wasm/examples/nodejs/javascript/refactoring/tx-create.js similarity index 97% rename from wasm/nodejs/refactoring/tx-create.js rename to wasm/examples/nodejs/javascript/refactoring/tx-create.js index 4d6e82d0f..c8131622e 100644 --- a/wasm/nodejs/refactoring/tx-create.js +++ b/wasm/examples/nodejs/javascript/refactoring/tx-create.js @@ -25,7 +25,11 @@ kaspa.init_console_panic_hook(); networkType, } = parseArgs(); - const rpc = new RpcClient("127.0.0.1", encoding, networkType); + const rpc = new RpcClient({ + url : "127.0.0.1", + encoding, + networkId + }); console.log(`Connecting to ${rpc.url}`) await rpc.connect(); diff --git a/wasm/nodejs/refactoring/tx-script-sign.js b/wasm/examples/nodejs/javascript/refactoring/tx-script-sign.js similarity index 94% rename from wasm/nodejs/refactoring/tx-script-sign.js rename to wasm/examples/nodejs/javascript/refactoring/tx-script-sign.js index b4f6bc40b..7382c21ba 100644 --- a/wasm/nodejs/refactoring/tx-script-sign.js +++ b/wasm/examples/nodejs/javascript/refactoring/tx-script-sign.js @@ -18,12 +18,16 @@ kaspa.init_console_panic_hook(); // Either NetworkType.Mainnet or NetworkType.Testnet const networkType = args.networkType; - // Either Encoding.Borsh or Encoding.SerdeJson + // Either Encoding.Borsh or Encoding.JSON const encoding = args.encoding; // The kaspa address that was passed as an argument or a default one const address = args.address ?? "kaspatest:qz7ulu4c25dh7fzec9zjyrmlhnkzrg4wmf89q7gzr3gfrsj3uz6xjceef60sd"; - const rpc = new RpcClient("127.0.0.1", encoding, networkType); + const rpc = new RpcClient({ + url : "127.0.0.1", + encoding, + networkId + }); console.log(`# connecting to ${URL}`) await rpc.connect(); diff --git a/wasm/nodejs/estimate.js b/wasm/examples/nodejs/javascript/transactions/estimate.js similarity index 82% rename from wasm/nodejs/estimate.js rename to wasm/examples/nodejs/javascript/transactions/estimate.js index 7caa264b1..4f97000dd 100644 --- a/wasm/nodejs/estimate.js +++ b/wasm/examples/nodejs/javascript/transactions/estimate.js @@ -1,4 +1,5 @@ // Run with: node demo.js +// @ts-ignore globalThis.WebSocket = require("websocket").w3cwebsocket; const { @@ -7,11 +8,11 @@ const { RpcClient, kaspaToSompi, initConsolePanicHook -} = require('./kaspa/kaspa_wasm'); +} = require('../../../../nodejs/kaspa'); initConsolePanicHook(); -const { encoding, networkId } = require("./utils").parseArgs(); +const { encoding, networkId } = require("../utils").parseArgs(); (async () => { @@ -23,7 +24,11 @@ const { encoding, networkId } = require("./utils").parseArgs(); const sourceAddress = privateKey.toKeypair().toAddress(networkId); console.info(`Full kaspa address: ${sourceAddress}`); - const rpc = new RpcClient("127.0.0.1", encoding, networkId); + const rpc = new RpcClient({ + url : "127.0.0.1", + encoding, + networkId + }); console.log(`Connecting to ${rpc.url}`); await rpc.connect(); @@ -34,7 +39,7 @@ const { encoding, networkId } = require("./utils").parseArgs(); return; } - let entries = await rpc.getUtxosByAddresses([sourceAddress]); + let { entries } = await rpc.getUtxosByAddresses([sourceAddress]); if (!entries.length) { console.error(`No UTXOs found for address ${sourceAddress}`); @@ -42,7 +47,7 @@ const { encoding, networkId } = require("./utils").parseArgs(); console.info(entries); // a very basic JS-driven utxo entry sort - entries.sort((a, b) => a.utxoEntry.amount > b.utxoEntry.amount || -(a.utxoEntry.amount < b.utxoEntry.amount)); + entries.sort((a, b) => a.amount > b.amount ? 1 : -1); // create a transaction generator // entries: an array of UtxoEntry @@ -66,8 +71,8 @@ const { encoding, networkId } = require("./utils").parseArgs(); // transaction according to the supplied outputs. let generator = new Generator({ entries, - outputs: [[sourceAddress, kaspaToSompi(0.2)]], - priorityFee: 0, + outputs: [{ address : sourceAddress, amount : kaspaToSompi(0.2)}], + priorityFee: kaspaToSompi(0.0001), changeAddress: sourceAddress, }); diff --git a/wasm/nodejs/generator.js b/wasm/examples/nodejs/javascript/transactions/generator.js similarity index 79% rename from wasm/nodejs/generator.js rename to wasm/examples/nodejs/javascript/transactions/generator.js index d6a55338e..ca6819f08 100644 --- a/wasm/nodejs/generator.js +++ b/wasm/examples/nodejs/javascript/transactions/generator.js @@ -1,4 +1,5 @@ // Run with: node demo.js +// @ts-ignore globalThis.WebSocket = require("websocket").w3cwebsocket; const { @@ -7,11 +8,11 @@ const { Generator, kaspaToSompi, initConsolePanicHook -} = require('./kaspa/kaspa_wasm'); +} = require('../../../../nodejs/kaspa'); initConsolePanicHook(); -const { encoding, networkId, destinationAddress: destinationAddressArg } = require("./utils").parseArgs(); +const { encoding, networkId, address : destinationAddress } = require("../utils").parseArgs(); (async () => { @@ -22,10 +23,14 @@ const { encoding, networkId, destinationAddress: destinationAddressArg } = requi console.info(`Source address: ${sourceAddress}`); // if not destination address is supplied, send funds to source address - const destinationAddress = destinationAddressArg || sourceAddress; - console.info(`Destination address: ${destinationAddress}`); - - const rpc = new RpcClient("127.0.0.1", encoding, networkId); + let address = destinationAddress || sourceAddress; + console.info(`Destination address: ${address}`); + + const rpc = new RpcClient({ + url : "127.0.0.1", + encoding, + networkId + }); console.log(`Connecting to ${rpc.url}`); await rpc.connect(); @@ -37,7 +42,7 @@ const { encoding, networkId, destinationAddress: destinationAddressArg } = requi } - let entries = await rpc.getUtxosByAddresses([sourceAddress]); + let { entries } = await rpc.getUtxosByAddresses([sourceAddress]); if (!entries.length) { console.error(`No UTXOs found for address ${sourceAddress}`); @@ -45,7 +50,7 @@ const { encoding, networkId, destinationAddress: destinationAddressArg } = requi console.info(entries); // a very basic JS-driven utxo entry sort - entries.sort((a, b) => a.utxoEntry.amount > b.utxoEntry.amount || -(a.utxoEntry.amount < b.utxoEntry.amount)); + entries.sort((a, b) => a.amount > b.amount ? 1 : -1); // create a transaction generator // entries: an array of UtxoEntry @@ -69,8 +74,9 @@ const { encoding, networkId, destinationAddress: destinationAddressArg } = requi // transaction according to the supplied outputs. let generator = new Generator({ entries, - outputs: [[destinationAddress, kaspaToSompi(0.2)]], - priorityFee: 0, + outputs: [{address, amount : kaspaToSompi(0.2)}], + // priorityFee: 1000n, + priorityFee: kaspaToSompi(0.0001), changeAddress: sourceAddress, }); diff --git a/wasm/nodejs/simple-transaction.js b/wasm/examples/nodejs/javascript/transactions/simple-transaction.js similarity index 76% rename from wasm/nodejs/simple-transaction.js rename to wasm/examples/nodejs/javascript/transactions/simple-transaction.js index 5f035e922..b215fd597 100644 --- a/wasm/nodejs/simple-transaction.js +++ b/wasm/examples/nodejs/javascript/transactions/simple-transaction.js @@ -1,4 +1,5 @@ // Run with: node demo.js +// @ts-ignore globalThis.WebSocket = require("websocket").w3cwebsocket; const { @@ -8,9 +9,9 @@ const { kaspaToSompi, createTransactions, initConsolePanicHook -} = require('./kaspa/kaspa_wasm'); +} = require('../../../../nodejs/kaspa'); -const { encoding, networkId, destinationAddress: destinationAddressArg } = require("./utils").parseArgs(); +const { encoding, networkId, address: destinationAddressArg } = require("../utils").parseArgs(); initConsolePanicHook(); @@ -27,8 +28,11 @@ initConsolePanicHook(); const destinationAddress = destinationAddressArg || sourceAddress; console.log(`Destination address: ${destinationAddress}`); - // let rpcUrl = RpcClient.parseUrl("127.0.0.1", encoding, networkType); - const rpc = new RpcClient("127.0.0.1", encoding, networkId); + const rpc = new RpcClient({ + url : "127.0.0.1", + encoding, + networkId + }); console.log(`Connecting to ${rpc.url}`); await rpc.connect(); @@ -39,7 +43,7 @@ initConsolePanicHook(); return; } - let entries = await rpc.getUtxosByAddresses([sourceAddress]); + let { entries } = (await rpc.getUtxosByAddresses([sourceAddress])); if (!entries.length) { console.error("No UTXOs found for address"); @@ -47,12 +51,12 @@ initConsolePanicHook(); console.info(entries); // a very basic JS-driven utxo entry sort - entries.sort((a, b) => a.utxoEntry.amount > b.utxoEntry.amount || -(a.utxoEntry.amount < b.utxoEntry.amount)); + entries.sort((a, b) => a.amount > b.amount ? 1 : -1); let { transactions, summary } = await createTransactions({ entries, - outputs: [[destinationAddress, kaspaToSompi(0.00012)]], - priorityFee: 0, + outputs: [{ address : destinationAddress, amount : kaspaToSompi(0.00012)}], + priorityFee: 0n, changeAddress: sourceAddress, }); diff --git a/wasm/nodejs/demo.js b/wasm/examples/nodejs/javascript/transactions/single-transaction-demo.js similarity index 76% rename from wasm/nodejs/demo.js rename to wasm/examples/nodejs/javascript/transactions/single-transaction-demo.js index d8b15564c..def206c15 100644 --- a/wasm/nodejs/demo.js +++ b/wasm/examples/nodejs/javascript/transactions/single-transaction-demo.js @@ -1,4 +1,5 @@ // Run with: node demo.js +// @ts-ignore globalThis.WebSocket = require("websocket").w3cwebsocket; const { @@ -6,13 +7,14 @@ const { RpcClient, createTransaction, signTransaction, - initConsolePanicHook -} = require('./kaspa/kaspa_wasm'); + initConsolePanicHook, + Resolver, +} = require('../../../../nodejs/kaspa'); initConsolePanicHook(); // command line arguments --network=(mainnet|testnet-) --encoding=borsh (default) -const { networkId, encoding } = require("./utils").parseArgs(); +const { networkId, encoding } = require("../utils").parseArgs(); (async () => { @@ -30,7 +32,12 @@ const { networkId, encoding } = require("./utils").parseArgs(); console.info(`Full kaspa address: ${address}`); console.info(address); - const rpc = new RpcClient("127.0.0.1", encoding, networkId); + const rpc = new RpcClient({ + //url : "127.0.0.1", + resolver: new Resolver(), + encoding, + networkId + }); console.log(`Connecting to ${rpc.url}`); await rpc.connect(); console.log(`Connected to ${rpc.url}`); @@ -43,7 +50,7 @@ const { networkId, encoding } = require("./utils").parseArgs(); try { - const utxos = await rpc.getUtxosByAddresses([address]); + const { entries : utxos } = await rpc.getUtxosByAddresses([address]); console.info(utxos); @@ -54,7 +61,7 @@ const { networkId, encoding } = require("./utils").parseArgs(); let total = utxos.reduce((agg, curr) => { - return curr.utxoEntry.amount + agg; + return curr.amount + agg; }, 0n); console.info('Amount sending', total - BigInt(utxos.length) * 2000n) @@ -65,19 +72,20 @@ const { networkId, encoding } = require("./utils").parseArgs(); }]; const changeAddress = address; - console.info(changeAddress); + console.log("changeAddress:", changeAddress) const tx = createTransaction(utxos, outputs, changeAddress, 0n, 0, 1, 1); - console.info(tx); + + console.info("Transaction before signing:", tx); const transaction = signTransaction(tx, [privateKey], true); console.log("Transaction:", transaction); // console.info(JSON.stringify(transaction, null, 4)); - let result = await rpc.submitTransaction(transaction); + let result = await rpc.submitTransaction({transaction}); - console.info(result); + console.info("submitTransaction result:", result); } finally { await rpc.disconnect(); } diff --git a/wasm/nodejs/utxo-context-generator.js b/wasm/examples/nodejs/javascript/transactions/utxo-context-generator.js similarity index 77% rename from wasm/nodejs/utxo-context-generator.js rename to wasm/examples/nodejs/javascript/transactions/utxo-context-generator.js index d76d65c8d..6d29151ea 100644 --- a/wasm/nodejs/utxo-context-generator.js +++ b/wasm/examples/nodejs/javascript/transactions/utxo-context-generator.js @@ -1,37 +1,44 @@ // Run with: node demo.js +// @ts-ignore globalThis.WebSocket = require("websocket").w3cwebsocket; const { PrivateKey, Address, RpcClient, + Generator, UtxoProcessor, UtxoContext, kaspaToSompi, createTransactions, initConsolePanicHook -} = require('./kaspa/kaspa_wasm'); +} = require('../../../../nodejs/kaspa'); initConsolePanicHook(); -const { encoding, networkId, destinationAddress } = require("./utils").parseArgs(); +const { encoding, networkId, address : destinationAddress } = require("../utils").parseArgs(); (async () => { const privateKey = new PrivateKey('b99d75736a0fd0ae2da658959813d680474f5a740a9c970a7da867141596178f'); - const sourceAddress = privateKey.toKeypair().toAddress(networkType); + const sourceAddress = privateKey.toKeypair().toAddress(networkId); console.log(`Source address: ${sourceAddress}`); // if not destination is specified, send back to ourselves - destinationAddress = destinationAddress ?? sourceAddress; + let address = destinationAddress ?? sourceAddress; console.log(`Destination address: ${destinationAddress}`); // 1) Initialize RPC - const rpc = new RpcClient("127.0.0.1", encoding, networkId); + const rpc = new RpcClient({ + url : "127.0.0.1", + encoding, + networkId + }); // 2) Create UtxoProcessor, passing RPC to it - let processor = await new UtxoProcessor({ rpc, networkId }); + let processor = new UtxoProcessor({ rpc, networkId }); + await processor.start(); // 3) Create one of more UtxoContext, passing UtxoProcessor to it // you can create UtxoContext objects as needed to monitor different @@ -39,7 +46,7 @@ const { encoding, networkId, destinationAddress } = require("./utils").parseArgs let context = await new UtxoContext({ processor }); // 4) Register a listener with the UtxoProcessor::events - processor.events.registerListener((event) => { + processor.addEventListener((event) => { console.log("event:", event); }); @@ -63,12 +70,13 @@ const { encoding, networkId, destinationAddress } = require("./utils").parseArgs console.log("Sending transaction"); let generator = new Generator({ - context, - outputs: [[destinationAddress, kaspaToSompi(0.2)]], - priorityFee: 0, + entries : context, + outputs: [{address, amount : kaspaToSompi(0.2)}], + priorityFee: kaspaToSompi(0.0001), changeAddress: sourceAddress, }); + let pending; while (pending = await generator.next()) { await pending.sign([privateKey]); let txid = await pending.submit(rpc); diff --git a/wasm/nodejs/utxo-context-listener.js b/wasm/examples/nodejs/javascript/transactions/utxo-context-listener.js similarity index 51% rename from wasm/nodejs/utxo-context-listener.js rename to wasm/examples/nodejs/javascript/transactions/utxo-context-listener.js index 5aa5b6f2e..0bb796c8c 100644 --- a/wasm/nodejs/utxo-context-listener.js +++ b/wasm/examples/nodejs/javascript/transactions/utxo-context-listener.js @@ -1,52 +1,61 @@ // Run with: node demo.js +// @ts-ignore globalThis.WebSocket = require("websocket").w3cwebsocket; const { PrivateKey, Address, RpcClient, + Resolver, UtxoProcessor, UtxoContext, kaspaToSompi, createTransactions, initConsolePanicHook -} = require('./kaspa/kaspa_wasm'); +} = require('../../../../nodejs/kaspa'); initConsolePanicHook(); -const { encoding, networkId, destinationAddress } = require("./utils").parseArgs(); +const { encoding, networkId, address : destinationAddress } = require("../utils").parseArgs(); (async () => { const privateKey = new PrivateKey('b99d75736a0fd0ae2da658959813d680474f5a740a9c970a7da867141596178f'); - const sourceAddress = privateKey.toKeypair().toAddress(networkType); + const sourceAddress = privateKey.toKeypair().toAddress(networkId).toString(); console.info(`Source address: ${sourceAddress}`); // if not destination is specified, send back to ourselves - destinationAddress = destinationAddress ?? sourceAddress; - console.info(`Destination address: ${destinationAddress}`); + let address = destinationAddress ?? sourceAddress; + console.info(`Tracking address: ${address}`); // 1) Initialize RPC - const rpc = new RpcClient("127.0.0.1", encoding, networkType); + const rpc = new RpcClient({ + // url : "127.0.0.1", + resolver : new Resolver(), + encoding, + networkId + }); // 2) Create UtxoProcessor, passing RPC to it - let processor = await new UtxoProcessor({ rpc, networkId }); + let processor = new UtxoProcessor({ rpc, networkId }); + await processor.start(); // 3) Create one of more UtxoContext, passing UtxoProcessor to it // you can create UtxoContext objects as needed to monitor different // address sets. - let context = await new UtxoContext({ processor }); + let context = new UtxoContext({ processor }); // 4) Register a listener with the UtxoProcessor::events - processor.events.registerListener((event) => { + processor.addEventListener("*", (event) => { console.log("event:", event); }); console.log(processor); // 5) Once the environment is setup, connect to RPC - console.log(`Connecting to ${rpc.url}`); + console.log(`Connecting RPC...`); await rpc.connect(); + console.log(`Connected RPC to ${rpc.url}`); let { isSynced } = await rpc.getServerInfo(); if (!isSynced) { console.error("Please wait for the node to sync"); @@ -54,7 +63,20 @@ const { encoding, networkId, destinationAddress } = require("./utils").parseArgs return; } + // rpc.addEventListener("virtual-daa-score-changed", async (event) => { + // console.log(event); + // }); + + // default address (if not supplied) - TODO - change to built-in wallet-stub address + // kaspatest:qpa8gs8w0quc3ghpx2l2dv30ny0mjuwyaj30xduw92v6mmta7df6uuz3ryfhy + + processor.addEventListener("utxo-proc-start", async (event) => { + // console.log("event:", event); + await context.trackAddresses([address]); + }); + + // 6) Register the address list with the UtxoContext - await context.trackAddresses([sourceAddress]); + // await context.trackAddresses([address]); })(); \ No newline at end of file diff --git a/wasm/examples/nodejs/javascript/utils.js b/wasm/examples/nodejs/javascript/utils.js new file mode 100644 index 000000000..b6356fb0b --- /dev/null +++ b/wasm/examples/nodejs/javascript/utils.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const path = require('path'); +const nodeUtil = require('node:util'); +const { parseArgs: nodeParseArgs, } = nodeUtil; + +const { + Address, + Encoding, + NetworkId, + Mnemonic, +} = require('../../../nodejs/kaspa'); + +/** + * Helper function to parse command line arguments for running the scripts + * @param options Additional options to configure the parsing, such as additional arguments for the script and additional help output to go with it + * @returns {{address: Address, tokens: any, networkId: (NetworkId), encoding: (Encoding)}} + */ +function parseArgs(options = { + additionalParseArgs: {}, + additionalHelpOutput: '', +}) { + const script = path.basename(process.argv[1]); + let args = process.argv.slice(2); + const { + values, + positionals, + tokens, + } = nodeParseArgs({ + args, options: { + ...options.additionalParseArgs, + help: { + type: 'boolean', + }, + json: { + type: 'boolean', + }, + address: { + type: 'string', + }, + network: { + type: 'string', + }, + encoding: { + type: 'string', + }, + }, tokens: true, allowPositionals: true + }); + if (values.help) { + console.log(`Usage: node ${script} [address] [mainnet|testnet-10|testnet-11] [--address
] [--network ] [--encoding ] ${options.additionalHelpOutput}`); + process.exit(0); + } + + let config = null; + // TODO load address from config file if no argument is specified + let configFile = path.join(__dirname, '../../data/config.json'); + if (fs.existsSync(configFile)) { + config = JSON.parse(fs.readFileSync(configFile, "utf8")); + } else { + console.error("Please create a config file by running 'node init' in the 'examples/' folder"); + process.exit(0); + } + + const addressRegex = new RegExp(/(kaspa|kaspatest):\S+/i); + const addressArg = values.address ?? positionals.find((positional) => addressRegex.test(positional)) ?? null; + const address = addressArg === null ? null : new Address(addressArg); + + const networkArg = values.network ?? positionals.find((positional) => positional.match(/^(testnet|mainnet|simnet|devnet)-\d+$/)) ?? config.networkId ?? null; + if (!networkArg) { + console.error('Network id must be specified: --network=(mainnet|testnet-)'); + process.exit(1); + } + const networkId = new NetworkId(networkArg); + + const encodingArg = values.encoding ?? positionals.find((positional) => positional.match(/^(borsh|json)$/)) ?? null; + let encoding = Encoding.Borsh; + if (encodingArg == "json") { + encoding = Encoding.SerdeJson; + } + + return { + address, + networkId, + encoding, + tokens, + }; +} + +module.exports = { + parseArgs, +}; diff --git a/wasm/examples/nodejs/javascript/version.js b/wasm/examples/nodejs/javascript/version.js new file mode 100644 index 000000000..59f5d3ee3 --- /dev/null +++ b/wasm/examples/nodejs/javascript/version.js @@ -0,0 +1,2 @@ +const { version } = require('../../../nodejs/kaspa'); +console.log("version:", version()); diff --git a/wasm/examples/nodejs/javascript/wallet/wallet.js b/wasm/examples/nodejs/javascript/wallet/wallet.js new file mode 100644 index 000000000..49dfdbf9c --- /dev/null +++ b/wasm/examples/nodejs/javascript/wallet/wallet.js @@ -0,0 +1,256 @@ +// @ts-ignore +globalThis.WebSocket = require('websocket').w3cwebsocket; // W3C WebSocket module shim + + +const path = require('path'); +const fs = require('fs'); +const kaspa = require('../../../../nodejs/kaspa'); +const { + Wallet, setDefaultStorageFolder, + AccountKind, Mnemonic, Resolver, + kaspaToSompi, + sompiToKaspaString, + Address +} = kaspa; + +let storageFolder = path.join(__dirname, '../../../data/wallets').normalize(); +if (!fs.existsSync(storageFolder)) { + fs.mkdirSync(storageFolder); +} + +setDefaultStorageFolder(storageFolder); + +(async()=>{ + //const filename = "wallet-394"; + const filename = "wallet-395"; + // const FILE_TX = path.join(storageFolder, filename+"-transactions.json"); + + // let txs = {}; + // if (fs.existsSync(FILE_TX)){ + // txs = JSON.parse(fs.readFileSync(FILE_TX)+"") + // } + + const balance = {}; + //const transactions = {}; + let wallet; + + const chalk = new ((await import('chalk')).Chalk)(); + + function log_title(title){ + console.log(chalk.bold(chalk.green(`\n\n${title}`))) + } + // function saveTransactions(){ + // Object.keys(transactions).forEach(id=>{ + // txs[id] = [...transactions[id].entries()]; + // }) + // fs.writeFileSync(FILE_TX, JSON.stringify(txs)) + // } + + async function log_transactions(accountId){ + + // saveTransactions(); + function value(tx){ + if (tx.data.type == "change"){ + return tx.data.data.changeValue + } + if (["external", "incoming"].includes(tx.data.type)){ + return tx.data.data.value + } + if (["transfer-outgoing", "transfer-incoming", "outgoing", "batch"].includes(tx.data.type)){ + return tx.data.data.paymentValue?? tx.data.data.changeValue + } + + } + let transactionsResult = await wallet.transactionsDataGet({ + accountId, + networkId: "testnet-11", + start:0, + end:20 + }) + //console.log("transactions", transactionsResult.transactions) + let list = []; + transactionsResult.transactions.forEach((tx)=>{ + // console.log("ID:", id); + // console.log("type:", tx.data.type, ", value:", value(tx)); + // console.log(chalk.dim("----------------------------")) + // let addresses = tx.data.data.utxoEntries.map(utxo=>{ + // return utxo.address.substring(0, 5)+"..." + // }); + list.push({ + Id: tx.id, + Type: tx.data.type, + Value: sompiToKaspaString(value(tx)||0) + }); + //console.log("tx.data", tx.id, tx.data) + }); + + log_title("Transactions") + console.table(list) + console.log(""); + + } + + try { + + const walletSecret = "abc"; + wallet = new Wallet({resident: false, networkId: "testnet-11", resolver: new Resolver()}); + //console.log("wallet", wallet) + // Ensure wallet file + if (!await wallet.exists(filename)){ + let response = await wallet.walletCreate({ + walletSecret, + filename, + title: "W-1" + }); + console.log("walletCreate : response", response) + } + + wallet.addEventListener(({type, data})=>{ + + switch (type){ + case "maturity": + case "pending": + case "discovery": + //console.log("transactions[data.binding.id]", data.binding.id, transactions[data.binding.id], transactions) + // console.log("record.hasAddress :receive:", data.hasAddress(firstAccount.receiveAddress)); + // console.log("record.hasAddress :change:", data.hasAddress(firstAccount.changeAddress)); + // console.log("record.data", data.data) + // console.log("record.blockDaaScore", data.blockDaaScore) + if (data.type != "change"){ + //transactions[data.binding.id].set(data.id+"", data.serialize()); + log_transactions(data.binding.id) + } + break; + case "reorg": + //transactions[data.binding.id].delete(data.id+""); + log_transactions(data.binding.id) + break; + case "balance": + balance[data.id] = data.balance; + log_title("Balance"); + let list = []; + Object.keys(balance).map(id=>{ + list.push({ + Account: id.substring(0, 5)+"...", + Mature: sompiToKaspaString(data.balance.mature), + Pending: sompiToKaspaString(data.balance.pending), + Outgoing: sompiToKaspaString(data.balance.outgoing), + MatureUtxo: data.balance.matureUtxoCount, + PendingUtxo: data.balance.pendingUtxoCount, + StasisUtxo: data.balance.stasisUtxoCount + }) + }) + console.table(list) + console.log(""); + break; + case "daa-score-change": + if (data.currentDaaScore%1000 == 0){ + console.log(`[EVENT] ${type}:`, data.currentDaaScore) + } + break; + case "server-status": + case "utxo-proc-start": + case "sync-state": + case "account-activation": + case "utxo-proc-stop": + case "connect": + case "stasis": + // + break; + default: + console.log(`[EVENT] ${type}:`, data) + + } + }) + + // Open wallet + await wallet.walletOpen({ + walletSecret, + filename, + accountDescriptors: false + }); + + // Ensure default account + await wallet.accountsEnsureDefault({ + walletSecret, + type: new AccountKind("bip32") // "bip32" + }); + + // // Create a new account + // // create private key + // let prvKeyData = await wallet.prvKeyDataCreate({ + // walletSecret, + // mnemonic: Mnemonic.random(24).phrase + // }); + + // //console.log("prvKeyData", prvKeyData); + + // let account = await wallet.accountsCreate({ + // walletSecret, + // type:"bip32", + // accountName:"Account-B", + // prvKeyDataId: prvKeyData.prvKeyDataId + // }); + + // console.log("new account:", account); + + // Connect to rpc + await wallet.connect(); + + // Start wallet processing + await wallet.start(); + + // List accounts + let accounts = await wallet.accountsEnumerate({}); + let firstAccount = accounts.accountDescriptors[0]; + + //console.log("firstAccount:", firstAccount); + + // Activate Account + await wallet.accountsActivate({ + accountIds:[firstAccount.accountId] + }); + + log_title("Accounts"); + accounts.accountDescriptors.forEach(a=>{ + console.log(`Account: ${a.accountId}`); + console.log(` Account type: ${a.kind.toString()}`); + console.log(` Account Name: ${a.accountName}`); + console.log(` Receive Address: ${a.receiveAddress}`); + console.log(` Change Address: ${a.changeAddress}`); + console.log("") + }); + + // // Account sweep/compound transactions + // let sweepResult = await wallet.accountsSend({ + // walletSecret, + // accountId: firstAccount.accountId + // }); + // console.log("sweepResult", sweepResult) + + // Send kaspa to address + let sendResult = await wallet.accountsSend({ + walletSecret, + accountId: firstAccount.accountId, + priorityFeeSompi: kaspaToSompi("0.001"), + destination:[{ + address: firstAccount.changeAddress, + amount: kaspaToSompi("1.5") + }] + }); + console.log("sendResult", sendResult); + + // Transfer kaspa between accounts + let transferResult = await wallet.accountsTransfer({ + walletSecret, + sourceAccountId: firstAccount.accountId, + destinationAccountId: firstAccount.accountId, + transferAmountSompi: kaspaToSompi("2.4"), + }); + console.log("transferResult", transferResult); + + + } catch(ex) { + console.error("Error:", ex); + } +})(); \ No newline at end of file diff --git a/wasm/examples/nodejs/typescript/.gitignore b/wasm/examples/nodejs/typescript/.gitignore new file mode 100644 index 000000000..a6c7c2852 --- /dev/null +++ b/wasm/examples/nodejs/typescript/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/wasm/examples/nodejs/typescript/README.md b/wasm/examples/nodejs/typescript/README.md new file mode 100644 index 000000000..44d2a11ba --- /dev/null +++ b/wasm/examples/nodejs/typescript/README.md @@ -0,0 +1,29 @@ +## Running TypeScript examples: + +### Dependencies + +Before running examples, please install project dependencies: + +```bash +npm install +``` + +### Using TS Node + +```bash +npm install -g typescript +npm install -g ts-node +ts-node src/version +``` + +### Using TS Compiler and NodeJS + +```bash +npm install -g typescript +tsc +node lib/version +``` + +### TypeScript install +Additional info on typescript installation can be found here: +https://www.typescriptlang.org/download diff --git a/wasm/examples/nodejs/typescript/src/address.ts b/wasm/examples/nodejs/typescript/src/address.ts new file mode 100644 index 000000000..199117252 --- /dev/null +++ b/wasm/examples/nodejs/typescript/src/address.ts @@ -0,0 +1,68 @@ +import { + PrivateKey, + PublicKey, + PublicKeyGenerator, + createAddress, + NetworkType, + initConsolePanicHook +} from "../../../../nodejs/kaspa"; + +initConsolePanicHook(); + +(async () => { + /*** Common Use-cases ***/ + demoGenerateAddressFromPrivateKeyHexString(); + demoGenerateAddressFromPublicKeyHexString(); + + /*** Advanced ***/ + // HD Wallet-style public key generation + let xpub: PublicKeyGenerator = await PublicKeyGenerator.fromMasterXPrv( + "kprv5y2qurMHCsXYrNfU3GCihuwG3vMqFji7PZXajMEqyBkNh9UZUJgoHYBLTKu1eM4MvUtomcXPQ3Sw9HZ5ebbM4byoUciHo1zrPJBQfqpLorQ", + false, + 0n + ); + + console.log("xpub", xpub) + + // Generates the first 10 Receive Public keys and their addresses + let compressedPublicKeys: string[] = await xpub.receivePubkeys(0, 10); + let addresses: string[] = compressedPublicKeys.map(key => createAddress(key, NetworkType.Mainnet).toString()); + console.log("receive addresses", addresses); + + // Generates the first 10 Change Public keys and their addresses + let compressedChangePublicKeys: string[] = await xpub.changePubkeys(0, 10); + console.log("change address compressedChangePublicKeys", compressedChangePublicKeys) + addresses = compressedChangePublicKeys.map(key => createAddress(key, NetworkType.Mainnet).toString()); + console.log("change addresses", addresses); + +})(); + +// Getting Kaspa Address from Private Key +function demoGenerateAddressFromPrivateKeyHexString() { + // From Hex string + const privateKey = new PrivateKey('b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef'); // From BIP0340 + console.info(privateKey.toKeypair().toAddress(NetworkType.Mainnet).toString()); +} + +function demoGenerateAddressFromPublicKeyHexString() { + // Given compressed public key: '02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659' + const publicKey = new PublicKey('02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659'); + console.info("Given compressed public key: '02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659'"); + console.info(publicKey.toString()); + console.info(publicKey.toAddress(NetworkType.Mainnet).toString()); + console.info(publicKey.toAddress(NetworkType.Mainnet).toString() == 'kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva'); + + // Given x-only public key: 'dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659' + const xOnlyPublicKey = new PublicKey('dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659'); + console.info("Given x-only public key: 'dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659'"); + console.info(xOnlyPublicKey.toString()); + console.info(xOnlyPublicKey.toAddress(NetworkType.Mainnet).toString()); + console.info(xOnlyPublicKey.toAddress(NetworkType.Mainnet).toString() == 'kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva'); + + // Given full DER public key: '0421eb0c4270128b16c93c5f0dac48d56051a6237dae997b58912695052818e348b0a895cbd0c93a11ee7afac745929d96a4642a71831f54a7377893af71a2e2ae' + const fullDERPublicKey = new PublicKey('0421eb0c4270128b16c93c5f0dac48d56051a6237dae997b58912695052818e348b0a895cbd0c93a11ee7afac745929d96a4642a71831f54a7377893af71a2e2ae'); + console.info("Given x-only public key: '0421eb0c4270128b16c93c5f0dac48d56051a6237dae997b58912695052818e348b0a895cbd0c93a11ee7afac745929d96a4642a71831f54a7377893af71a2e2ae'"); + console.info(fullDERPublicKey.toString()); + console.info(fullDERPublicKey.toAddress(NetworkType.Mainnet).toString()); + console.info(fullDERPublicKey.toAddress(NetworkType.Mainnet).toString() == 'kaspa:qqs7krzzwqfgk9kf830smtzg64s9rf3r0khfj76cjynf2pfgrr35saatu88xq'); +} diff --git a/wasm/nodejs/utils.js b/wasm/examples/nodejs/typescript/src/utils.ts similarity index 88% rename from wasm/nodejs/utils.js rename to wasm/examples/nodejs/typescript/src/utils.ts index fb1321ce3..e668a1480 100644 --- a/wasm/nodejs/utils.js +++ b/wasm/examples/nodejs/typescript/src/utils.ts @@ -1,12 +1,12 @@ -const path = require('path'); -const nodeUtil = require('node:util'); +import * as path from 'path'; +import * as nodeUtil from 'node:util'; const { parseArgs: nodeParseArgs } = nodeUtil; -const { +import { Address, Encoding, NetworkId, -} = require('./kaspa/kaspa_wasm'); +} from "../../../../nodejs/kaspa"; /** * Helper function to parse command line arguments for running the scripts @@ -24,7 +24,8 @@ function parseArgs(options = { positionals, tokens, } = nodeParseArgs({ - args, options: { + args, + options: { ...options.additionalParseArgs, help: { type: 'boolean', @@ -41,7 +42,12 @@ function parseArgs(options = { encoding: { type: 'string', }, - }, tokens: true, allowPositionals: true + address:{ + type: 'string' + } + }, + tokens: true, + allowPositionals: true }); if (values.help) { console.log(`Usage: node ${script} [address] [mainnet|testnet] [--destination
] [--network ] [--encoding ] ${options.additionalHelpOutput}`); @@ -73,6 +79,6 @@ function parseArgs(options = { }; } -module.exports = { +export { parseArgs, -}; +} diff --git a/wasm/examples/nodejs/typescript/src/utxo-context-listener.ts b/wasm/examples/nodejs/typescript/src/utxo-context-listener.ts new file mode 100644 index 000000000..be3669195 --- /dev/null +++ b/wasm/examples/nodejs/typescript/src/utxo-context-listener.ts @@ -0,0 +1,109 @@ +// Run with: node demo.js +import { w3cwebsocket } from "websocket"; +(globalThis.WebSocket as any) = w3cwebsocket; + +import { + PrivateKey, + Address, + RpcClient, + Resolver, + UtxoProcessor, + UtxoContext, + kaspaToSompi, + createTransactions, + initConsolePanicHook, + IUtxoProcessorEvent, + IPendingEvent, + IDiscoveryEvent, + IMaturityEvent, + ITransactionRecord, + UtxoProcessorEventType, +} from "../../../../nodejs/kaspa"; + +import { parseArgs } from "./utils"; + + +initConsolePanicHook(); + + +let { encoding, networkId, destinationAddress } = parseArgs(); + +(async () => { + + const privateKey = new PrivateKey('b99d75736a0fd0ae2da658959813d680474f5a740a9c970a7da867141596178f'); + const sourceAddress = privateKey.toKeypair().toAddress(networkId); + let address = new Address("kaspa:qrxkessyzxkv5ve7rj7u36nxxvtt08lsknnd8e8zw3p7xsf8ck0cqeuyrhkp0") + address = new Address("kaspa:qpamkvhgh0kzx50gwvvp5xs8ktmqutcy3dfs9dc3w7lm9rq0zs76vf959mmrp"); + console.info(`Source address: ${sourceAddress}`); + console.info(`address: ${address}`); + + // if not destination is specified, send back to ourselves + destinationAddress = destinationAddress ?? sourceAddress; + console.info(`Destination address: ${destinationAddress}`); + + // 1) Initialize RPC + const rpc = new RpcClient({ + resolver: new Resolver(), + encoding, + networkId + }); + + // 2) Create UtxoProcessor, passing RPC to it + let processor = new UtxoProcessor({ rpc, networkId }); + await processor.start(); + + // 3) Create one of more UtxoContext, passing UtxoProcessor to it + // you can create UtxoContext objects as needed to monitor different + // address sets. + let context = new UtxoContext({ processor }); + + // 4) Register a listener with the UtxoProcessor::events + processor.addEventListener(({event, data}) => { + // handle the event + // since the event is a union type, you can switch on the + // event type and cast the data to the appropriate type + switch (event) { + //string enums are not working for @Matoo + //case UtxoProcessorEventType.Discovery: { + case "discovery": { + let record = data.record; + console.log("Discovery event record:", record); + } break; + //case UtxoProcessorEventType.Pending: { + case "pending":{ + let record = data.record; + console.log("Pending event record:", record); + } break; + //case UtxoProcessorEventType.Maturity: { + case "maturity": { + let record = data.record; + console.log("Maturity event record:", record); + } break; + default: { + console.log("Other event:", event, "data:", data); + } + } + }); + + processor.addEventListener("discovery", (data) => { + console.log("Discovery event record:", data.record); + }); + + console.log(processor); + + // 5) Once the environment is setup, connect to RPC + console.log(`Connecting to ${rpc.url}`); + await rpc.connect(); + + // for local nodes, wait for the node to sync + let { isSynced } = await rpc.getServerInfo(); + if (!isSynced) { + console.error("Please wait for the node to sync"); + rpc.disconnect(); + return; + } + + // 6) Register the address list with the UtxoContext + await context.trackAddresses([sourceAddress], undefined); + +})(); \ No newline at end of file diff --git a/wasm/examples/nodejs/typescript/src/version.ts b/wasm/examples/nodejs/typescript/src/version.ts new file mode 100644 index 000000000..ac603b009 --- /dev/null +++ b/wasm/examples/nodejs/typescript/src/version.ts @@ -0,0 +1,3 @@ +import { version } from "../../../../nodejs/kaspa"; + +console.log("version:", version()); diff --git a/wasm/examples/nodejs/typescript/src/wallet.ts b/wasm/examples/nodejs/typescript/src/wallet.ts new file mode 100644 index 000000000..d3ab8776f --- /dev/null +++ b/wasm/examples/nodejs/typescript/src/wallet.ts @@ -0,0 +1,16 @@ +import {version, Wallet} from "../../../../nodejs/kaspa"; + +import {w3cwebsocket} from "websocket"; +(globalThis.WebSocket as any) = w3cwebsocket; + +(async()=>{ + let wallet = new Wallet({resident: false}); + console.log("wallet", wallet) + let response = await wallet.walletCreate({ + walletSecret: "abc", + filename: "aaaaaa__xxx3", + title: "XX2" + }); + + console.log("response", response) +})(); \ No newline at end of file diff --git a/wasm/examples/nodejs/typescript/tsconfig.json b/wasm/examples/nodejs/typescript/tsconfig.json new file mode 100644 index 000000000..e7176b027 --- /dev/null +++ b/wasm/examples/nodejs/typescript/tsconfig.json @@ -0,0 +1,113 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + + "include": [ + "src/**/*.ts" + ] +} diff --git a/wasm/examples/package.json b/wasm/examples/package.json new file mode 100644 index 000000000..08aa14fc2 --- /dev/null +++ b/wasm/examples/package.json @@ -0,0 +1,12 @@ +{ + "name": "kaspa-wasm-examples", + "private": true, + "version": "1.0.0", + "dependencies": { + "websocket": "1.0.34", + "chalk": "5.3.0" + }, + "devDependencies": { + "@types/websocket": "^1.0.10" + } +} diff --git a/wasm/examples/web/browser-extension.html b/wasm/examples/web/browser-extension.html new file mode 100644 index 000000000..4c18772e6 --- /dev/null +++ b/wasm/examples/web/browser-extension.html @@ -0,0 +1,106 @@ + + + + + + + + + + \ No newline at end of file diff --git a/wasm/examples/web/get-block-dag-info.html b/wasm/examples/web/get-block-dag-info.html new file mode 100644 index 000000000..fa5d7f3c3 --- /dev/null +++ b/wasm/examples/web/get-block-dag-info.html @@ -0,0 +1,37 @@ + + + + + + + + \ No newline at end of file diff --git a/wasm/examples/web/get-server-info.html b/wasm/examples/web/get-server-info.html new file mode 100644 index 000000000..8e020db34 --- /dev/null +++ b/wasm/examples/web/get-server-info.html @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/wasm/examples/web/index.html b/wasm/examples/web/index.html new file mode 100644 index 000000000..522bc047d --- /dev/null +++ b/wasm/examples/web/index.html @@ -0,0 +1,89 @@ + + + + + + + + Rusty Kaspa WASM32 SDK v
 
+ Packages:
+
    + Examples: + + + + + + \ No newline at end of file diff --git a/wasm/examples/web/resources/ferris.svg b/wasm/examples/web/resources/ferris.svg new file mode 100644 index 000000000..5fe9e0730 --- /dev/null +++ b/wasm/examples/web/resources/ferris.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/wasm/examples/web/resources/kaspa.svg b/wasm/examples/web/resources/kaspa.svg new file mode 100644 index 000000000..f449fdfd5 --- /dev/null +++ b/wasm/examples/web/resources/kaspa.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/wasm/examples/web/resources/rust.svg b/wasm/examples/web/resources/rust.svg new file mode 100644 index 000000000..dd830c261 --- /dev/null +++ b/wasm/examples/web/resources/rust.svg @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/wasm/examples/web/resources/style.css b/wasm/examples/web/resources/style.css new file mode 100644 index 000000000..73774fc7b --- /dev/null +++ b/wasm/examples/web/resources/style.css @@ -0,0 +1,116 @@ +body { + margin: 0px; + padding: 32px; + font-size: 16px; + font-family: "Consolas", "Andale Mono", monospace; + /* white-space: pre-wrap; */ +} +@media (prefers-color-scheme: dark) { + body { + background: rgb(41, 41, 40); + color: rgb(188, 189, 229); + } +} + +code { + display: block; + font-size: inherit; + font-family: inherit; + white-space: pre-wrap; +} + +.banner { + opacity: 0.75; + position:fixed; + right: 32px; + top: 32px; +} + +span.number { + color: rgb(11, 103, 20); +} + +span.key { + color: rgb(22, 32, 110); +} + +span.string { + color: rgb(16, 115, 105); +} + +span.boolean { + color: rgb(91, 9, 0); +} + +span.keyword { + color: rgb(2, 83, 67); +} + +.network { + color: rgb(11, 103, 20); +} + +a, .link { + color: rgb(0, 156, 117); + text-decoration: none; +} + +a:hover, .link:hover { + color: rgb(80, 121, 111); + text-decoration: none; + cursor: pointer; +} +.link:disabled{ + opacity:0.5; + cursor:not-allowed +} + +input.address { + font-size: 14px; + font-family: "Consolas", "Andale Mono", monospace; + clear: both; + width : 600px; + height: 2.5rem; + line-height: 2.5rem; + border-radius: 10px; + border: 1px solid rgb(0, 156, 117); + box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; +} + +input::placeholder { + font-size: 14px; + font-family: "Consolas", "Andale Mono", monospace; +} + +div.button { + cursor: pointer; + user-select: none; + white-space: nowrap; + font-size: 15.2px; + height: 2.5rem; + line-height: 2.5rem; + width:fit-content; + padding: 0 1.5rem; + text-align: center; + border: 1px solid rgb(0, 156, 117); + border-radius: 10px; + box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; +} + +.wallet{ + padding:10px; + border:1px solid #DDD; + border-radius: 5px; + margin:2px; + display: inline-flex; + align-items: center; + cursor:pointer; +} +.wallet .title{ + font-size:16px; + margin-left:10px; +} +.wallet .icon{ + width:50px; + height:50px; +} \ No newline at end of file diff --git a/wasm/examples/web/resources/utils.js b/wasm/examples/web/resources/utils.js new file mode 100644 index 000000000..3f9cd8ae1 --- /dev/null +++ b/wasm/examples/web/resources/utils.js @@ -0,0 +1,136 @@ +document.body.innerHTML = +`<- Back | Network:
     
    +` +// ++ document.body.innerHTML; + +// @ts-ignore +String.prototype.color = function(color) { + console.log(this,color); + return `${this}`; +} + +// @ts-ignore +String.prototype.class = function(className) { + return `${this}`; +} + +function currentNetwork() { + return window.location.hash.replace(/^#/,'') || 'mainnet'; +} + +// @ts-ignore +window.changeNetwork = (network) => { + console.log("network",network); + window.location.hash = network; + location.reload(); +} + +function createMenu() { + let menu = document.getElementById('menu'); + [ 'mainnet', 'testnet-10', 'testnet-11' ].forEach((network) => { + if (network === currentNetwork()) { + let el = document.createElement('text'); + el.innerHTML = ` [${network}] `; + menu.appendChild(el); + } else { + + let el = document.createElement('a'); + el.id = network; + el.href = `javascript: changeNetwork("${network}")`; + el.innerHTML = ` ${network} `; + menu.appendChild(el); + } + }); +} + +document.addEventListener('DOMContentLoaded', () => { + createMenu(); +}); + +function disconnectHandler(rpc) { + // @ts-ignore + window.$rpc = rpc; + let actions = document.getElementById('actions'); + actions.innerHTML = ` | Disconnect`; +} + +// @ts-ignore +window.disconnect = async function() { + // @ts-ignore + await $rpc.disconnect(); + document.getElementById('actions').innerHTML = ` | Reconnect`; +} + +// @ts-ignore +window.reconnect = async function() { + document.getElementById('actions').innerHTML = ` | Connecting...`; + // @ts-ignore + await $rpc.connect(); + document.getElementById('actions').innerHTML = ` | Disconnect`; +} + +// Generate a random id +function randomId() { + return (Math.round(Math.random()*1e8)).toString(16); +} + +// Log to an element by its id +function logToId(id, ...args) { + let el = document.getElementById(id); + if (!el) { + el = document.createElement('code'); + el.id = id; + document.body.appendChild(el); + } + + el.innerHTML = args.map((arg) => { + return typeof arg === 'object' ? stringify(arg) : arg; + }).join(' ') + "
    "; +} + +// Clear the content of an element by its id +function clearId(id) { + if (id) { + let el = document.getElementById(id); + if (el) { + el.innerHTML = ''; + } + } +} + +function log(...args) { + let el = document.createElement('code'); + el.innerHTML = args.map((arg) => { + return typeof arg === 'object' ? stringify(arg) : arg; + }).join(' ') + "
    "; + document.body.appendChild(el); +} + +function stringify(json) { + if (typeof json != 'string') { + json = JSON.stringify(json, (k, v) => { return typeof v === "bigint" ? v.toString() + 'n' : v; }, 2); + } + json = json.replace(/&/g, '&').replace(//g, '>').replace(/"(\d+)n"/g,"$1n"); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?n?)/g, function (match) { + var cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + }); +} + +export { log, logToId, clearId, randomId, stringify, currentNetwork, disconnectHandler }; diff --git a/wasm/examples/web/resources/wasm.svg b/wasm/examples/web/resources/wasm.svg new file mode 100644 index 000000000..f2d67d77a --- /dev/null +++ b/wasm/examples/web/resources/wasm.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/wasm/examples/web/subscribe-block-added.html b/wasm/examples/web/subscribe-block-added.html new file mode 100644 index 000000000..3ad54a7bf --- /dev/null +++ b/wasm/examples/web/subscribe-block-added.html @@ -0,0 +1,64 @@ + + + + + + + + \ No newline at end of file diff --git a/wasm/examples/web/subscribe-daa-changed.html b/wasm/examples/web/subscribe-daa-changed.html new file mode 100644 index 000000000..5a4d18b11 --- /dev/null +++ b/wasm/examples/web/subscribe-daa-changed.html @@ -0,0 +1,58 @@ + + + + + + + + \ No newline at end of file diff --git a/wasm/examples/web/utxo-context.html b/wasm/examples/web/utxo-context.html new file mode 100644 index 000000000..380177a44 --- /dev/null +++ b/wasm/examples/web/utxo-context.html @@ -0,0 +1,97 @@ + + + + + + + + \ No newline at end of file diff --git a/wasm/examples/web/xpub.html b/wasm/examples/web/xpub.html new file mode 100644 index 000000000..b66e4d13c --- /dev/null +++ b/wasm/examples/web/xpub.html @@ -0,0 +1,54 @@ + + + + + + \ No newline at end of file diff --git a/wasm/index.html b/wasm/index.html new file mode 100644 index 000000000..1646b6882 --- /dev/null +++ b/wasm/index.html @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/wasm/nodejs/.gitignore b/wasm/nodejs/.gitignore index 25c8fdbab..f59ec20aa 100644 --- a/wasm/nodejs/.gitignore +++ b/wasm/nodejs/.gitignore @@ -1,2 +1 @@ -node_modules -package-lock.json \ No newline at end of file +* \ No newline at end of file diff --git a/wasm/nodejs/derivation.js b/wasm/nodejs/derivation.js deleted file mode 100644 index 46aed07ee..000000000 --- a/wasm/nodejs/derivation.js +++ /dev/null @@ -1,33 +0,0 @@ -const kaspa = require('./kaspa/kaspa_wasm'); -const { - Mnemonic, - XPrv, - DerivationPath -} = kaspa; - -kaspa.initConsolePanicHook(); - -(async () => { - - const mnemonic = Mnemonic.random(); - console.log("mnemonic:", mnemonic); - const seed = mnemonic.toSeed("my_password"); - console.log("seed:", seed); - - // --- - - const xPrv = new XPrv(seed); - console.log("xPrv", xPrv.intoString("xprv")) - - console.log("xPrv", xPrv.derivePath("m/1'/2'/3").intoString("xprv")) - - const path = new DerivationPath("m/1'"); - path.push(2, true); - path.push(3, false); - console.log(`path: ${path}`); - - console.log("xPrv", xPrv.derivePath(path).intoString("xprv")) - - const xPub = xPrv.publicKey(); - console.log("xPub", xPub.derivePath("m/1").intoString("xpub")); -})(); diff --git a/wasm/nodejs/package.json b/wasm/nodejs/package.json deleted file mode 100644 index c0ab8f8bb..000000000 --- a/wasm/nodejs/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name" : "kaspa-wasm-nodejs", - "version" : "1.0.0", - "dependencies": { - "websocket" : "1.0.34" - } -} diff --git a/wasm/npm/LICENSE b/wasm/npm/LICENSE index 261eeb9e9..b66757abc 100644 --- a/wasm/npm/LICENSE +++ b/wasm/npm/LICENSE @@ -1,201 +1,15 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +ISC License - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Copyright (c) 2022-2024 Kaspa developers - 1. Definitions. +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/wasm/npm/README.md b/wasm/npm/README.md index fbd21b8f3..0208405aa 100644 --- a/wasm/npm/README.md +++ b/wasm/npm/README.md @@ -1,10 +1,29 @@ # Kaspa WASM SDK -An integration wrapper around [`kaspa-wasm`](https://www.npmjs.com/package/kaspa-wasm) module that uses [`ws`](https://www.npmjs.com/package/ws) together with the [`isomorphic-ws`](https://www.npmjs.com/package/isomorphic-ws) w3c adaptor for WebSocket communication. +An integration wrapper around [`kaspa-wasm`](https://www.npmjs.com/package/kaspa-wasm) module that uses [`websocket`](https://www.npmjs.com/package/websocket) W3C adaptor for WebSocket communication. + +This is a Node.js module that provides bindings to the Kaspa WASM SDK strictly for use in the Node.js environment. The web browser version of the SDK is available as part of official SDK releases at [https://github.com/kaspanet/rusty-kaspa/releases](https://github.com/kaspanet/rusty-kaspa/releases) ## Usage -Kaspa module exports include all WASM32 bindings. +Kaspa NPM module exports include all WASM32 bindings. ```javascript const kaspa = require('kaspa'); -``` \ No newline at end of file +console.log(kaspa.version()); +``` + +## Documentation + +Documentation is available at [https://kaspa.aspectron.org/docs/](https://kaspa.aspectron.org/docs/) + + +## Building from source & Examples + +SDK examples as well as information on building the project from source can be found at [https://github.com/kaspanet/rusty-kaspa/tree/master/wasm](https://github.com/kaspanet/rusty-kaspa/tree/master/wasm) + +## Releases + +Official releases as well as releases for Web Browsers are available at [https://github.com/kaspanet/rusty-kaspa/releases](https://github.com/kaspanet/rusty-kaspa/releases). + +Nightly / developer builds are available at: [https://aspectron.org/en/projects/kaspa-wasm.html](https://aspectron.org/en/projects/kaspa-wasm.html) + diff --git a/wasm/npm/index.js b/wasm/npm/index.js index cba493d89..61f047cdc 100644 --- a/wasm/npm/index.js +++ b/wasm/npm/index.js @@ -1,3 +1,2 @@ -globalThis.WebSocket = require('isomorphic-ws'); -// globalThis.WebSocket = require('websocket').w3cwebsocket; +globalThis.WebSocket = require('websocket').w3cwebsocket; module.exports = require('./kaspa/kaspa_wasm'); diff --git a/wasm/npm/package.json b/wasm/npm/package.json index 7df05402c..c4a2dc59d 100644 --- a/wasm/npm/package.json +++ b/wasm/npm/package.json @@ -1,6 +1,6 @@ { "name": "kaspa", - "version": "0.13.0", + "version": "0.13.5", "description": "Kaspa SDK", "main": "index.js", "scripts": { @@ -16,14 +16,13 @@ "sdk" ], "author": "Kaspa developers", - "license": "Apache-2.0", + "license": "ISC", "bugs": { "url": "https://github.com/kaspanet/rusty-kaspa/issues" }, "homepage": "https://github.com/kaspanet/rusty-kaspa#readme", "dependencies": { - "isomorphic-ws": "5.0.0", - "ws": "8.14.2", - "kaspa-wasm": "0.13.0" + "websocket": "1.0.34", + "kaspa-wasm": "0.13.5" } } diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index cc2873aa7..912ed9428 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -40,8 +40,8 @@ available NPM modules: The `kaspa-wasm` module is a pure WASM32 module that includes the entire wallet framework, but does not support RPC due to an absence of a native WebSocket in NodeJs environment, while -the `kaspa` module includes `isomorphic-ws` dependency simulating -the W3C WebSocket and thus supports RPC. +the `kaspa` module includes `websocket` package dependency simulating +the W3C WebSocket and due to this supports RPC. ## Examples @@ -56,20 +56,18 @@ repository at . ## Using RPC -**NODEJS:** If you are building from source, to use WASM RPC client in the NodeJS environment, -you need to introduce a W3C WebSocket object before loading the WASM32 library. You can use -any Node.js module that exposes a W3C-compatible WebSocket implementation. Two of such modules -are [WebSocket](https://www.npmjs.com/package/websocket) (provides a custom implementation) -and [isomorphic-ws](https://www.npmjs.com/package/isomorphic-ws) (built on top of the ws -WebSocket module). +**NODEJS:** If you are building from source, to use WASM RPC client +in the NodeJS environment, you need to introduce a global W3C WebSocket +object before loading the WASM32 library (to simulate the browser behavior). +You can the [WebSocket](https://www.npmjs.com/package/websocket) +module that offers W3C WebSocket compatibility and is compatible +with Kaspa RPC implementation. You can use the following shims: ```js // WebSocket globalThis.WebSocket = require('websocket').w3cwebsocket; -// isomorphic-ws -globalThis.WebSocket = require('isomorphic-ws'); ``` ## Loading in a Web App @@ -107,6 +105,12 @@ let {RpcClient,Encoding,initConsolePanicHook} = require('./kaspa-rpc'); // if port is not specified, it will use the default port for the specified network const rpc = new RpcClient("127.0.0.1", Encoding.Borsh, "testnet-10"); +const rpc = new RpcClient({ + url : "127.0.0.1", + encoding : Encoding.Borsh, + networkId : "testnet-10" +}); + (async () => { try { @@ -125,25 +129,84 @@ For more details, please follow the [**integrating with Kaspa**](https://kaspa.a #![allow(unused_imports)] -pub mod utils; -pub use crate::utils::*; +#[cfg(all( + any(feature = "wasm32-sdk", feature = "wasm32-rpc", feature = "wasm32-core", feature = "wasm32-keygen"), + not(target_arch = "wasm32") +))] +compile_error!("`kaspa-wasm` crate for WASM32 target must be built with `--features wasm32-sdk|wasm32-rpc|wasm32-core|wasm32-keygen`"); -pub use kaspa_addresses::{Address, Version as AddressVersion}; -pub use kaspa_consensus_core::tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; -pub use kaspa_pow::wasm::*; +mod version; +pub use version::*; -pub mod rpc { - //! Kaspa RPC interface - //! +cfg_if::cfg_if! { - pub mod messages { - //! Kaspa RPC messages - pub use kaspa_rpc_core::model::message::*; - } - pub use kaspa_rpc_core::api::rpc::RpcApi; - pub use kaspa_wrpc_client::wasm::RpcClient; -} + if #[cfg(feature = "wasm32-sdk")] { + + pub use kaspa_addresses::{Address, Version as AddressVersion}; + pub use kaspa_consensus_core::tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; + pub use kaspa_pow::wasm::*; + + pub mod rpc { + //! Kaspa RPC interface + //! + + pub mod messages { + //! Kaspa RPC messages + pub use kaspa_rpc_core::model::message::*; + } + pub use kaspa_rpc_core::api::rpc::RpcApi; + pub use kaspa_rpc_core::wasm::message::*; + + pub use kaspa_wrpc_wasm::client::*; + pub use kaspa_wrpc_wasm::resolver::*; + pub use kaspa_wrpc_wasm::notify::*; + } + + pub use kaspa_consensus_wasm::*; + pub use kaspa_wallet_keys::prelude::*; + pub use kaspa_wallet_core::wasm::*; + + } else if #[cfg(feature = "wasm32-core")] { + + pub use kaspa_addresses::{Address, Version as AddressVersion}; + pub use kaspa_consensus_core::tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput}; + pub use kaspa_pow::wasm::*; -pub use kaspa_consensus_wasm::*; + pub mod rpc { + //! Kaspa RPC interface + //! -pub use kaspa_wallet_core::wasm::{tx::*, utils::*, utxo::*, wallet::*, xprivatekey::*, xpublickey::*}; + pub mod messages { + //! Kaspa RPC messages + pub use kaspa_rpc_core::model::message::*; + } + pub use kaspa_rpc_core::api::rpc::RpcApi; + pub use kaspa_rpc_core::wasm::message::*; + + pub use kaspa_wrpc_wasm::client::*; + pub use kaspa_wrpc_wasm::resolver::*; + pub use kaspa_wrpc_wasm::notify::*; + } + + pub use kaspa_consensus_wasm::*; + pub use kaspa_wallet_keys::prelude::*; + pub use kaspa_wallet_core::wasm::*; + + } else if #[cfg(feature = "wasm32-rpc")] { + + pub use kaspa_rpc_core::api::rpc::RpcApi; + pub use kaspa_rpc_core::wasm::message::*; + pub use kaspa_rpc_core::wasm::message::IPingRequest; + pub use kaspa_wrpc_wasm::client::*; + pub use kaspa_wrpc_wasm::resolver::*; + pub use kaspa_wrpc_wasm::notify::*; + pub use kaspa_wasm_core::types::*; + + } else if #[cfg(feature = "wasm32-keygen")] { + + pub use kaspa_addresses::{Address, Version as AddressVersion}; + pub use kaspa_wallet_keys::prelude::*; + pub use kaspa_wasm_core::types::*; + + } +} diff --git a/wasm/src/utils.rs b/wasm/src/utils.rs deleted file mode 100644 index 0651ccca2..000000000 --- a/wasm/src/utils.rs +++ /dev/null @@ -1,30 +0,0 @@ -use js_sys::BigInt; -use kaspa_math::Uint256; -use kaspa_utils::hex::ToHex; -use num::Float; -use wasm_bindgen::prelude::*; - -// https://github.com/tmrlvi/kaspa-miner/blob/bf361d02a46c580f55f46b5dfa773477634a5753/src/client/stratum.rs#L36 -const DIFFICULTY_1_TARGET: (u64, i16) = (0xffffu64, 208); // 0xffff 2^208 - -/// `calculate_difficulty` is based on set_difficulty function: -#[wasm_bindgen(js_name = calculateDifficulty)] -pub fn calculate_difficulty(difficulty: f32) -> Result { - let mut buf = [0u64, 0u64, 0u64, 0u64]; - let (mantissa, exponent, _) = difficulty.recip().integer_decode(); - let new_mantissa = mantissa * DIFFICULTY_1_TARGET.0; - let new_exponent = (DIFFICULTY_1_TARGET.1 + exponent) as u64; - let start = (new_exponent / 64) as usize; - let remainder = new_exponent % 64; - - buf[start] = new_mantissa << remainder; // bottom - if start < 3 { - buf[start + 1] = new_mantissa >> (64 - remainder); // top - } else if new_mantissa.leading_zeros() < remainder as u32 { - return Err(JsError::new("Target is too big")); - } - - // let target_pool = Uint256(buf); - // workflow_log::log_info!("Difficulty: {:?}, Target: 0x{}", difficulty, target_pool.to_hex()); - Ok(Uint256(buf).try_into()?) -} diff --git a/wasm/src/version.rs b/wasm/src/version.rs new file mode 100644 index 000000000..21ebbb8d7 --- /dev/null +++ b/wasm/src/version.rs @@ -0,0 +1,8 @@ +use wasm_bindgen::prelude::*; + +/// Returns the version of the Rusty Kaspa framework. +/// @category General +#[wasm_bindgen] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} diff --git a/wasm/web/.gitignore b/wasm/web/.gitignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/wasm/web/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file