diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 693266885..49fe4e463 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -126,6 +126,9 @@ jobs: - name: Run cargo doc tests with features=no-asm on kaspa-hashes run: cargo test --doc --release -p kaspa-hashes --features=no-asm + - name: Run cargo doc + run: cargo doc --release --no-deps --workspace + # test-release: # name: Test Suite Release # runs-on: ${{ matrix.os }} diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 8f46cb1fe..537eeef89 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -65,8 +65,8 @@ jobs: cp target/x86_64-unknown-linux-musl/release/kaspad bin/ cp target/x86_64-unknown-linux-musl/release/rothschild bin/ cp target/x86_64-unknown-linux-musl/release/kaspa-wallet bin/ - archive="bin/rusty-kaspa-${{ github.event.release.tag_name }}-linux-musl-amd64.zip" - asset_name="rusty-kaspa-${{ github.event.release.tag_name }}-linux-musl-amd64.zip" + archive="bin/rusty-kaspa-${{ github.event.release.tag_name }}-linux-amd64.zip" + asset_name="rusty-kaspa-${{ github.event.release.tag_name }}-linux-amd64.zip" zip -r "${archive}" ./bin/* echo "archive=${archive}" >> $GITHUB_ENV echo "asset_name=${asset_name}" >> $GITHUB_ENV @@ -76,12 +76,10 @@ jobs: shell: bash run: | cargo build --bin kaspad --release - cargo build --bin simpa --release cargo build --bin rothschild --release cargo build --bin kaspa-wallet --release mkdir bin || true cp target/release/kaspad.exe bin/ - cp target/release/simpa.exe bin/ cp target/release/rothschild.exe bin/ cp target/release/kaspa-wallet.exe bin/ archive="bin/rusty-kaspa-${{ github.event.release.tag_name }}-win64.zip" @@ -94,12 +92,10 @@ jobs: if: runner.os == 'macOS' run: | cargo build --bin kaspad --release - cargo build --bin simpa --release cargo build --bin rothschild --release cargo build --bin kaspa-wallet --release mkdir bin || true cp target/release/kaspad bin/ - cp target/release/simpa bin/ cp target/release/rothschild bin/ cp target/release/kaspa-wallet bin/ archive="bin/rusty-kaspa-${{ github.event.release.tag_name }}-osx.zip" diff --git a/Cargo.lock b/Cargo.lock index 7f23aca0f..8e9829fea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,18 +435,17 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", - "bitflags 1.3.2", "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", "itoa", "matchit", "memchr", @@ -455,25 +454,28 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper 0.1.2", - "tower", + "sync_wrapper 1.0.1", + "tower 0.4.13", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", ] @@ -493,12 +495,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -2008,12 +2004,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - [[package]] name = "httparse" version = "1.9.4" @@ -2069,6 +2059,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2086,24 +2077,25 @@ dependencies = [ "http 1.1.0", "hyper 1.4.1", "hyper-util", - "rustls 0.23.12", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", - "webpki-roots 0.26.5", + "webpki-roots", ] [[package]] name = "hyper-timeout" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ - "hyper 0.14.30", + "hyper 1.4.1", + "hyper-util", "pin-project-lite", "tokio", - "tokio-io-timeout", + "tower-service", ] [[package]] @@ -2121,7 +2113,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.7", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -2363,7 +2355,7 @@ dependencies = [ [[package]] name = "kaspa-addresses" -version = "0.15.1" +version = "0.15.2" dependencies = [ "borsh", "criterion", @@ -2380,7 +2372,7 @@ dependencies = [ [[package]] name = "kaspa-addressmanager" -version = "0.15.1" +version = "0.15.2" dependencies = [ "borsh", "igd-next", @@ -2403,14 +2395,14 @@ dependencies = [ [[package]] name = "kaspa-alloc" -version = "0.15.1" +version = "0.15.2" dependencies = [ "mimalloc", ] [[package]] name = "kaspa-bip32" -version = "0.15.1" +version = "0.15.2" dependencies = [ "borsh", "bs58", @@ -2437,7 +2429,7 @@ dependencies = [ [[package]] name = "kaspa-cli" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "borsh", @@ -2484,7 +2476,7 @@ dependencies = [ [[package]] name = "kaspa-connectionmanager" -version = "0.15.1" +version = "0.15.2" dependencies = [ "duration-string", "futures-util", @@ -2501,7 +2493,7 @@ dependencies = [ [[package]] name = "kaspa-consensus" -version = "0.15.1" +version = "0.15.2" dependencies = [ "arc-swap", "async-channel 2.3.1", @@ -2544,7 +2536,7 @@ dependencies = [ [[package]] name = "kaspa-consensus-client" -version = "0.15.1" +version = "0.15.2" dependencies = [ "ahash", "cfg-if 1.0.0", @@ -2572,7 +2564,7 @@ dependencies = [ [[package]] name = "kaspa-consensus-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "bincode", @@ -2610,7 +2602,7 @@ dependencies = [ [[package]] name = "kaspa-consensus-notify" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "cfg-if 1.0.0", @@ -2629,7 +2621,7 @@ dependencies = [ [[package]] name = "kaspa-consensus-wasm" -version = "0.15.1" +version = "0.15.2" dependencies = [ "cfg-if 1.0.0", "faster-hex", @@ -2653,7 +2645,7 @@ dependencies = [ [[package]] name = "kaspa-consensusmanager" -version = "0.15.1" +version = "0.15.2" dependencies = [ "duration-string", "futures", @@ -2671,7 +2663,7 @@ dependencies = [ [[package]] name = "kaspa-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "cfg-if 1.0.0", "ctrlc", @@ -2689,7 +2681,7 @@ dependencies = [ [[package]] name = "kaspa-daemon" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "borsh", @@ -2711,7 +2703,7 @@ dependencies = [ [[package]] name = "kaspa-database" -version = "0.15.1" +version = "0.15.2" dependencies = [ "bincode", "enum-primitive-derive", @@ -2733,7 +2725,7 @@ dependencies = [ [[package]] name = "kaspa-grpc-client" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-stream", @@ -2755,7 +2747,7 @@ dependencies = [ "prost", "rand 0.8.5", "regex", - "rustls 0.23.12", + "rustls", "thiserror", "tokio", "tokio-stream", @@ -2765,7 +2757,7 @@ dependencies = [ [[package]] name = "kaspa-grpc-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-stream", @@ -2794,7 +2786,7 @@ dependencies = [ [[package]] name = "kaspa-grpc-server" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-stream", @@ -2819,7 +2811,7 @@ dependencies = [ "paste", "prost", "rand 0.8.5", - "rustls 0.23.12", + "rustls", "thiserror", "tokio", "tokio-stream", @@ -2830,7 +2822,7 @@ dependencies = [ [[package]] name = "kaspa-hashes" -version = "0.15.1" +version = "0.15.2" dependencies = [ "blake2b_simd", "borsh", @@ -2851,7 +2843,7 @@ dependencies = [ [[package]] name = "kaspa-index-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-trait", @@ -2870,7 +2862,7 @@ dependencies = [ [[package]] name = "kaspa-index-processor" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-trait", @@ -2898,7 +2890,7 @@ dependencies = [ [[package]] name = "kaspa-math" -version = "0.15.1" +version = "0.15.2" dependencies = [ "borsh", "criterion", @@ -2919,14 +2911,14 @@ dependencies = [ [[package]] name = "kaspa-merkle" -version = "0.15.1" +version = "0.15.2" dependencies = [ "kaspa-hashes", ] [[package]] name = "kaspa-metrics-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "borsh", @@ -2942,7 +2934,7 @@ dependencies = [ [[package]] name = "kaspa-mining" -version = "0.15.1" +version = "0.15.2" dependencies = [ "criterion", "futures-util", @@ -2969,7 +2961,7 @@ dependencies = [ [[package]] name = "kaspa-mining-errors" -version = "0.15.1" +version = "0.15.2" dependencies = [ "kaspa-consensus-core", "thiserror", @@ -2977,7 +2969,7 @@ dependencies = [ [[package]] name = "kaspa-muhash" -version = "0.15.1" +version = "0.15.2" dependencies = [ "criterion", "kaspa-hashes", @@ -2990,7 +2982,7 @@ dependencies = [ [[package]] name = "kaspa-notify" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-trait", @@ -3026,7 +3018,7 @@ dependencies = [ [[package]] name = "kaspa-p2p-flows" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "chrono", @@ -3057,7 +3049,7 @@ dependencies = [ [[package]] name = "kaspa-p2p-lib" -version = "0.15.1" +version = "0.15.2" dependencies = [ "borsh", "ctrlc", @@ -3088,7 +3080,7 @@ dependencies = [ [[package]] name = "kaspa-perf-monitor" -version = "0.15.1" +version = "0.15.2" dependencies = [ "kaspa-core", "log", @@ -3100,7 +3092,7 @@ dependencies = [ [[package]] name = "kaspa-pow" -version = "0.15.1" +version = "0.15.2" dependencies = [ "criterion", "js-sys", @@ -3116,7 +3108,7 @@ dependencies = [ [[package]] name = "kaspa-rpc-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-trait", @@ -3158,7 +3150,7 @@ dependencies = [ [[package]] name = "kaspa-rpc-macros" -version = "0.15.1" +version = "0.15.2" dependencies = [ "convert_case 0.6.0", "proc-macro-error", @@ -3170,7 +3162,7 @@ dependencies = [ [[package]] name = "kaspa-rpc-service" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "kaspa-addresses", @@ -3199,7 +3191,7 @@ dependencies = [ [[package]] name = "kaspa-testing-integration" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "async-trait", @@ -3259,7 +3251,7 @@ dependencies = [ [[package]] name = "kaspa-txscript" -version = "0.15.1" +version = "0.15.2" dependencies = [ "blake2b_simd", "borsh", @@ -3291,7 +3283,7 @@ dependencies = [ [[package]] name = "kaspa-txscript-errors" -version = "0.15.1" +version = "0.15.2" dependencies = [ "secp256k1", "thiserror", @@ -3299,7 +3291,7 @@ dependencies = [ [[package]] name = "kaspa-utils" -version = "0.15.1" +version = "0.15.2" dependencies = [ "arc-swap", "async-channel 2.3.1", @@ -3335,21 +3327,23 @@ dependencies = [ [[package]] name = "kaspa-utils-tower" -version = "0.15.1" +version = "0.15.2" dependencies = [ + "bytes", "cfg-if 1.0.0", "futures", - "hyper 0.14.30", + "http-body 1.0.1", + "http-body-util", "log", "pin-project-lite", "tokio", - "tower", + "tower 0.5.1", "tower-http", ] [[package]] name = "kaspa-utxoindex" -version = "0.15.1" +version = "0.15.2" dependencies = [ "futures", "kaspa-consensus", @@ -3370,7 +3364,7 @@ dependencies = [ [[package]] name = "kaspa-wallet" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-std", "async-trait", @@ -3382,7 +3376,7 @@ dependencies = [ [[package]] name = "kaspa-wallet-cli-wasm" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "js-sys", @@ -3396,7 +3390,7 @@ dependencies = [ [[package]] name = "kaspa-wallet-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "aes", "ahash", @@ -3404,7 +3398,7 @@ dependencies = [ "async-channel 2.3.1", "async-std", "async-trait", - "base64 0.22.1", + "base64", "borsh", "cfb-mode", "cfg-if 1.0.0", @@ -3477,7 +3471,7 @@ dependencies = [ [[package]] name = "kaspa-wallet-keys" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "borsh", @@ -3510,7 +3504,7 @@ dependencies = [ [[package]] name = "kaspa-wallet-macros" -version = "0.15.1" +version = "0.15.2" dependencies = [ "convert_case 0.5.0", "proc-macro-error", @@ -3523,7 +3517,7 @@ dependencies = [ [[package]] name = "kaspa-wallet-pskt" -version = "0.15.1" +version = "0.15.2" dependencies = [ "bincode", "derive_builder", @@ -3550,7 +3544,7 @@ dependencies = [ [[package]] name = "kaspa-wasm" -version = "0.15.1" +version = "0.15.2" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -3578,7 +3572,7 @@ dependencies = [ [[package]] name = "kaspa-wasm-core" -version = "0.15.1" +version = "0.15.2" dependencies = [ "faster-hex", "hexplay", @@ -3589,7 +3583,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-client" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-std", "async-trait", @@ -3606,7 +3600,7 @@ dependencies = [ "paste", "rand 0.8.5", "regex", - "rustls 0.23.12", + "rustls", "serde", "serde-wasm-bindgen", "serde_json", @@ -3625,7 +3619,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-example-subscriber" -version = "0.15.1" +version = "0.15.2" dependencies = [ "ctrlc", "futures", @@ -3640,7 +3634,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-proxy" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "clap 4.5.16", @@ -3659,7 +3653,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-server" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-trait", "borsh", @@ -3675,7 +3669,7 @@ dependencies = [ "log", "num_cpus", "paste", - "rustls 0.23.12", + "rustls", "serde", "thiserror", "tokio", @@ -3687,7 +3681,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-simple-client-example" -version = "0.15.1" +version = "0.15.2" dependencies = [ "futures", "kaspa-rpc-core", @@ -3697,7 +3691,7 @@ dependencies = [ [[package]] name = "kaspa-wrpc-wasm" -version = "0.15.1" +version = "0.15.2" dependencies = [ "ahash", "async-std", @@ -3727,7 +3721,7 @@ dependencies = [ [[package]] name = "kaspad" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "cfg-if 1.0.0", @@ -4788,9 +4782,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" dependencies = [ "bytes", "prost-derive", @@ -4798,13 +4792,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" dependencies = [ "bytes", "heck", - "itertools 0.12.1", + "itertools 0.13.0", "log", "multimap", "once_cell", @@ -4819,12 +4813,12 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.75", @@ -4832,9 +4826,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" dependencies = [ "prost", ] @@ -4850,7 +4844,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.12", + "rustls", "socket2 0.5.7", "thiserror", "tokio", @@ -4867,7 +4861,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustc-hash 2.0.0", - "rustls 0.23.12", + "rustls", "slab", "thiserror", "tinyvec", @@ -5067,7 +5061,7 @@ version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-core", @@ -5087,8 +5081,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.12", - "rustls-pemfile 2.1.3", + "rustls", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -5096,13 +5090,13 @@ dependencies = [ "sync_wrapper 1.0.1", "system-configuration", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.26.5", + "webpki-roots", "windows-registry", ] @@ -5151,7 +5145,7 @@ dependencies = [ [[package]] name = "rothschild" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "clap 4.5.16", @@ -5227,48 +5221,28 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.6", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ - "base64 0.22.1", + "base64", "rustls-pki-types", ] @@ -5278,16 +5252,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.102.6" @@ -5341,16 +5305,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "secp256k1" version = "0.29.0" @@ -5486,7 +5440,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ - "base64 0.22.1", + "base64", "chrono", "hex", "indexmap 1.9.3", @@ -5603,7 +5557,7 @@ dependencies = [ [[package]] name = "simpa" -version = "0.15.1" +version = "0.15.2" dependencies = [ "async-channel 2.3.1", "cfg-if 1.0.0", @@ -5985,16 +5939,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" version = "2.4.0" @@ -6006,23 +5950,13 @@ dependencies = [ "syn 2.0.75", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.12", + "rustls", "rustls-pki-types", "tokio", ] @@ -6046,12 +5980,12 @@ checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", - "rustls 0.23.12", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tungstenite", - "webpki-roots 0.26.5", + "webpki-roots", ] [[package]] @@ -6114,41 +6048,43 @@ dependencies = [ [[package]] name = "tonic" -version = "0.10.2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +checksum = "c6f6ba989e4b2c58ae83d862d3a3e27690b6e3ae630d0deb59f3697f32aa88ad" dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.21.7", + "base64", "bytes", "flate2", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", "prost", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls-pemfile", + "socket2 0.5.7", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", - "webpki-roots 0.25.4", + "webpki-roots", ] [[package]] name = "tonic-build" -version = "0.10.2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" +checksum = "fe4ee8877250136bd7e3d2331632810a4df4ea5e004656990d8d66d2f5ee8a67" dependencies = [ "prettyplease", "proc-macro2", @@ -6177,19 +6113,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags 2.6.0", "bytes", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", "pin-project-lite", "tower-layer", "tower-service", @@ -6213,7 +6157,6 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6264,7 +6207,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "rustls 0.23.12", + "rustls", "rustls-pki-types", "sha1", "thiserror", @@ -6581,12 +6524,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "webpki-roots" version = "0.26.5" @@ -7156,7 +7093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d161c4b844eee479f81306f2526266f9608a663e0a679d9fc0572ee15c144e06" dependencies = [ "async-std", - "base64 0.22.1", + "base64", "cfg-if 1.0.0", "chrome-sys", "faster-hex", diff --git a/Cargo.toml b/Cargo.toml index eaf07936e..8e6da754a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ members = [ [workspace.package] rust-version = "1.81.0" -version = "0.15.1" +version = "0.15.2" authors = ["Kaspa developers"] license = "ISC" repository = "https://github.com/kaspanet/rusty-kaspa" @@ -80,61 +80,61 @@ include = [ ] [workspace.dependencies] -# kaspa-testing-integration = { version = "0.15.1", path = "testing/integration" } -kaspa-addresses = { version = "0.15.1", path = "crypto/addresses" } -kaspa-addressmanager = { version = "0.15.1", path = "components/addressmanager" } -kaspa-bip32 = { version = "0.15.1", path = "wallet/bip32" } -kaspa-cli = { version = "0.15.1", path = "cli" } -kaspa-connectionmanager = { version = "0.15.1", path = "components/connectionmanager" } -kaspa-consensus = { version = "0.15.1", path = "consensus" } -kaspa-consensus-core = { version = "0.15.1", path = "consensus/core" } -kaspa-consensus-client = { version = "0.15.1", path = "consensus/client" } -kaspa-consensus-notify = { version = "0.15.1", path = "consensus/notify" } -kaspa-consensus-wasm = { version = "0.15.1", path = "consensus/wasm" } -kaspa-consensusmanager = { version = "0.15.1", path = "components/consensusmanager" } -kaspa-core = { version = "0.15.1", path = "core" } -kaspa-daemon = { version = "0.15.1", path = "daemon" } -kaspa-database = { version = "0.15.1", path = "database" } -kaspa-grpc-client = { version = "0.15.1", path = "rpc/grpc/client" } -kaspa-grpc-core = { version = "0.15.1", path = "rpc/grpc/core" } -kaspa-grpc-server = { version = "0.15.1", path = "rpc/grpc/server" } -kaspa-hashes = { version = "0.15.1", path = "crypto/hashes" } -kaspa-index-core = { version = "0.15.1", path = "indexes/core" } -kaspa-index-processor = { version = "0.15.1", path = "indexes/processor" } -kaspa-math = { version = "0.15.1", path = "math" } -kaspa-merkle = { version = "0.15.1", path = "crypto/merkle" } -kaspa-metrics-core = { version = "0.15.1", path = "metrics/core" } -kaspa-mining = { version = "0.15.1", path = "mining" } -kaspa-mining-errors = { version = "0.15.1", path = "mining/errors" } -kaspa-muhash = { version = "0.15.1", path = "crypto/muhash" } -kaspa-notify = { version = "0.15.1", path = "notify" } -kaspa-p2p-flows = { version = "0.15.1", path = "protocol/flows" } -kaspa-p2p-lib = { version = "0.15.1", path = "protocol/p2p" } -kaspa-perf-monitor = { version = "0.15.1", path = "metrics/perf_monitor" } -kaspa-pow = { version = "0.15.1", path = "consensus/pow" } -kaspa-rpc-core = { version = "0.15.1", path = "rpc/core" } -kaspa-rpc-macros = { version = "0.15.1", path = "rpc/macros" } -kaspa-rpc-service = { version = "0.15.1", path = "rpc/service" } -kaspa-txscript = { version = "0.15.1", path = "crypto/txscript" } -kaspa-txscript-errors = { version = "0.15.1", path = "crypto/txscript/errors" } -kaspa-utils = { version = "0.15.1", path = "utils" } -kaspa-utils-tower = { version = "0.15.1", path = "utils/tower" } -kaspa-utxoindex = { version = "0.15.1", path = "indexes/utxoindex" } -kaspa-wallet = { version = "0.15.1", path = "wallet/native" } -kaspa-wallet-cli-wasm = { version = "0.15.1", path = "wallet/wasm" } -kaspa-wallet-keys = { version = "0.15.1", path = "wallet/keys" } -kaspa-wallet-pskt = { version = "0.15.1", path = "wallet/pskt" } -kaspa-wallet-core = { version = "0.15.1", path = "wallet/core" } -kaspa-wallet-macros = { version = "0.15.1", path = "wallet/macros" } -kaspa-wasm = { version = "0.15.1", path = "wasm" } -kaspa-wasm-core = { version = "0.15.1", path = "wasm/core" } -kaspa-wrpc-client = { version = "0.15.1", path = "rpc/wrpc/client" } -kaspa-wrpc-proxy = { version = "0.15.1", path = "rpc/wrpc/proxy" } -kaspa-wrpc-server = { version = "0.15.1", path = "rpc/wrpc/server" } -kaspa-wrpc-wasm = { version = "0.15.1", path = "rpc/wrpc/wasm" } -kaspa-wrpc-example-subscriber = { version = "0.15.1", path = "rpc/wrpc/examples/subscriber" } -kaspad = { version = "0.15.1", path = "kaspad" } -kaspa-alloc = { version = "0.15.1", path = "utils/alloc" } +# kaspa-testing-integration = { version = "0.15.2", path = "testing/integration" } +kaspa-addresses = { version = "0.15.2", path = "crypto/addresses" } +kaspa-addressmanager = { version = "0.15.2", path = "components/addressmanager" } +kaspa-bip32 = { version = "0.15.2", path = "wallet/bip32" } +kaspa-cli = { version = "0.15.2", path = "cli" } +kaspa-connectionmanager = { version = "0.15.2", path = "components/connectionmanager" } +kaspa-consensus = { version = "0.15.2", path = "consensus" } +kaspa-consensus-core = { version = "0.15.2", path = "consensus/core" } +kaspa-consensus-client = { version = "0.15.2", path = "consensus/client" } +kaspa-consensus-notify = { version = "0.15.2", path = "consensus/notify" } +kaspa-consensus-wasm = { version = "0.15.2", path = "consensus/wasm" } +kaspa-consensusmanager = { version = "0.15.2", path = "components/consensusmanager" } +kaspa-core = { version = "0.15.2", path = "core" } +kaspa-daemon = { version = "0.15.2", path = "daemon" } +kaspa-database = { version = "0.15.2", path = "database" } +kaspa-grpc-client = { version = "0.15.2", path = "rpc/grpc/client" } +kaspa-grpc-core = { version = "0.15.2", path = "rpc/grpc/core" } +kaspa-grpc-server = { version = "0.15.2", path = "rpc/grpc/server" } +kaspa-hashes = { version = "0.15.2", path = "crypto/hashes" } +kaspa-index-core = { version = "0.15.2", path = "indexes/core" } +kaspa-index-processor = { version = "0.15.2", path = "indexes/processor" } +kaspa-math = { version = "0.15.2", path = "math" } +kaspa-merkle = { version = "0.15.2", path = "crypto/merkle" } +kaspa-metrics-core = { version = "0.15.2", path = "metrics/core" } +kaspa-mining = { version = "0.15.2", path = "mining" } +kaspa-mining-errors = { version = "0.15.2", path = "mining/errors" } +kaspa-muhash = { version = "0.15.2", path = "crypto/muhash" } +kaspa-notify = { version = "0.15.2", path = "notify" } +kaspa-p2p-flows = { version = "0.15.2", path = "protocol/flows" } +kaspa-p2p-lib = { version = "0.15.2", path = "protocol/p2p" } +kaspa-perf-monitor = { version = "0.15.2", path = "metrics/perf_monitor" } +kaspa-pow = { version = "0.15.2", path = "consensus/pow" } +kaspa-rpc-core = { version = "0.15.2", path = "rpc/core" } +kaspa-rpc-macros = { version = "0.15.2", path = "rpc/macros" } +kaspa-rpc-service = { version = "0.15.2", path = "rpc/service" } +kaspa-txscript = { version = "0.15.2", path = "crypto/txscript" } +kaspa-txscript-errors = { version = "0.15.2", path = "crypto/txscript/errors" } +kaspa-utils = { version = "0.15.2", path = "utils" } +kaspa-utils-tower = { version = "0.15.2", path = "utils/tower" } +kaspa-utxoindex = { version = "0.15.2", path = "indexes/utxoindex" } +kaspa-wallet = { version = "0.15.2", path = "wallet/native" } +kaspa-wallet-cli-wasm = { version = "0.15.2", path = "wallet/wasm" } +kaspa-wallet-keys = { version = "0.15.2", path = "wallet/keys" } +kaspa-wallet-pskt = { version = "0.15.2", path = "wallet/pskt" } +kaspa-wallet-core = { version = "0.15.2", path = "wallet/core" } +kaspa-wallet-macros = { version = "0.15.2", path = "wallet/macros" } +kaspa-wasm = { version = "0.15.2", path = "wasm" } +kaspa-wasm-core = { version = "0.15.2", path = "wasm/core" } +kaspa-wrpc-client = { version = "0.15.2", path = "rpc/wrpc/client" } +kaspa-wrpc-proxy = { version = "0.15.2", path = "rpc/wrpc/proxy" } +kaspa-wrpc-server = { version = "0.15.2", path = "rpc/wrpc/server" } +kaspa-wrpc-wasm = { version = "0.15.2", path = "rpc/wrpc/wasm" } +kaspa-wrpc-example-subscriber = { version = "0.15.2", path = "rpc/wrpc/examples/subscriber" } +kaspad = { version = "0.15.2", path = "kaspad" } +kaspa-alloc = { version = "0.15.2", path = "utils/alloc" } # external aes = "0.8.3" @@ -150,6 +150,7 @@ bincode = { version = "1.3.3", default-features = false } blake2b_simd = "1.0.2" borsh = { version = "1.5.1", features = ["derive", "rc"] } bs58 = { version = "0.5.0", features = ["check"], default-features = false } +bytes = "1.7.1" cc = "1.0.83" cfb-mode = "0.8.2" cfg-if = "1.0.0" @@ -188,6 +189,8 @@ hex-literal = "0.4.1" hexplay = "0.3.0" hmac = { version = "0.12.1", default-features = false } home = "0.5.5" +http-body = "1.0.1" +http-body-util = "0.1.2" igd-next = { version = "0.14.2", features = ["aio_tokio"] } indexmap = "2.1.0" intertrait = "0.2.2" @@ -211,8 +214,7 @@ parking_lot = "0.12.1" paste = "1.0.14" pbkdf2 = "0.12.2" portable-atomic = { version = "1.5.1", features = ["float"] } -prost = "0.12.1" -# prost = "0.13.1" +prost = "0.13.2" rand = "0.8.5" rand_chacha = "0.3.1" rand_core = { version = "0.6.4", features = ["std"] } @@ -251,8 +253,8 @@ 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-webpki-roots", "gzip", "transport"] } -tonic-build = { version = "0.10.2", features = ["prost"] } +tonic = { version = "0.12.2", features = ["tls-webpki-roots", "gzip", "transport"] } +tonic-build = { version = "0.12.2", features = ["prost"] } triggered = "0.1.2" uuid = { version = "1.5.0", features = ["v4", "fast-rng", "serde"] } wasm-bindgen = { version = "0.2.93", features = ["serde-serialize"] } @@ -262,12 +264,11 @@ web-sys = "0.3.70" xxhash-rust = { version = "0.8.7", features = ["xxh3"] } zeroize = { version = "1.6.0", default-features = false, features = ["alloc"] } pin-project-lite = "0.2.13" -tower-http = { version = "0.4.4", features = [ +tower-http = { version = "0.5.2", features = [ "map-response-body", "map-request-body", ] } -tower = "0.4.7" -hyper = "0.14.27" +tower = "0.5.1" chrono = "0.4.31" indexed_db_futures = "0.5.0" # workflow dependencies that are not a part of core libraries diff --git a/README.md b/README.md index ada38c55d..8749c49e9 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,15 @@ The default branch of this repository is `master` and new contributions are cons ## Installation
Building on Linux - + 1. Install general prerequisites ```bash - sudo apt install curl git build-essential libssl-dev pkg-config + sudo apt install curl git build-essential libssl-dev pkg-config ``` 2. Install Protobuf (required for gRPC) - + ```bash sudo apt install protobuf-compiler libprotobuf-dev #Required for gRPC ``` @@ -36,8 +36,8 @@ The default branch of this repository is `master` and new contributions are cons llvm python3-clang ``` 3. Install the [rust toolchain](https://rustup.rs/) - - If you already have rust installed, update it by running: `rustup update` + + If you already have rust installed, update it by running: `rustup update` 4. Install wasm-pack ```bash cargo install wasm-pack @@ -45,7 +45,7 @@ The default branch of this repository is `master` and new contributions are cons 4. Install wasm32 target ```bash rustup target add wasm32-unknown-unknown - ``` + ``` 5. Clone the repo ```bash git clone https://github.com/kaspanet/rusty-kaspa @@ -55,7 +55,7 @@ The default branch of this repository is `master` and new contributions are cons -
+
Building on Windows @@ -63,18 +63,18 @@ The default branch of this repository is `master` and new contributions are cons 2. Install [Protocol Buffers](https://github.com/protocolbuffers/protobuf/releases/download/v21.10/protoc-21.10-win64.zip) and add the `bin` directory to your `Path` - + 3. Install [LLVM-15.0.6-win64.exe](https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.6/LLVM-15.0.6-win64.exe) Add the `bin` directory of the LLVM installation (`C:\Program Files\LLVM\bin`) to PATH - + set `LIBCLANG_PATH` environment variable to point to the `bin` directory as well **IMPORTANT:** Due to C++ dependency configuration issues, LLVM `AR` installation on Windows may not function correctly when switching between WASM and native C++ code compilation (native `RocksDB+secp256k1` vs WASM32 builds of `secp256k1`). Unfortunately, manually setting `AR` environment variable also confuses C++ build toolchain (it should not be set for native but should be set for WASM32 targets). Currently, the best way to address this, is as follows: after installing LLVM on Windows, go to the target `bin` installation directory and copy or rename `LLVM_AR.exe` to `AR.exe`. - + 4. Install the [rust toolchain](https://rustup.rs/) - - If you already have rust installed, update it by running: `rustup update` + + If you already have rust installed, update it by running: `rustup update` 5. Install wasm-pack ```bash cargo install wasm-pack @@ -82,16 +82,16 @@ The default branch of this repository is `master` and new contributions are cons 6. Install wasm32 target ```bash rustup target add wasm32-unknown-unknown - ``` + ``` 7. Clone the repo ```bash git clone https://github.com/kaspanet/rusty-kaspa cd rusty-kaspa ``` -
+
-
+
Building on Mac OS @@ -99,8 +99,8 @@ The default branch of this repository is `master` and new contributions are cons ```bash brew install protobuf ``` - 2. Install llvm. - + 2. Install llvm. + The default XCode installation of `llvm` does not support WASM build targets. To build WASM on MacOS you need to install `llvm` from homebrew (at the time of writing, the llvm version for MacOS is 16.0.1). ```bash @@ -133,8 +133,8 @@ To build WASM on MacOS you need to install `llvm` from homebrew (at the time of source ~/.zshrc ``` 3. Install the [rust toolchain](https://rustup.rs/) - - If you already have rust installed, update it by running: `rustup update` + + If you already have rust installed, update it by running: `rustup update` 4. Install wasm-pack ```bash cargo install wasm-pack @@ -142,14 +142,14 @@ To build WASM on MacOS you need to install `llvm` from homebrew (at the time of 4. Install wasm32 target ```bash rustup target add wasm32-unknown-unknown - ``` + ``` 5. Clone the repo ```bash git clone https://github.com/kaspanet/rusty-kaspa cd rusty-kaspa ``` -
+
@@ -235,7 +235,7 @@ cargo run --release --bin kaspad -- --testnet ``` **Testnet 11** - + For participation in the 10BPS test network (TN11), see the following detailed [guide](docs/testnet11.md).
@@ -249,7 +249,7 @@ cargo run --release --bin kaspad -- --configfile /path/to/configfile.toml # or cargo run --release --bin kaspad -- -C /path/to/configfile.toml ``` - - The config file should be a list of \ = \ separated by newlines. + - The config file should be a list of \ = \ separated by newlines. - Whitespace around the `=` is fine, `arg=value` and `arg = value` are both parsed correctly. - Values with special characters like `.` or `=` will require quoting the value i.e \ = "\". - Arguments with multiple values should be surrounded with brackets like `addpeer = ["10.0.0.1", "1.2.3.4"]`. @@ -297,17 +297,17 @@ wRPC **Sidenote:** Rusty Kaspa integrates an optional wRPC - subsystem. wRPC is a high-performance, platform-neutral, Rust-centric, WebSocket-framed RPC + subsystem. wRPC is a high-performance, platform-neutral, Rust-centric, WebSocket-framed RPC implementation that can use [Borsh](https://borsh.io/) and JSON protocol encoding. - JSON protocol messaging - is similar to JSON-RPC 1.0, but differs from the specification due to server-side + JSON protocol messaging + is similar to JSON-RPC 1.0, but differs from the specification due to server-side notifications. [Borsh](https://borsh.io/) encoding is meant for inter-process communication. When using [Borsh](https://borsh.io/) - both client and server should be built from the same codebase. + both client and server should be built from the same codebase. - JSON protocol is based on + JSON protocol is based on Kaspa data structures and is data-structure-version agnostic. You can connect to the JSON endpoint using any WebSocket library. Built-in RPC clients for JavaScript and TypeScript capable of running in web browsers and Node.js are available as a part of @@ -316,27 +316,23 @@ wRPC
- -
- - ## Benchmarking & Testing -
+
Simulation framework (Simpa) Logging in `kaspad` and `simpa` can be [filtered](https://docs.rs/env_logger/0.10.0/env_logger/#filtering-results) by either: -The current codebase supports a full in-process network simulation, building an actual DAG over virtual time with virtual delay and benchmarking validation time (following the simulation generation). +The current codebase supports a full in-process network simulation, building an actual DAG over virtual time with virtual delay and benchmarking validation time (following the simulation generation). To see the available commands -```bash +```bash cargo run --release --bin simpa -- --help -``` +``` -The following command will run a simulation to produce 1000 blocks with communication delay of 2 seconds and 8 BPS (blocks per second) while attempting to fill each block with up to 200 transactions. +The following command will run a simulation to produce 1000 blocks with communication delay of 2 seconds and 8 BPS (blocks per second) while attempting to fill each block with up to 200 transactions. ```bash cargo run --release --bin simpa -- -t=200 -d=2 -b=8 -n=1000 @@ -347,7 +343,7 @@ cargo run --release --bin simpa -- -t=200 -d=2 -b=8 -n=1000 -
+
Heap Profiling @@ -362,7 +358,7 @@ It will produce `{bin-name}-heap.json` file in the root of the workdir, that can
-
+
Tests @@ -384,12 +380,21 @@ cd rusty-kaspa cargo nextest run --release ``` +
+
+ +Lints + +```bash +cd rusty-kaspa +./check +```
-
+
Benchmarks @@ -400,7 +405,7 @@ cargo bench
-
+
Logging @@ -415,4 +420,3 @@ Logging in `kaspad` and `simpa` can be [filtered](https://docs.rs/env_logger/0.1 In this command we set the `loglevel` to `INFO`.
- diff --git a/cli/src/modules/connect.rs b/cli/src/modules/connect.rs index 024f7e693..a755915d4 100644 --- a/cli/src/modules/connect.rs +++ b/cli/src/modules/connect.rs @@ -14,11 +14,11 @@ impl Connect { 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) + (true, Resolver::default().get_url(WrpcEncoding::Borsh, network_id).await.map_err(|e| e.to_string())?) } 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) + (true, Resolver::default().get_url(WrpcEncoding::Borsh, network_id).await.map_err(|e| e.to_string())?) } Some(url) => { (false, wrpc_client.parse_url_with_network_type(url.to_string(), network_id.into()).map_err(|e| e.to_string())?) diff --git a/cli/src/modules/rpc.rs b/cli/src/modules/rpc.rs index f32523c4a..cf6bc6bd2 100644 --- a/cli/src/modules/rpc.rs +++ b/cli/src/modules/rpc.rs @@ -114,17 +114,28 @@ impl Rpc { } let hash = argv.remove(0); let hash = RpcHash::from_hex(hash.as_str())?; - let result = rpc.get_block_call(None, GetBlockRequest { hash, include_transactions: true }).await?; + let include_transactions = argv.first().and_then(|x| x.parse::().ok()).unwrap_or(true); + let result = rpc.get_block_call(None, GetBlockRequest { hash, include_transactions }).await?; self.println(&ctx, result); } // RpcApiOps::GetSubnetwork => { // let result = rpc.get_subnetwork_call(GetSubnetworkRequest { }).await?; // self.println(&ctx, result); // } - // RpcApiOps::GetVirtualChainFromBlock => { - // let result = rpc.get_virtual_chain_from_block_call(GetVirtualChainFromBlockRequest { }).await?; - // self.println(&ctx, result); - // } + RpcApiOps::GetVirtualChainFromBlock => { + if argv.is_empty() { + return Err(Error::custom("Missing startHash argument")); + }; + let start_hash = RpcHash::from_hex(argv.remove(0).as_str())?; + let include_accepted_transaction_ids = argv.first().and_then(|x| x.parse::().ok()).unwrap_or_default(); + let result = rpc + .get_virtual_chain_from_block_call( + None, + GetVirtualChainFromBlockRequest { start_hash, include_accepted_transaction_ids }, + ) + .await?; + self.println(&ctx, result); + } // RpcApiOps::GetBlocks => { // let result = rpc.get_blocks_call(GetBlocksRequest { }).await?; // self.println(&ctx, result); diff --git a/components/consensusmanager/src/lib.rs b/components/consensusmanager/src/lib.rs index 54bdda40b..6d31653aa 100644 --- a/components/consensusmanager/src/lib.rs +++ b/components/consensusmanager/src/lib.rs @@ -9,7 +9,8 @@ mod session; pub use batch::BlockProcessingBatch; pub use session::{ - spawn_blocking, ConsensusInstance, ConsensusProxy, ConsensusSessionBlocking, SessionLock, SessionReadGuard, SessionWriteGuard, + spawn_blocking, ConsensusInstance, ConsensusProxy, ConsensusSessionBlocking, ConsensusSessionOwned, SessionLock, SessionReadGuard, + SessionWriteGuard, }; /// Consensus controller trait. Includes methods required to start/stop/control consensus, but which should not diff --git a/components/consensusmanager/src/session.rs b/components/consensusmanager/src/session.rs index 81d589148..8e0c6e933 100644 --- a/components/consensusmanager/src/session.rs +++ b/components/consensusmanager/src/session.rs @@ -91,7 +91,7 @@ impl ConsensusInstance { /// Returns an unguarded *blocking* consensus session. There's no guarantee that data will not be pruned between /// two sequential consensus calls. This session doesn't hold the consensus pruning lock, so it should - /// be preferred upon [`session_blocking`] when data consistency is not important. + /// be preferred upon [`session_blocking()`](Self::session_blocking) when data consistency is not important. pub fn unguarded_session_blocking(&self) -> ConsensusSessionBlocking<'static> { ConsensusSessionBlocking::new_without_session_guard(self.consensus.clone()) } @@ -100,7 +100,7 @@ impl ConsensusInstance { /// that consensus state is consistent between operations, that is, no pruning was performed between the calls. /// The returned object is an *owned* consensus session type which can be cloned and shared across threads. /// The sharing ability is useful for spawning blocking operations on a different thread using the same - /// session object, see [`ConsensusSessionOwned::spawn_blocking`]. The caller is responsible to make sure + /// session object, see [`ConsensusSessionOwned::spawn_blocking()`](ConsensusSessionOwned::spawn_blocking). The caller is responsible to make sure /// that the overall lifetime of this session is not too long (~2 seconds max) pub async fn session(&self) -> ConsensusSessionOwned { let g = self.session_lock.read_owned().await; @@ -109,7 +109,7 @@ impl ConsensusInstance { /// Returns an unguarded consensus session. There's no guarantee that data will not be pruned between /// two sequential consensus calls. This session doesn't hold the consensus pruning lock, so it should - /// be preferred upon [`session`] when data consistency is not important. + /// be preferred upon [`session()`](Self::session) when data consistency is not important. pub fn unguarded_session(&self) -> ConsensusSessionOwned { ConsensusSessionOwned::new_without_session_guard(self.consensus.clone()) } @@ -139,7 +139,8 @@ impl Deref for ConsensusSessionBlocking<'_> { } /// An *owned* consensus session type which can be cloned and shared across threads. -/// See method `spawn_blocking` within for context on the usefulness of this type +/// See method `spawn_blocking` within for context on the usefulness of this type. +/// Please note - you must use [`ConsensusProxy`] type alias instead of this struct. #[derive(Clone)] pub struct ConsensusSessionOwned { _session_guard: Option, @@ -267,8 +268,12 @@ impl ConsensusSessionOwned { self.clone().spawn_blocking(|c| c.is_nearly_synced()).await } - pub async fn async_get_virtual_chain_from_block(&self, hash: Hash) -> ConsensusResult { - self.clone().spawn_blocking(move |c| c.get_virtual_chain_from_block(hash)).await + pub async fn async_get_virtual_chain_from_block( + &self, + low: Hash, + chain_path_added_limit: Option, + ) -> ConsensusResult { + self.clone().spawn_blocking(move |c| c.get_virtual_chain_from_block(low, chain_path_added_limit)).await } pub async fn async_get_virtual_utxos( @@ -380,8 +385,12 @@ impl ConsensusSessionOwned { /// Returns acceptance data for a set of blocks belonging to the selected parent chain. /// /// See `self::get_virtual_chain` - pub async fn async_get_blocks_acceptance_data(&self, hashes: Vec) -> ConsensusResult>> { - self.clone().spawn_blocking(move |c| c.get_blocks_acceptance_data(&hashes)).await + pub async fn async_get_blocks_acceptance_data( + &self, + hashes: Vec, + merged_blocks_limit: Option, + ) -> ConsensusResult>> { + self.clone().spawn_blocking(move |c| c.get_blocks_acceptance_data(&hashes, merged_blocks_limit)).await } pub async fn async_is_chain_block(&self, hash: Hash) -> ConsensusResult { diff --git a/consensus/client/src/error.rs b/consensus/client/src/error.rs index e0aab2156..e632f517d 100644 --- a/consensus/client/src/error.rs +++ b/consensus/client/src/error.rs @@ -1,3 +1,5 @@ +//! The [`Error`](enum@Error) enum used by this crate + use thiserror::Error; use wasm_bindgen::{JsError, JsValue}; use workflow_wasm::jserror::JsErrorData; diff --git a/consensus/client/src/hash.rs b/consensus/client/src/hash.rs index 4402cfb1b..1577689a6 100644 --- a/consensus/client/src/hash.rs +++ b/consensus/client/src/hash.rs @@ -1,3 +1,10 @@ +//! +//! WASM bindings for transaction hashers: [`TransactionSigningHash`](native::TransactionSigningHash) +//! and [`TransactionSigningHashECDSA`](native::TransactionSigningHashECDSA). +//! + +#![allow(non_snake_case)] + use crate::imports::*; use crate::result::Result; use kaspa_hashes as native; diff --git a/consensus/client/src/header.rs b/consensus/client/src/header.rs index 56fd92845..6f04a73c4 100644 --- a/consensus/client/src/header.rs +++ b/consensus/client/src/header.rs @@ -1,3 +1,9 @@ +//! +//! Implementation of the Block [`Header`] struct. +//! + +#![allow(non_snake_case)] + use crate::error::Error; use js_sys::{Array, Object}; use kaspa_consensus_core::hashing; @@ -59,10 +65,15 @@ export interface IRawHeader { #[wasm_bindgen] extern "C" { + /// WASM (TypeScript) type definition for the Header-like struct: `Header | IHeader | IRawHeader`. + /// + /// @category Consensus #[wasm_bindgen(typescript_type = "Header | IHeader | IRawHeader")] pub type HeaderT; } +/// Kaspa Block Header +/// /// @category Consensus #[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] #[serde(rename_all = "camelCase")] diff --git a/consensus/client/src/input.rs b/consensus/client/src/input.rs index 736696bfa..a5018199d 100644 --- a/consensus/client/src/input.rs +++ b/consensus/client/src/input.rs @@ -1,3 +1,9 @@ +//! +//! Implementation of the client-side [`TransactionInput`] struct used by the client-side [`Transaction`] struct. +//! + +#![allow(non_snake_case)] + use crate::imports::*; use crate::result::Result; use crate::TransactionOutpoint; @@ -33,14 +39,21 @@ export interface ITransactionInputVerboseData { } #[wasm_bindgen] extern "C" { + /// WASM (TypeScript) type representing `ITransactionInput | TransactionInput` + /// @category Consensus #[wasm_bindgen(typescript_type = "ITransactionInput | TransactionInput")] pub type TransactionInputT; + /// WASM (TypeScript) type representing `ITransactionInput[] | TransactionInput[]` + /// @category Consensus #[wasm_bindgen(typescript_type = "(ITransactionInput | TransactionInput)[]")] pub type TransactionInputArrayAsArgT; + /// WASM (TypeScript) type representing `TransactionInput[]` + /// @category Consensus #[wasm_bindgen(typescript_type = "TransactionInput[]")] pub type TransactionInputArrayAsResultT; } +/// Inner type used by [`TransactionInput`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionInputInner { diff --git a/consensus/client/src/lib.rs b/consensus/client/src/lib.rs index eb482eab1..3afae2f78 100644 --- a/consensus/client/src/lib.rs +++ b/consensus/client/src/lib.rs @@ -1,3 +1,17 @@ +//! +//! # Client-side consensus primitives. +//! +//! This crate offers client-side primitives mirroring the consensus layer of the Kaspa p2p node. +//! It declares structs such as [`Transaction`], [`TransactionInput`], [`TransactionOutput`], +//! [`TransactionOutpoint`], [`UtxoEntry`], and [`UtxoEntryReference`] +//! that are used by the Wallet subsystem as well as WASM bindings. +//! +//! Unlike raw consensus primitives (used for high-performance DAG processing) the primitives +//! offered in this crate are designed to be used in client-side applications. Their internal +//! data is typically wrapped into `Arc>`, allowing for easy sharing between +//! async / threaded environments and WASM bindings. +//! + pub mod error; mod imports; mod input; diff --git a/consensus/client/src/outpoint.rs b/consensus/client/src/outpoint.rs index 06be53f6a..a9b39f5e4 100644 --- a/consensus/client/src/outpoint.rs +++ b/consensus/client/src/outpoint.rs @@ -1,3 +1,11 @@ +//! +//! Implementation of the client-side [`TransactionOutpoint`] used by the [`TransactionInput`] struct. +//! + +#![allow(non_snake_case)] + +use cfg_if::cfg_if; + use crate::imports::*; use crate::result::Result; @@ -14,6 +22,7 @@ export interface ITransactionOutpoint { } "#; +/// Inner type used by [`TransactionOutpoint`] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd)] #[serde(rename_all = "camelCase")] pub struct TransactionOutpointInner { @@ -110,26 +119,31 @@ impl TransactionOutpoint { } } -#[cfg_attr(feature = "wasm32-sdk", wasm_bindgen)] -impl 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 }) } - } +cfg_if! { + if #[cfg(feature = "wasm32-sdk")] { - #[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] + impl TransactionOutpoint { + #[wasm_bindgen(constructor)] + pub fn ctor(transaction_id: TransactionId, index: u32) -> TransactionOutpoint { + Self { inner: Arc::new(TransactionOutpointInner { transaction_id, index }) } + } - #[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(js_name = "getId")] + pub fn id_string(&self) -> String { + format!("{}-{}", self.get_transaction_id_as_string(), self.get_index()) + } - #[cfg_attr(feature = "wasm32-sdk", wasm_bindgen(getter, js_name = index))] - pub fn get_index(&self) -> TransactionIndexType { - self.inner().index + #[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)] + pub fn get_index(&self) -> TransactionIndexType { + self.inner().index + } + } } } diff --git a/consensus/client/src/output.rs b/consensus/client/src/output.rs index 8f335c47d..17b4a58c8 100644 --- a/consensus/client/src/output.rs +++ b/consensus/client/src/output.rs @@ -1,3 +1,9 @@ +//! +//! Implementation of the client-side [`TransactionOutput`] used by the [`Transaction`] struct. +//! + +#![allow(non_snake_case)] + use crate::imports::*; #[wasm_bindgen(typescript_custom_section)] @@ -28,14 +34,21 @@ export interface ITransactionOutputVerboseData { #[wasm_bindgen] extern "C" { + /// WASM (TypeScript) type representing `ITransactionOutput | TransactionOutput` + /// @category Consensus #[wasm_bindgen(typescript_type = "ITransactionOutput | TransactionOutput")] pub type TransactionOutputT; + /// WASM (TypeScript) type representing `ITransactionOutput[] | TransactionOutput[]` + /// @category Consensus #[wasm_bindgen(typescript_type = "(ITransactionOutput | TransactionOutput)[]")] pub type TransactionOutputArrayAsArgT; + /// WASM (TypeScript) type representing `TransactionOutput[]` + /// @category Consensus #[wasm_bindgen(typescript_type = "TransactionOutput[]")] pub type TransactionOutputArrayAsResultT; } +/// Inner type used by [`TransactionOutput`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionOutputInner { diff --git a/consensus/client/src/result.rs b/consensus/client/src/result.rs index 4c8cb83f5..d8bff8aa1 100644 --- a/consensus/client/src/result.rs +++ b/consensus/client/src/result.rs @@ -1 +1,3 @@ +//! [`Result`] type alias that is bound to the [`Error`](super::error::Error) type from this crate. + pub type Result = std::result::Result; diff --git a/consensus/client/src/serializable/mod.rs b/consensus/client/src/serializable/mod.rs index a590ab286..ab78d956b 100644 --- a/consensus/client/src/serializable/mod.rs +++ b/consensus/client/src/serializable/mod.rs @@ -1,3 +1,24 @@ +//! +//! # Standardized JSON serialization and deserialization of Kaspa transactions. +//! +//! This module provides standardized JSON serialization and deserialization of +//! Kaspa transactions. There are two sub-modules: `numeric` and `string`. +//! +//! The `numeric` module provides serialization and deserialization of transactions +//! with all large integer values as `bigint` types in WASM or numerical values that +//! exceed the largest integer that can be represented by the JavaScript `number` type. +//! +//! The `string` module provides serialization and deserialization of transactions +//! with all large integer values as `string` types. This allows deserialization +//! via JSON in JavaScript environments and later conversion to `bigint` types. +//! +//! These data structures can be used for manual transport of transactions using JSON. +//! For more advanced use cases, please refer to `PSKT` in the [`kaspa_wallet_pskt`](https://docs.rs/kaspa_wallet_pskt) +//! crate. +//! + +#![allow(non_snake_case)] + pub mod numeric; pub mod string; @@ -80,6 +101,7 @@ export interface ISerializableTransaction { #[wasm_bindgen] extern "C" { + /// WASM (TypeScript) representation of the `ISerializableTransaction` interface. #[wasm_bindgen(extends = js_sys::Array, typescript_type = "ISerializableTransaction")] pub type SerializableTransactionT; } diff --git a/consensus/client/src/serializable/numeric.rs b/consensus/client/src/serializable/numeric.rs index 733afd54e..6c24db634 100644 --- a/consensus/client/src/serializable/numeric.rs +++ b/consensus/client/src/serializable/numeric.rs @@ -1,4 +1,10 @@ -//! This module implements the primitives for external transaction signing. +//! +//! This module implements transaction-related primitives for JSON serialization +//! where all large integer values (`u64`) are serialized to JSON using `serde` and +//! can exceed the largest integer value representable by the JavaScript `number` type. +//! (i.e. transactions serialized using this module can not be deserialized in JavaScript +//! but may be deserialized in other JSON-capable environments that support large integers) +//! use crate::error::Error; use crate::imports::*; diff --git a/consensus/client/src/serializable/string.rs b/consensus/client/src/serializable/string.rs index e35cdb028..35c7907b2 100644 --- a/consensus/client/src/serializable/string.rs +++ b/consensus/client/src/serializable/string.rs @@ -1,4 +1,7 @@ -//! This module implements the primitives for external transaction signing. +//! +//! This module implements transaction-related primitives for JSON serialization +//! where all large integer values (`u64`) are serialized to and from JSON as strings. +//! use crate::imports::*; use crate::result::Result; diff --git a/consensus/client/src/sign.rs b/consensus/client/src/sign.rs index c254aee07..4044dc570 100644 --- a/consensus/client/src/sign.rs +++ b/consensus/client/src/sign.rs @@ -1,3 +1,7 @@ +//! +//! Utilities for signing transactions. +//! + use crate::transaction::Transaction; use core::iter::once; use itertools::Itertools; diff --git a/consensus/client/src/transaction.rs b/consensus/client/src/transaction.rs index fb6d185f0..17cc38126 100644 --- a/consensus/client/src/transaction.rs +++ b/consensus/client/src/transaction.rs @@ -1,3 +1,7 @@ +//! +//! Declares the client-side [`Transaction`] type, which represents a Kaspa transaction. +//! + #![allow(non_snake_case)] use crate::imports::*; @@ -53,10 +57,13 @@ export interface ITransactionVerboseData { #[wasm_bindgen] extern "C" { + /// WASM (TypeScript) type representing `ITransaction | Transaction` + /// @category Consensus #[wasm_bindgen(typescript_type = "ITransaction | Transaction")] pub type TransactionT; } +/// Inner type used by [`Transaction`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionInner { diff --git a/consensus/client/src/utils.rs b/consensus/client/src/utils.rs index 4f543d45b..7e08556fe 100644 --- a/consensus/client/src/utils.rs +++ b/consensus/client/src/utils.rs @@ -1,3 +1,9 @@ +//! +//! Client-side utility functions and their WASM bindings. +//! + +#![allow(non_snake_case)] + use crate::imports::*; use crate::result::Result; use kaspa_addresses::*; diff --git a/consensus/client/src/utxo.rs b/consensus/client/src/utxo.rs index 0a8b3fdb9..bbfc1199d 100644 --- a/consensus/client/src/utxo.rs +++ b/consensus/client/src/utxo.rs @@ -1,3 +1,13 @@ +//! +//! # UTXO client-side data structures. +//! +//! This module provides client-side data structures for UTXO management. +//! In particular, the [`UtxoEntry`] and [`UtxoEntryReference`] structs +//! are used to represent UTXO entries in the wallet subsystem and WASM bindings. +//! + +#![allow(non_snake_case)] + use crate::imports::*; use crate::outpoint::{TransactionOutpoint, TransactionOutpointInner}; use crate::result::Result; @@ -29,16 +39,22 @@ export interface IUtxoEntry { #[wasm_bindgen] extern "C" { + /// WASM type representing an array of [`UtxoEntryReference`] objects (i.e. `UtxoEntryReference[]`) #[wasm_bindgen(extends = Array, typescript_type = "UtxoEntryReference[]")] pub type UtxoEntryReferenceArrayT; + /// WASM type representing a UTXO entry interface (a UTXO-like object) #[wasm_bindgen(typescript_type = "IUtxoEntry")] pub type IUtxoEntry; + /// WASM type representing an array of UTXO entries (i.e. `IUtxoEntry[]`) #[wasm_bindgen(typescript_type = "IUtxoEntry[]")] pub type IUtxoEntryArray; } +/// A UTXO entry Id is a unique identifier for a UTXO entry defined by the `txid+output_index`. pub type UtxoEntryId = TransactionOutpointInner; +/// [`UtxoEntry`] struct represents a client-side UTXO entry. +/// /// @category Wallet SDK #[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] #[serde(rename_all = "camelCase")] @@ -119,6 +135,8 @@ impl From<&UtxoEntry> for cctx::UtxoEntry { } } +/// [`Arc`] reference to a [`UtxoEntry`] used by the wallet subsystems. +/// /// @category Wallet SDK #[derive(Clone, Debug, Serialize, Deserialize, CastFromJs)] #[wasm_bindgen(inspectable)] @@ -251,6 +269,7 @@ impl PartialOrd for UtxoEntryReference { } } +/// An extension trait to convert a JS value into a vec of UTXO entry references. pub trait TryIntoUtxoEntryReferences { fn try_into_utxo_entry_references(&self) -> Result>; } diff --git a/consensus/core/src/api/mod.rs b/consensus/core/src/api/mod.rs index 4833c7659..365b8404c 100644 --- a/consensus/core/src/api/mod.rs +++ b/consensus/core/src/api/mod.rs @@ -39,7 +39,7 @@ pub struct BlockValidationFutures { /// A future triggered when DAG state which included this block has been processed by the virtual processor /// (exceptions are header-only blocks and trusted blocks which have the future completed before virtual - /// processing along with the [`block_task`]) + /// processing along with the `block_task`) pub virtual_state_task: BlockValidationFuture, } @@ -157,7 +157,12 @@ pub trait ConsensusApi: Send + Sync { unimplemented!() } - fn get_virtual_chain_from_block(&self, hash: Hash) -> ConsensusResult { + /// Gets the virtual chain paths from `low` to the `sink` hash, or until `chain_path_added_limit` is reached + /// + /// Note: + /// 1) `chain_path_added_limit` will populate removed fully, and then the added chain path, up to `chain_path_added_limit` amount of hashes. + /// 1.1) use `None to impose no limit with optimized backward chain iteration, for better performance in cases where batching is not required. + fn get_virtual_chain_from_block(&self, low: Hash, chain_path_added_limit: Option) -> ConsensusResult { unimplemented!() } @@ -297,7 +302,11 @@ pub trait ConsensusApi: Send + Sync { /// Returns acceptance data for a set of blocks belonging to the selected parent chain. /// /// See `self::get_virtual_chain` - fn get_blocks_acceptance_data(&self, hashes: &[Hash]) -> ConsensusResult>> { + fn get_blocks_acceptance_data( + &self, + hashes: &[Hash], + merged_blocks_limit: Option, + ) -> ConsensusResult>> { unimplemented!() } diff --git a/consensus/core/src/config/bps.rs b/consensus/core/src/config/bps.rs index c0c52a6df..5e98aac5d 100644 --- a/consensus/core/src/config/bps.rs +++ b/consensus/core/src/config/bps.rs @@ -33,7 +33,7 @@ impl Bps { } /// Returns the GHOSTDAG K value which was pre-computed for this BPS - /// (see [`calculate_ghostdag_k`] and [`gen_ghostdag_table`] for the full calculation) + /// (see [`calculate_ghostdag_k`] and `gen_ghostdag_table` for the full calculation) #[rustfmt::skip] pub const fn ghostdag_k() -> KType { match BPS { diff --git a/consensus/core/src/lib.rs b/consensus/core/src/lib.rs index 46ad3f2ce..188b2403b 100644 --- a/consensus/core/src/lib.rs +++ b/consensus/core/src/lib.rs @@ -1,3 +1,9 @@ +//! +//! # Consensus Core +//! +//! This crate implements primitives used in the Kaspa node consensus processing. +//! + extern crate alloc; extern crate core; extern crate self as consensus_core; diff --git a/consensus/core/src/network.rs b/consensus/core/src/network.rs index d5e9abd24..18e52eacb 100644 --- a/consensus/core/src/network.rs +++ b/consensus/core/src/network.rs @@ -1,3 +1,16 @@ +//! +//! # Network Types +//! +//! This module implements [`NetworkType`] (such as `mainnet`, `testnet`, `devnet`, and `simnet`) +//! and [`NetworkId`] that combines a network type with an optional numerical suffix. +//! +//! The suffix is used to differentiate between multiple networks of the same type and is used +//! explicitly with `testnet` networks, allowing declaration of testnet versions such as +//! `testnet-10`, `testnet-11`, etc. +//! + +#![allow(non_snake_case)] + use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_addresses::Prefix; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; diff --git a/consensus/core/src/tx.rs b/consensus/core/src/tx.rs index bad1b679a..a4dd7dd45 100644 --- a/consensus/core/src/tx.rs +++ b/consensus/core/src/tx.rs @@ -1,3 +1,11 @@ +//! +//! # Transaction +//! +//! This module implements consensus [`Transaction`] structure and related types. +//! + +#![allow(non_snake_case)] + mod script_public_key; use borsh::{BorshDeserialize, BorshSerialize}; @@ -25,6 +33,7 @@ use crate::{ /// COINBASE_TRANSACTION_INDEX is the index of the coinbase transaction in every block pub const COINBASE_TRANSACTION_INDEX: usize = 0; +/// A 32-byte Kaspa transaction identifier. pub type TransactionId = kaspa_hashes::Hash; /// Holds details about an individual transaction output in a utxo diff --git a/consensus/src/consensus/mod.rs b/consensus/src/consensus/mod.rs index 8474a6864..1731729a3 100644 --- a/consensus/src/consensus/mod.rs +++ b/consensus/src/consensus/mod.rs @@ -607,14 +607,26 @@ impl ConsensusApi for Consensus { self.config.is_nearly_synced(compact.timestamp, compact.daa_score) } - fn get_virtual_chain_from_block(&self, hash: Hash) -> ConsensusResult { - // Calculate chain changes between the given hash and the - // sink. Note that we explicitly don't + fn get_virtual_chain_from_block(&self, low: Hash, chain_path_added_limit: Option) -> ConsensusResult { + // Calculate chain changes between the given `low` and the current sink hash (up to `limit` amount of block hashes). + // Note: + // 1) that we explicitly don't // do the calculation against the virtual itself so that we // won't later need to remove it from the result. + // 2) supplying `None` as `chain_path_added_limit` will result in the full chain path, with optimized performance. let _guard = self.pruning_lock.blocking_read(); - self.validate_block_exists(hash)?; - Ok(self.services.dag_traversal_manager.calculate_chain_path(hash, self.get_sink())) + + // Verify that the block exists + self.validate_block_exists(low)?; + + // Verify that source is on chain(block) + self.services + .reachability_service + .is_chain_ancestor_of(self.get_source(), low) + .then_some(()) + .ok_or(ConsensusError::General("the queried hash does not have source on its chain"))?; + + Ok(self.services.dag_traversal_manager.calculate_chain_path(low, self.get_sink(), chain_path_added_limit)) } /// Returns a Vec of header samples since genesis @@ -914,11 +926,35 @@ impl ConsensusApi for Consensus { self.acceptance_data_store.get(hash).unwrap_option().ok_or(ConsensusError::MissingData(hash)) } - fn get_blocks_acceptance_data(&self, hashes: &[Hash]) -> ConsensusResult>> { + fn get_blocks_acceptance_data( + &self, + hashes: &[Hash], + merged_blocks_limit: Option, + ) -> ConsensusResult>> { + // Note: merged_blocks_limit will limit after the sum of merged blocks is breached along the supplied hash's acceptance data + // and not limit the acceptance data within a queried hash. i.e. It has mergeset_size_limit granularity, this is to guarantee full acceptance data coverage. + if merged_blocks_limit.is_none() { + return hashes + .iter() + .copied() + .map(|hash| self.acceptance_data_store.get(hash).unwrap_option().ok_or(ConsensusError::MissingData(hash))) + .collect::>>(); + } + let merged_blocks_limit = merged_blocks_limit.unwrap(); // we handle `is_none`, so may unwrap. + let mut num_of_merged_blocks = 0usize; + hashes .iter() .copied() - .map(|hash| self.acceptance_data_store.get(hash).unwrap_option().ok_or(ConsensusError::MissingData(hash))) + .map_while(|hash| { + let entry = self.acceptance_data_store.get(hash).unwrap_option().ok_or(ConsensusError::MissingData(hash)); + num_of_merged_blocks += entry.as_ref().map_or(0, |entry| entry.len()); + if num_of_merged_blocks > merged_blocks_limit { + None + } else { + Some(entry) + } + }) .collect::>>() } diff --git a/consensus/src/pipeline/virtual_processor/processor.rs b/consensus/src/pipeline/virtual_processor/processor.rs index fb5550319..f7454e2cb 100644 --- a/consensus/src/pipeline/virtual_processor/processor.rs +++ b/consensus/src/pipeline/virtual_processor/processor.rs @@ -298,7 +298,7 @@ impl VirtualStateProcessor { assert_eq!(virtual_ghostdag_data.selected_parent, new_sink); let sink_multiset = self.utxo_multisets_store.get(new_sink).unwrap(); - let chain_path = self.dag_traversal_manager.calculate_chain_path(prev_sink, new_sink); + let chain_path = self.dag_traversal_manager.calculate_chain_path(prev_sink, new_sink, None); let new_virtual_state = self .calculate_and_commit_virtual_state( virtual_read, diff --git a/consensus/src/processes/ghostdag/protocol.rs b/consensus/src/processes/ghostdag/protocol.rs index 87beeb565..8dfe4e793 100644 --- a/consensus/src/processes/ghostdag/protocol.rs +++ b/consensus/src/processes/ghostdag/protocol.rs @@ -91,7 +91,7 @@ impl pub fn ghostdag(&self, parents: &[Hash]) -> GhostdagData { assert!(!parents.is_empty(), "genesis must be added via a call to init"); diff --git a/consensus/src/processes/reachability/interval.rs b/consensus/src/processes/reachability/interval.rs index 9f8d7fbd0..b910f3ddf 100644 --- a/consensus/src/processes/reachability/interval.rs +++ b/consensus/src/processes/reachability/interval.rs @@ -89,7 +89,7 @@ impl Interval { } /// Splits this interval to exactly |sizes| parts where - /// |part_i| = sizes[i]. This method expects sum(sizes) to be exactly + /// |part_i| = sizes\[i\]. This method expects sum(sizes) to be exactly /// equal to the interval's size. pub fn split_exact(&self, sizes: &[u64]) -> Vec { assert_eq!(sizes.iter().sum::(), self.size(), "sum of sizes must be equal to the interval's size"); @@ -107,7 +107,7 @@ impl Interval { /// Splits this interval to |sizes| parts /// by the allocation rule described below. This method expects sum(sizes) /// to be smaller or equal to the interval's size. Every part_i is - /// allocated at least sizes[i] capacity. The remaining budget is + /// allocated at least sizes\[i\] capacity. The remaining budget is /// split by an exponentially biased rule described below. /// /// This rule follows the GHOSTDAG protocol behavior where the child diff --git a/consensus/src/processes/sync/mod.rs b/consensus/src/processes/sync/mod.rs index 847222968..3978913ba 100644 --- a/consensus/src/processes/sync/mod.rs +++ b/consensus/src/processes/sync/mod.rs @@ -111,7 +111,7 @@ impl< (blocks, highest_reached) } - fn find_highest_common_chain_block(&self, low: Hash, high: Hash) -> Hash { + pub fn find_highest_common_chain_block(&self, low: Hash, high: Hash) -> Hash { self.reachability_service .default_backward_chain_iterator(low) .find(|candidate| self.reachability_service.is_chain_ancestor_of(*candidate, high)) diff --git a/consensus/src/processes/traversal_manager.rs b/consensus/src/processes/traversal_manager.rs index 3ae0aef5d..23dc5c69f 100644 --- a/consensus/src/processes/traversal_manager.rs +++ b/consensus/src/processes/traversal_manager.rs @@ -31,7 +31,7 @@ impl ChainPath { + pub fn calculate_chain_path(&self, from: Hash, to: Hash, chain_path_added_limit: Option) -> ChainPath { let mut removed = Vec::new(); let mut common_ancestor = from; for current in self.reachability_service.default_backward_chain_iterator(from) { @@ -42,9 +42,20 @@ impl; -/// Kaspa `Address` struct that serializes to and from an address format string: `kaspa:qz0s...t8cv`. +/// Kaspa [`Address`] struct that serializes to and from an address format string: `kaspa:qz0s...t8cv`. +/// /// @category Address #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, CastFromJs)] #[wasm_bindgen(inspectable)] @@ -516,12 +526,24 @@ impl TryCastFromJs for Address { #[wasm_bindgen] extern "C" { + /// WASM (TypeScript) type representing an Address-like object: `Address | string`. + /// + /// @category Address #[wasm_bindgen(extends = js_sys::Array, typescript_type = "Address | string")] pub type AddressT; + /// WASM (TypeScript) type representing an array of Address-like objects: `(Address | string)[]`. + /// + /// @category Address #[wasm_bindgen(extends = js_sys::Array, typescript_type = "(Address | string)[]")] pub type AddressOrStringArrayT; + /// WASM (TypeScript) type representing an array of [`Address`] objects: `Address[]`. + /// + /// @category Address #[wasm_bindgen(extends = js_sys::Array, typescript_type = "Address[]")] pub type AddressArrayT; + /// WASM (TypeScript) type representing an [`Address`] or an undefined value: `Address | undefined`. + /// + /// @category Address #[wasm_bindgen(typescript_type = "Address | undefined")] pub type AddressOrUndefinedT; } diff --git a/crypto/txscript/src/opcodes/mod.rs b/crypto/txscript/src/opcodes/mod.rs index 4406bd5b6..ad800d248 100644 --- a/crypto/txscript/src/opcodes/mod.rs +++ b/crypto/txscript/src/opcodes/mod.rs @@ -2747,7 +2747,7 @@ mod test { (1u64, vec![], false), // Case 1: 0 = locktime < txLockTime (0x800000, vec![0x7f, 0, 0], false), // Case 2: 0 < locktime < txLockTime (0x800000, vec![0x7f, 0, 0, 0, 0, 0, 0, 0, 0], true), // Case 3: locktime too big - (LOCK_TIME_THRESHOLD * 2, vec![0x7f, 0, 0, 0], true), // Case 4: lock times are inconsistant + (LOCK_TIME_THRESHOLD * 2, vec![0x7f, 0, 0, 0], true), // Case 4: lock times are inconsistent ] { let mut tx = base_tx.clone(); tx.0.lock_time = tx_lock_time; diff --git a/indexes/utxoindex/src/core/errors.rs b/indexes/utxoindex/src/core/errors.rs index 61aa877ab..0e0998905 100644 --- a/indexes/utxoindex/src/core/errors.rs +++ b/indexes/utxoindex/src/core/errors.rs @@ -4,7 +4,7 @@ use thiserror::Error; use crate::IDENT; use kaspa_database::prelude::StoreError; -/// Errors originating from the [`UtxoIndex`]. +/// Errors originating from the [`UtxoIndex`](crate::UtxoIndex). #[derive(Error, Debug)] pub enum UtxoIndexError { #[error("[{IDENT}]: {0}")] @@ -14,5 +14,5 @@ pub enum UtxoIndexError { DBResetError(#[from] io::Error), } -/// Results originating from the [`UtxoIndex`]. +/// Results originating from the [`UtxoIndex`](crate::UtxoIndex). pub type UtxoIndexResult = Result; diff --git a/indexes/utxoindex/src/index.rs b/indexes/utxoindex/src/index.rs index b71935afa..3b1bf2fe9 100644 --- a/indexes/utxoindex/src/index.rs +++ b/indexes/utxoindex/src/index.rs @@ -21,7 +21,8 @@ use std::{ const RESYNC_CHUNK_SIZE: usize = 2048; //Increased from 1k (used in go-kaspad), for quicker resets, while still having a low memory footprint. -/// UtxoIndex indexes [`CompactUtxoEntryCollections`] by [`ScriptPublicKey`], commits them to its owns store, and emits changes. +/// UtxoIndex indexes `CompactUtxoEntryCollections` by [`ScriptPublicKey`](kaspa_consensus_core::tx::ScriptPublicKey), +/// commits them to its owns store, and emits changes. /// Note: The UtxoIndex struct by itself is not thread save, only correct usage of the supplied RwLock via `new` makes it so. /// please follow guidelines found in the comments under `utxoindex::core::api::UtxoIndexApi` for proper thread safety. pub struct UtxoIndex { @@ -131,7 +132,7 @@ impl UtxoIndexApi for UtxoIndex { /// Deletes and reinstates the utxoindex database, syncing it from scratch via the consensus database. /// /// **Notes:** - /// 1) There is an implicit expectation that the consensus store must have [VirtualParent] tips. i.e. consensus database must be initiated. + /// 1) There is an implicit expectation that the consensus store must have VirtualParent tips. i.e. consensus database must be initiated. /// 2) resyncing while consensus notifies of utxo differences, may result in a corrupted db. fn resync(&mut self) -> UtxoIndexResult<()> { info!("Resyncing the utxoindex..."); diff --git a/indexes/utxoindex/src/update_container.rs b/indexes/utxoindex/src/update_container.rs index 8555a02d4..96449dbff 100644 --- a/indexes/utxoindex/src/update_container.rs +++ b/indexes/utxoindex/src/update_container.rs @@ -25,7 +25,7 @@ impl UtxoIndexChanges { } } - /// Add a [`UtxoDiff`] the the [`UtxoIndexChanges`] struct. + /// Add a [`UtxoDiff`] the [`UtxoIndexChanges`] struct. pub fn update_utxo_diff(&mut self, utxo_diff: UtxoDiff) { let (to_add, mut to_remove) = (utxo_diff.add, utxo_diff.remove); @@ -53,7 +53,7 @@ impl UtxoIndexChanges { } } - /// Add a [`Vec<(TransactionOutpoint, UtxoEntry)>`] the the [`UtxoIndexChanges`] struct + /// Add a [`Vec<(TransactionOutpoint, UtxoEntry)>`] the [`UtxoIndexChanges`] struct /// /// Note: This is meant to be used when resyncing. pub fn add_utxos_from_vector(&mut self, utxo_vector: Vec<(TransactionOutpoint, UtxoEntry)>) { diff --git a/metrics/core/src/data.rs b/metrics/core/src/data.rs index d47941aa7..ce9dc7216 100644 --- a/metrics/core/src/data.rs +++ b/metrics/core/src/data.rs @@ -252,65 +252,6 @@ pub enum Metric { } impl Metric { - // TODO - this will be refactored at a later date - // as this requires changes and testing in /kos - // pub fn group(&self) -> &'static str { - // match self { - // Metric::NodeCpuUsage - // | Metric::NodeResidentSetSizeBytes - // | Metric::NodeVirtualMemorySizeBytes - // | Metric::NodeFileHandlesCount - // | Metric::NodeDiskIoReadBytes - // | Metric::NodeDiskIoWriteBytes - // | Metric::NodeDiskIoReadPerSec - // | Metric::NodeDiskIoWritePerSec - // | Metric::NodeBorshLiveConnections - // | Metric::NodeBorshConnectionAttempts - // | Metric::NodeBorshHandshakeFailures - // | Metric::NodeJsonLiveConnections - // | Metric::NodeJsonConnectionAttempts - // | Metric::NodeJsonHandshakeFailures - // | Metric::NodeBorshBytesTx - // | Metric::NodeBorshBytesRx - // | Metric::NodeJsonBytesTx - // | Metric::NodeJsonBytesRx - // | Metric::NodeP2pBytesTx - // | Metric::NodeP2pBytesRx - // | Metric::NodeGrpcUserBytesTx - // | Metric::NodeGrpcUserBytesRx - // | Metric::NodeTotalBytesTx - // | Metric::NodeTotalBytesRx - // | Metric::NodeBorshBytesTxPerSecond - // | Metric::NodeBorshBytesRxPerSecond - // | Metric::NodeJsonBytesTxPerSecond - // | Metric::NodeJsonBytesRxPerSecond - // | Metric::NodeP2pBytesTxPerSecond - // | Metric::NodeP2pBytesRxPerSecond - // | Metric::NodeGrpcUserBytesTxPerSecond - // | Metric::NodeGrpcUserBytesRxPerSecond - // | Metric::NodeTotalBytesTxPerSecond - // | Metric::NodeTotalBytesRxPerSecond - // | Metric::NodeActivePeers => "system", - // // -- - // Metric::NodeBlocksSubmittedCount - // | Metric::NodeHeadersProcessedCount - // | Metric::NodeDependenciesProcessedCount - // | Metric::NodeBodiesProcessedCount - // | Metric::NodeTransactionsProcessedCount - // | Metric::NodeChainBlocksProcessedCount - // | Metric::NodeMassProcessedCount - // | Metric::NodeDatabaseBlocksCount - // | Metric::NodeDatabaseHeadersCount - // | Metric::NetworkMempoolSize - // | Metric::NetworkTransactionsPerSecond - // | Metric::NetworkTipHashesCount - // | Metric::NetworkDifficulty - // | Metric::NetworkPastMedianTime - // | Metric::NetworkVirtualParentHashesCount - // | Metric::NetworkVirtualDaaScore => "kaspa", - // } - // } - pub fn is_key_performance_metric(&self) -> bool { matches!( self, diff --git a/mining/src/feerate/fee_estimation.ipynb b/mining/src/feerate/fee_estimation.ipynb index 694f47450..a8b8fbfc8 100644 --- a/mining/src/feerate/fee_estimation.ipynb +++ b/mining/src/feerate/fee_estimation.ipynb @@ -252,7 +252,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -303,7 +303,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAD8CAYAAAB5Pm/hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvhp/UCwAAHW9JREFUeJzt3Xl0XGeZ5/HvU6WSVLKszVpiSbblJMZZyGLjOAHTaUgAk8CQhKUn0ECGgTZNBw6cYTJDaOYA5wwzORMCPX2gM52QNMlAJxMghEAHTAiBEAix5Wze4tiJN8mbbEebtZbqmT/qypZt2ZalKt/Srd/nHJ1776t7q57K8ruv3nrvvebuiIhIdMXCLkBERHJLQS8iEnEKehGRiFPQi4hEnIJeRCTiFPQiIhGnoBcRiTgFvYhIxCnoRUQirijsAgBqa2u9paUl7DJERKaVNWvW7Hf3ulPtlxdB39LSQmtra9hliIhMK2a2fSL7aehGRCTiFPQiIhGnoBcRiTgFvYhIxCnoRUQiTkEvIhJxCnoRkYib1kG/aU8Pt698mc6+obBLERHJW9M66LcfOMR3n3yVnQf7wy5FRCRvTeugb6goBWBv90DIlYiI5K9TBr2ZzTGzJ81so5mtN7PPB+1fM7N2M3sh+Ll2zDG3mtkWM9tkZstzVfzhoO9R0IuInMhE7nWTAr7o7s+Z2UxgjZk9Hvzu2+7+zbE7m9kFwI3AhUAj8Bsze4O7j2SzcIDa8mLMYG/3YLZfWkQkMk7Zo3f33e7+XLDeA2wEmk5yyHXAg+4+6O5bgS3A0mwUe6yieIza8hL2aehGROSETmuM3sxagEXAs0HTZ83sJTO718yqg7YmYOeYw9oY58RgZivMrNXMWjs6Ok678FENFSUaoxcROYkJB72ZlQM/Ab7g7t3AncA5wKXAbuCO0V3HOdyPa3C/y92XuPuSurpT3k75hBpmlmroRkTkJCYU9GaWIBPyP3T3hwHcfa+7j7h7GribI8MzbcCcMYc3A7uyV/LR6itK2acvY0VETmgis24MuAfY6O7fGtM+e8xuNwDrgvVHgRvNrMTM5gMLgFXZK/loDRUl7O8dYiiVztVbiIhMaxOZdbMM+Biw1sxeCNq+DHzYzC4lMyyzDfg0gLuvN7OHgA1kZuzcnIsZN6NGp1h29A7SVJXM1duIiExbpwx6d3+a8cfdHzvJMd8AvjGFuibsrDEXTSnoRUSON62vjAWorygB0BRLEZETmPZBf+Q2CJp5IyIynmkf9DVlxRTFTHPpRUROYNoHfSxm1M8sUY9eROQEpn3Qg+bSi4icTCSCXrdBEBE5sYgEfSl7uhT0IiLjiUzQdw+k6BtKhV2KiEjeiUTQj14otatTvXoRkWNFIugbDwe9nh0rInKsiAR95qIpBb2IyPEiEfQNFaWYwS59ISsicpxIBH0iHqNhZql69CIi44hE0ENm+EZBLyJyvMgE/eyqJLs1dCMicpzIBH1TVZL2zn7cj3s8rYhIQYtM0DdWljKUSnPg0FDYpYiI5JXoBH0wl363LpoSETlK5IK+XV/IiogcJXJBr5k3IiJHi0zQV5clKE3E2N2loBcRGSsyQW9mNFYmdWMzEZFjRCboITN806ahGxGRo0Qq6JuqkrS/rqAXERkrUkE/d1YZ+3sH6R8aCbsUEZG8Eamgn1NTBsDO1/tCrkREJH9EK+irM1MsdxxQ0IuIjIpU0M9Vj15E5DiRCvqaGcWUFcfZcVBBLyIy6pRBb2ZzzOxJM9toZuvN7PNBe42ZPW5mm4NlddBuZvaPZrbFzF4ys8W5/hBjamVuTRk7D2rmjYjIqIn06FPAF939fOAK4GYzuwD4EvCEuy8Angi2Aa4BFgQ/K4A7s171STRXl7FTPXoRkcNOGfTuvtvdnwvWe4CNQBNwHXBfsNt9wPXB+nXA/Z7xZ6DKzGZnvfITmFtTxo6DfbovvYhI4LTG6M2sBVgEPAs0uPtuyJwMgPpgtyZg55jD2oK2Y19rhZm1mllrR0fH6Vd+AnNrkvQPj+i+9CIigQkHvZmVAz8BvuDu3SfbdZy247rX7n6Xuy9x9yV1dXUTLeOURufS6wtZEZGMCQW9mSXIhPwP3f3hoHnv6JBMsNwXtLcBc8Yc3gzsyk65p3Z4iqWCXkQEmNisGwPuATa6+7fG/OpR4KZg/SbgZ2PaPx7MvrkC6Bod4jkTmqsV9CIiYxVNYJ9lwMeAtWb2QtD2ZeA24CEz+ySwA/hQ8LvHgGuBLUAf8ImsVnwKyeI4dTNLNMVSRCRwyqB396cZf9wd4Opx9nfg5inWNSVza8rYduBQmCWIiOSNSF0ZO2p+7QwFvYhIILJBv7d7kEODqbBLEREJXSSD/uzaGQBs3a9evYhIJIN+fp2CXkRkVCSDvmWWgl5EZFQkg740EaepKqmgFxEhokEPmS9kX1PQi4hEO+i3dvTqLpYiUvAiHfTdAykO6i6WIlLgohv0mnkjIgJEOOhH59JrnF5ECl1kg76pKkkiburRi0jBi2zQF8VjzJs1gy37esMuRUQkVJENeoAF9eUKehEpeNEO+oaZbD9wiIHhkbBLEREJTaSDvqtviLTD+f/tVyy77bc88nx72CWJiJxxkQ36R55v58HVO4HMk8nbO/u59eG1CnsRKTiRDfrbV25iMJU+qq1/eITbV24KqSIRkXBENuh3dY7/zNgTtYuIRFVkg76xKnla7SIiURXZoL9l+UKSifhRbclEnFuWLwypIhGRcBSFXUCuXL+oCYCv/3w9r/cNUzezhL+/9vzD7SIihSKyPXrIhP1Dn34zAF++9jyFvIgUpEgHPUBL7QwSceOVvbpCVkQKU+SDPhGPcU5dORt3d4ddiohIKCIf9AAXNFawYZeCXkQKU0EE/YWNlezrGaSjZzDsUkREzriCCPoLZlcAsEHDNyJSgAor6DV8IyIF6JRBb2b3mtk+M1s3pu1rZtZuZi8EP9eO+d2tZrbFzDaZ2fJcFX46KssSNFcn1aMXkYI0kR7994F3j9P+bXe/NPh5DMDMLgBuBC4MjvknM4uPc+wZd8HsCtbv6gq7DBGRM+6UQe/uTwEHJ/h61wEPuvugu28FtgBLp1Bf1lzQWMHW/YfoG0qFXYqIyBk1lTH6z5rZS8HQTnXQ1gTsHLNPW9B2HDNbYWatZtba0dExhTIm5sLGStzh5T09OX8vEZF8MtmgvxM4B7gU2A3cEbTbOPv6eC/g7ne5+xJ3X1JXVzfJMibugsbMF7Lr9YWsiBSYSQW9u+919xF3TwN3c2R4pg2YM2bXZmDX1ErMjsbKUqrLEqxr0zi9iBSWSQW9mc0es3kDMDoj51HgRjMrMbP5wAJg1dRKzA4z45I5VbzY1hl2KSIiZ9Qpb1NsZg8AbwNqzawN+CrwNjO7lMywzDbg0wDuvt7MHgI2ACngZncfyU3pp++S5iqeemUzhwZTzCiJ7B2aRUSOcsq0c/cPj9N8z0n2/wbwjakUlSuXzqki7bC2vYsrzp4VdjkiImdEQVwZO+ri5koAXtyp4RsRKRwFFfSzykuYW1OmcXoRKSgFFfRA5gvZnZp5IyKFo/CCvrmS9s5+9vUMhF2KiMgZUXBBv2huFYB69SJSMAou6C9srKQoZjy/4/WwSxEROSMKLuhLE3EubKygdZuCXkQKQ8EFPcBlLTW80NbJYCpvruUSEcmZwgz6+TUMpdK8pPveiEgBKMygb6kBYNXWid5mX0Rk+irIoK+ZUcy59eWs3qagF5HoK8igh0yvfs221xlJj3u7fBGRyCjYoF86v5qewRQv79GDSEQk2go26EfH6VdrnF5EIq5gg765uoymqiTPvHYg7FJERHKqYIMe4K3n1vKnVw9onF5EIq2gg37Zglp6BlKsbdd8ehGJrsIO+nMyT5l6enNHyJWIiOROQQf9rPISLphdwdNb9oddiohIzhR00AP8xYJantveSd9QKuxSRERyouCDftm5tQyNpHU7BBGJrIIP+staaiiOx3h6s4ZvRCSaCj7ok8VxLj+7hic37Qu7FBGRnCj4oAe46rx6Xu04xLb9h8IuRUQk6xT0wNXnNQDwxMvq1YtI9CjogbmzylhQX84TG/eGXYqISNYp6ANXn9/Aqq0H6R4YDrsUEZGsUtAH3nF+Pam089QrukpWRKJFQR9YNLea6rIET2zUOL2IRMspg97M7jWzfWa2bkxbjZk9bmabg2V10G5m9o9mtsXMXjKzxbksPpviMeOq8xr4zca9DKZGwi5HRCRrJtKj/z7w7mPavgQ84e4LgCeCbYBrgAXBzwrgzuyUeWa89+LZ9Ayk+KPufSMiEXLKoHf3p4Bj7w9wHXBfsH4fcP2Y9vs9489AlZnNzlaxubbs3FoqSov4xUu7wy5FRCRrJjtG3+DuuwGCZX3Q3gTsHLNfW9B2HDNbYWatZtba0ZEfX4AWF8VYfuFZPL5ewzciEh3Z/jLWxmkb9/FN7n6Xuy9x9yV1dXVZLmPyrr14Nj2DKf7wioZvRCQaiiZ53F4zm+3uu4OhmdGpKm3AnDH7NQO7plLgmbbsnFqSiRife+B5BoZHaKxKcsvyhVy/aNw/TERE8t5ke/SPAjcF6zcBPxvT/vFg9s0VQNfoEM908dja3QyNOP3DIzjQ3tnPrQ+v5ZHn28MuTURkUiYyvfIB4BlgoZm1mdkngduAd5rZZuCdwTbAY8BrwBbgbuDvclJ1Dt2+ctNxDwvvHx7h9pWbQqpIRGRqTjl04+4fPsGvrh5nXwdunmpRYdrV2X9a7SIi+U5Xxh6jsSp5Wu0iIvlOQX+MW5YvJJmIH9WWTMS5ZfnCkCoSEZmayc66iazR2TW3r9xEe2c/8ZjxP254o2bdiMi0pR79OK5f1MQfv3QVd3zoEkbSTkNladgliYhMmoL+JN5z8WyqyhL832e2h12KiMikKehPojQR598vmcOvN+xld5dm3YjI9KSgP4WPXjGPtDsPPLsj7FJERCZFQX8Kc2rKuGphPf+6aidDqXTY5YiInDYF/QR87M3z2N87yL+tnVa37RERART0E3LlgjoW1Jfzz79/jczFvyIi04eCfgJiMeNv//IcXt7Tw+825ce980VEJkpBP0Hvu7SRxspS7vz9q2GXIiJyWhT0E5SIx/jUX5zNqq0HWbP99bDLERGZMAX9abhx6RyqyhJ857ebwy5FRGTCFPSnoay4iBVXns2TmzpYs/3Y56WLiOQnBf1p+g9vaaG2vIT/9atNmoEjItOCgv40lRUX8dm3n8OzWw/y9BY9QFxE8p+CfhI+fPlcmqqSfHPlJtJp9epFJL8p6CehpCjOF96xgBfbuvjZi3pouIjkNwX9JH1gcTMXN1dy2y9f5tBgKuxyREROSEE/SbGY8dV/dyF7uwf5p99tCbscEZETUtBPwZvmVXPDoibufmor2w8cCrscEZFxKein6EvXnEdxUYy//+k6TbcUkbykoJ+ihopS/us15/H0lv38aE1b2OWIiBxHQZ8Ff710Lktbavjvv9jAvu6BsMsRETmKgj4LYjHjtg9cxEAqzVce0RCOiOQXBX2WnF1Xzn9+1xv49Ya9PLh6Z9jliIgcpqDPok+99Wzeem4tX//5erbs6wm7HBERQEGfVbGY8a2/uoSy4iI+98ALDAyPhF2SiMjUgt7MtpnZWjN7wcxag7YaM3vczDYHy+rslDo91FeU8s0PXczG3d18/efrwy5HRCQrPfq3u/ul7r4k2P4S8IS7LwCeCLYLylXnNfB3bzuHB1bt5IfPbg+7HBEpcLkYurkOuC9Yvw+4Pgfvkfe++K6FvH1hHV97dD2rt+khJSISnqkGvQO/NrM1ZrYiaGtw990AwbJ+iu8xLcVjxj/cuIjm6jI+84M17DjQF3ZJIlKgphr0y9x9MXANcLOZXTnRA81shZm1mllrR0fHFMvIT5XJBHd/fAmptPPxe59lf+9g2CWJSAGaUtC7+65guQ/4KbAU2GtmswGC5b4THHuXuy9x9yV1dXVTKSOvnVtfzj03Xcae7gH+4/dX65bGInLGTTrozWyGmc0cXQfeBawDHgVuCna7CfjZVIuc7t40r5rvfmQx63d18zf3t9I/pGmXInLmTKVH3wA8bWYvAquAf3P3XwG3Ae80s83AO4Ptgnf1+Q3c/sGLeea1A3zyvtUKexE5Y4ome6C7vwZcMk77AeDqqRQVVe9f3AzAF3/0Ip+8bzX33HQZyeJ4yFWJSNTpytgz7P2Lm/nWX13Cn187wEfveZbXDw2FXZKIRJyCPgQ3LGrmOx9ZzNr2Lj7wf/7EzoOaeikiuTPpoRuZmmsvmk1teQmfum8177/zT3z8ink8uHonuzr7aaxKcsvyhVy/qCnsMkUkAtSjD9HS+TX85DNvYTiV5o7HX6G9sx8H2jv7ufXhtTzyfHvYJYpIBCjoQ7agYSal43wh2z88wu0rN4VQkYhEjYI+D+ztGv/xg7s6+89wJSISRQr6PNBYlRy3vTKZ0GMJRWTKFPR54JblC0kmjh6+iRl09g/zN/e3srtLPXsRmTwFfR64flET//P9F9FUlcSApqokd3zwEr7ynvN5est+3nHH7/mXP25lJK3evYicPsuHoYElS5Z4a2tr2GXkpZ0H+/jKI+v4/SsdXNRUydfedwFvmlcTdlkikgfMbM2Yhz6dkHr0eW5OTRnf/8RlfOcji9jbPcAH7nyGz/xgDdv2Hwq7NBGZJnTB1DRgZrz34kauOq+eu5/ayj8/9Sq/2biXv758Hn/7l+dwVmVp2CWKSB7T0M00tK97gG//5hUeam0jbsaHljTzmbedQ3N1WdilicgZNNGhGwX9NLbjQB93/v5VfrxmJ+5w3aVNfGJZC29sqgy7NBE5AxT0BWRXZz93PfUa/2/1TvqHR1gyr5qb3tLCu994Fom4voYRiSoFfQHq6h/mR607uf+Z7ew42Ef9zBJuWNzEBxc3s6BhZtjliUiWKegL2Eja+d2mfTywagdPbupgJO1cMqeKDy5u4j0XN1IzozjsEkUkCxT0AkBHzyA/e6GdH69p4+U9PcQMLp8/i2suOovlF55FQ4Vm7IhMVwp6OYq7s2F3N79cu4dfrtvNqx2ZefhvmlfNVefVc+WCOi5srCAWs5ArFZGJUtDLSW3e28Ov1u1h5YY9rGvvBmDWjGL+YkEtV76hjmXn1qq3L5LnFPQyYR09g/xhcwdPvdLBU5v3czB4ju3cmjKWzq9haUsNl82voWVWGWbq8YvkCwW9TEo67azf1c2zWw+wautBWre/fjj4a8tLuHROFRc3V3JRcyUXNVVSW14ScsUihUtBL1nh7rza0cuqra/Tuu0gL7Z18tr+Q4z+Z9NYWcpFzZVc2FjJGxpmsvCsmcytKSOusX6RnJto0OteN3JSZsa59TM5t34mH7l8LgA9A8Os39XNuvYuXmrr4qW2Tlau33v4mJKiGOfUlbPwrJksaChnQf1M5teW0VxdRmni+McmikhuKejltM0sTXDF2bO44uxZh9sODabYsq+XTXt72Ly3h017e3nm1QP8dMwDzs2gsTLJvFllzJs1g5ZgObemjKaqJBXJIn0HIJIDCnrJihklRVwyp4pL5lQd1d7VP8yrHb3sONDHtgOH2B4sV67fc3js//BrFMeZXZWksSpJY2UpjVVJZleW0lSV5KzKUupmllBeopOByOlS0EtOVSYTLJ5bzeK51cf9rqt/mB0H+thxsI/dXf20d/azu3OAXV39bNjVzf7eweOOKU3EqC0voW5mCXXB8vB2sF5dlqC6rJiKZELfFYigoJcQVSYTmdk7zePfbXNgeIQ9XZng39M1wP7eQTp6BtnfO0RHzyDbD/QdNSvoWGaZ96guKw6WmfWqsmKqyxJUzSimKpmgvLSIitIiZpYmKC8pYmZpETOKi3TxmESGgl7yVmkiTkvtDFpqZ5x0v+GRNAcPZcK/o3eQzr4hXj80nFn2DfN63xBd/cN09A7yyt5eOvuGODQ0ctLXNIPy4iLKSzPBP/YkMHoiKCuOkzy8jFMW/CQTRUfWi+OUBfuUFMU07CShyFnQm9m7gf8NxIHvufttuXovKWyJeIyGitLTupJ3MDVCV98wXf3DdA+k6B1M0TMwTO9Aip6BzHrPYGa9dyBFz2DmhLHzYB/dAykODaboHz75yeJYMYNk4sjJoTQRo6QocwIoGbteFKM0Mdp+pK2kKB7sd2Tf0f2Ki2Ik4jGK4kZxfPz1RLCu4azwPPJ8O7ev3MSuzn4aq5Lcsnwh1y9qyvn75iTozSwOfBd4J9AGrDazR919Qy7eT+R0lRTFqa+IUz+F2zyk085AaoS+oRH6hzLLvqHUkfXhEfqHUkH7kX36hzNtQ6k0A8MjDKbSDAyn6eofZnA4zWAqzWAq0z44nGYgNUI2L3eJGYdDPzHmBDC6XhSPURw3io75fVHMKIob8VhmPWZGUcyIx4NlzIjb2O3YkfbYkX2KYkYsNv4+R+8XIxaDoljm5BQzgqVhY9ZjRrDMHGfB9lHrwe8txpH1Y14j139tPfJ8O7c+vPZwB6G9s59bH14LkPOwz1WPfimwxd1fAzCzB4HrAAW9REYsZsGwTG5HQN2dVNoPnxQyJ4Dj11PpNEMpZ3gksz6ccoZG0qRG0gyPOMNB2/BI+qj1Y48bSnnm+JHMPr2pFEOpNCNpP/yTOrxMM5KGkXT6cNvYfaYTO3zCOHLiiFnm3/PoiWP0hBAfc3IYbY+ZBfuCceQkE4tltl/e083wyNH/TPqHR7h95aZpG/RNwM4x223A5Tl6L5FIM7PDPevp9PgYdyftkEqnSacJTgo+7gnh2BPF4RPJiJP2zEnIHdLBa46kHXdnJNh2z+yfHt1n7Pp422Ne47j18Y457vUy6yPuh+vKvP+R7cxy9J+DHxfyo3Z19uf830Wugn68v4GO+pRmtgJYATB37twclSEiYcn0fCEeG70aurCvil52229pHyfUG6uSOX/vXD1QtA2YM2a7Gdg1dgd3v8vdl7j7krq6uhyVISKSH25ZvpDkMbcASSbi3LJ8Yc7fO1c9+tXAAjObD7QDNwIfydF7iYjkvdFx+MjMunH3lJl9FlhJ5u+1e919fS7eS0Rkurh+UdMZCfZj5Wy6gLs/BjyWq9cXEZGJydUYvYiI5AkFvYhIxCnoRUQiTkEvIhJxCnoRkYhT0IuIRJyCXkQk4syzef/TyRZh1gFsn+ThtcD+LJaTj6L+GfX5pjd9vvDMc/dT3kMmL4J+Ksys1d2XhF1HLkX9M+rzTW/6fPlPQzciIhGnoBcRibgoBP1dYRdwBkT9M+rzTW/6fHlu2o/Ri4jIyUWhRy8iIicxbYPezO41s31mti7sWnLBzOaY2ZNmttHM1pvZ58OuKZvMrNTMVpnZi8Hn+3rYNeWCmcXN7Hkz+0XYteSCmW0zs7Vm9oKZtYZdT7aZWZWZ/djMXg7+X3xz2DVNxrQdujGzK4Fe4H53f2PY9WSbmc0GZrv7c2Y2E1gDXO/uG0IuLSvMzIAZ7t5rZgngaeDz7v7nkEvLKjP7T8ASoMLd3xt2PdlmZtuAJe6er/PMp8TM7gP+4O7fM7NioMzdO8Ou63RN2x69uz8FHAy7jlxx993u/lyw3gNsBM78o2lyxDN6g81E8DM9ex0nYGbNwHuA74Vdi5w+M6sArgTuAXD3oekY8jCNg76QmFkLsAh4NtxKsisY1ngB2Ac87u6R+nzAPwD/BUiHXUgOOfBrM1tjZivCLibLzgY6gH8Jht++Z2Yzwi5qMhT0ec7MyoGfAF9w9+6w68kmdx9x90uBZmCpmUVmCM7M3gvsc/c1YdeSY8vcfTFwDXBzMKQaFUXAYuBOd18EHAK+FG5Jk6Ogz2PB2PVPgB+6+8Nh15MrwZ/DvwPeHXIp2bQMeF8whv0gcJWZ/SDckrLP3XcFy33AT4Gl4VaUVW1A25i/NH9MJvinHQV9ngq+rLwH2Oju3wq7nmwzszozqwrWk8A7gJfDrSp73P1Wd2929xbgRuC37v7RkMvKKjObEUwUIBjSeBcQmVlw7r4H2GlmC4Omq4FpORmiKOwCJsvMHgDeBtSaWRvwVXe/J9yqsmoZ8DFgbTCODfBld38sxJqyaTZwn5nFyXQ4HnL3SE5BjLAG4KeZPglFwL+6+6/CLSnrPgf8MJhx8xrwiZDrmZRpO71SREQmRkM3IiIRp6AXEYk4Bb2ISMQp6EVEIk5BLyIScQp6EZGIU9CLiEScgl5EJOL+P7IeihVMYkBBAAAAAElFTkSuQmCC\n", + "image/png": "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", "text/plain": [ "
" ] @@ -355,7 +355,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -396,7 +396,7 @@ "source": [ "### Outliers: solution\n", "\n", - "Compute the estimator conditioned on the event the the top most transaction captures the first slot. This decreases `total_weight` on the one hand (thus increasing `p`), while increasing `inclusion_interval` on the other, by capturing a block slot. If this estimator gives lower prediction times we switch to it, and then repeat the process with the next highest transaction. The process convegres when the estimator is no longer improving or if all block slots are captured. " + "Compute the estimator conditioned on the event the top most transaction captures the first slot. This decreases `total_weight` on the one hand (thus increasing `p`), while increasing `inclusion_interval` on the other, by capturing a block slot. If this estimator gives lower prediction times we switch to it, and then repeat the process with the next highest transaction. The process converges when the estimator is no longer improving or if all block slots are captured. " ] }, { @@ -406,7 +406,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYAAAAD8CAYAAAB+UHOxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvhp/UCwAAHmVJREFUeJzt3X2QXXWd5/H39z72U/op6TzQnZggEVREwR7Ah3IdIw5RxzCzYuG4Gl1qU7MwPoyzpehUyYxTM+PUuAO6A9REQeMui7DImjgLg5mAhSgBwlMMhJAQIOkkJB066Tx2p/ve7/5xTie307e7k7597+m+5/OqunXP+Z3fOed7fehPfufR3B0REYmfRNQFiIhINBQAIiIxpQAQEYkpBYCISEwpAEREYkoBICISUwoAEZGYGjcAzOwOM9tnZpuKLPtvZuZmNiucNzP7vpltM7ONZnZJQd/lZrY1/Cyf3J8hIiJn60xGAD8Grjy90czmA1cAOwqalwKLw88K4LawbytwI3AZcClwo5m1lFK4iIiUJjVeB3d/xMwWFll0E/A1YHVB2zLgJx7cXrzezJrNbB7wQWCtu/cAmNlaglC5a6x9z5o1yxcuLLZrEREZzVNPPbXf3dvG6zduABRjZp8Adrn7c2ZWuKgd2Fkw3xW2jdY+poULF7Jhw4aJlCgiEltm9tqZ9DvrADCzOuAvgY8UW1ykzcdoL7b9FQSHj1iwYMHZliciImdoIlcBvRlYBDxnZq8CHcDTZjaX4F/28wv6dgC7x2gfwd1Xununu3e2tY07ghERkQk66wBw99+5+2x3X+juCwn+uF/i7q8Da4DPhVcDXQ70uvse4EHgI2bWEp78/UjYJiIiETmTy0DvAh4DzjezLjO7dozu9wPbgW3AD4DrAMKTv38DPBl+vj10QlhERKJhU/l9AJ2dna6TwCIiZ8fMnnL3zvH66U5gEZGYUgCIiMRUVQbA4b4Bblr7Es/uPBh1KSIiU1ZVBkAu73xv3Vaefu1A1KWIiExZVRkA9dng/rYj/YMRVyIiMnVVZQCkkwlq0gkFgIjIGKoyACAYBRzuUwCIiIymegMgk+Jw30DUZYiITFlVGwC1maQOAYmIjKFqA6Auk+SIDgGJiIyqqgPgkA4BiYiMqooDQCeBRUTGUsUBoHMAIiJjqeoAONo/yFR+2qmISJSqNgDqsynyDsdO5KIuRURkSqraAKjLJAE9DkJEZDRVGwD1meB5QDoRLCJSXNUGgEYAIiJjq9oAGHoiqB4HISJSXNUGwMkRgA4BiYgUNW4AmNkdZrbPzDYVtP2jmb1oZhvN7P+aWXPBsm+Y2TYz22Jmf1DQfmXYts3Mbpj8nzJc3dA5AB0CEhEp6kxGAD8GrjytbS1wobtfBLwEfAPAzN4GXAO8PVznVjNLmlkSuAVYCrwN+HTYt2zqssEIQCeBRUSKGzcA3P0RoOe0tl+6+9Bf1vVARzi9DPipu/e7+yvANuDS8LPN3be7+wngp2Hfshm6CkiHgEREipuMcwD/GXggnG4HdhYs6wrbRmsvm2TCSCeNI/06CSwiUkxJAWBmfwkMAncONRXp5mO0F9vmCjPbYGYburu7SymPbErPAxIRGc2EA8DMlgMfBz7jpx640wXML+jWAeweo30Ed1/p7p3u3tnW1jbR8gDIpBIc0iEgEZGiJhQAZnYl8HXgE+5+rGDRGuAaM8ua2SJgMfAE8CSw2MwWmVmG4ETxmtJKH186aToHICIyitR4HczsLuCDwCwz6wJuJLjqJwusNTOA9e7+p+7+vJndA7xAcGjoenfPhdv5M+BBIAnc4e7Pl+H3DJNJJjh0XOcARESKGTcA3P3TRZpvH6P/3wJ/W6T9fuD+s6quRNl0kl4FgIhIUVV7JzBATSqhABARGUVVB0A2HbwXWC+FEREZqboDIJVgIOf0DeSjLkVEZMqp6gCoSQWPg9BhIBGRkao6ALLp4OcpAERERqruAEgpAERERlPVAVCT1iEgEZHRVHUAaAQgIjK6qg4AjQBEREZX1QGQ0QhARGRUVR0ACTNq0noekIhIMVUdABC8E0ABICIyUgwCQM8DEhEpRgEgIhJTVR8AmVSCg8cUACIip6v6AKjROwFERIqq+gDIphIc6lMAiIicrvoDIJ2kfzBP30Au6lJERKaUqg+AmvBmMF0KKiIyXNUHQG34OIgDOhEsIjLMuAFgZneY2T4z21TQ1mpma81sa/jdErabmX3fzLaZ2UYzu6RgneVh/61mtrw8P2ekoecB9Rw9UaldiohMC2cyAvgxcOVpbTcA69x9MbAunAdYCiwOPyuA2yAIDOBG4DLgUuDGodAot9rM0AhAASAiUmjcAHD3R4Ce05qXAavC6VXAVQXtP/HAeqDZzOYBfwCsdfcedz8ArGVkqJRFrUYAIiJFTfQcwBx33wMQfs8O29uBnQX9usK20drLbugQ0AEFgIjIMJN9EtiKtPkY7SM3YLbCzDaY2Ybu7u6SC0omjGwqQY8OAYmIDDPRANgbHtoh/N4XtncB8wv6dQC7x2gfwd1Xununu3e2tbVNsLzh6jJJjQBERE4z0QBYAwxdybMcWF3Q/rnwaqDLgd7wENGDwEfMrCU8+fuRsK0iatJJenQZqIjIMKnxOpjZXcAHgVlm1kVwNc93gHvM7FpgB3B12P1+4KPANuAY8AUAd+8xs78Bngz7fdvdTz+xXDbZVIKeo/2V2p2IyLQwbgC4+6dHWbSkSF8Hrh9lO3cAd5xVdZOkNp3UVUAiIqep+juBIbgX4MBRHQISESkUiwCoSSc5PpDTA+FERArEIgB0M5iIyEjxCICMAkBE5HSxCICTdwPrZjARkZNiEQA6BCQiMlKsAkB3A4uInBKLAMimExgaAYiIFIpFACTMqMsm6T6iABARGRKLAACoy6TYf0SPgxARGRKLADh/3wP8YvBP+ZftH4abLoSN90RdkohI5MZ9FtB0d/6+B7ji5b8j7X1BQ+9O+MWXgumLPhVdYSIiEav6EcD7d9xKOt83vHHgOKz7djQFiYhMEVUfADP69xZf0NtV2UJERKaYqg+Aw9k5xRc0dVS2EBGRKabqA+DRBdcxkKgZ3piuhSXfiqYgEZEpoupPAm+ZvRSA97x6C00n9tFXN4+6pX+tE8AiEntVPwKAIARue9dqzu2/k7vff7/++IuIEJMAAKhJJ0gYdB/WzWAiIhCjADAzGrK6G1hEZEhJAWBmf25mz5vZJjO7y8xqzGyRmT1uZlvN7G4zy4R9s+H8tnD5wsn4AWejNpPUCEBEJDThADCzduBLQKe7XwgkgWuAfwBucvfFwAHg2nCVa4ED7n4ecFPYr6Jq00n2KQBERIDSDwGlgFozSwF1wB7gQ8C94fJVwFXh9LJwnnD5EjOzEvd/VuoyKQWAiEhowgHg7ruA7wI7CP7w9wJPAQfdfTDs1gW0h9PtwM5w3cGw/8yJ7n8iGrIpeo6cYDCXr+RuRUSmpFIOAbUQ/Kt+EXAOUA8sLdLVh1YZY1nhdleY2QYz29Dd3T3R8oqqzybJubNf7wUQESnpENCHgVfcvdvdB4D7gPcCzeEhIYAOYHc43QXMBwiXNwE9p2/U3Ve6e6e7d7a1tZVQ3kgNNUFZe3qPT+p2RUSmo1ICYAdwuZnVhcfylwAvAA8Dnwz7LAdWh9NrwnnC5Q+5+4gRQDnNyKYBeL23b5yeIiLVr5RzAI8TnMx9GvhduK2VwNeBr5rZNoJj/LeHq9wOzAzbvwrcUELdE9KQHRoBKABEREp6FpC73wjceFrzduDSIn37gKtL2V+patIJUglj7yEFgIhIbO4EhuBu4Bk1KY0ARESIWQAA1GdTOgksIkJsA0AjABGR2AVAQzbF3kN95PMVvQBJRGTKiWUADOScnmO6GUxE4i2WAQC6F0BEJH4BUKN7AUREIIYBMCMcAew+qCuBRCTeYhcAdZkkqYTRdeBY1KWIiEQqdgFgZjTVptnZoxGAiMRb7AIAgvMAO3o0AhCReItlADTWpNmpQ0AiEnOxDICm2jSH+wbpPT4QdSkiIpGJZQA0hpeC6kSwiMRZPAOgNngxjE4Ei0icxToANAIQkTiLZQDUpBJkUwm6DmgEICLxFcsAMLPgSiBdCioiMRbLAACYoXsBRCTmYhsATXVpdvQc03sBRCS2SgoAM2s2s3vN7EUz22xm7zGzVjNba2Zbw++WsK+Z2ffNbJuZbTSzSybnJ0xMS22G/sE8e/SCeBGJqVJHAN8D/s3dLwDeCWwGbgDWuftiYF04D7AUWBx+VgC3lbjvkjTXBVcCvdJ9NMoyREQiM+EAMLNG4APA7QDufsLdDwLLgFVht1XAVeH0MuAnHlgPNJvZvAlXXqKW+gwAr+w/ElUJIiKRKmUEcC7QDfzIzJ4xsx+aWT0wx933AITfs8P+7cDOgvW7wrZI1GeSZJIJXtYIQERiqpQASAGXALe5+8XAUU4d7inGirSNOANrZivMbIOZbeju7i6hvLGZGS31aV7ZrwAQkXgqJQC6gC53fzycv5cgEPYOHdoJv/cV9J9fsH4HsPv0jbr7SnfvdPfOtra2EsobX1NNmu3dOgQkIvE04QBw99eBnWZ2fti0BHgBWAMsD9uWA6vD6TXA58KrgS4HeocOFUWluT7DroPH6R/MRVmGiEgkUiWu/0XgTjPLANuBLxCEyj1mdi2wA7g67Hs/8FFgG3As7Buplro0eYedPcc4b/aMqMsREamokgLA3Z8FOossWlKkrwPXl7K/ydZcF1wJ9HL3UQWAiMRObO8EBmgNA2DbPp0HEJH4iXUAZFIJmmrTvPj64ahLERGpuFgHAEBrfYYX9xyKugwRkYqLfQDMrM+wff9RTgzmoy5FRKSiFAANGXJ5Z7seCSEiMRP7AJjVkAVgi84DiEjMxD4AWuoyJEwBICLxE/sASCaM1vqMAkBEYif2AQDBlUAv6EogEYkZBQCwzH7D/zm+Av+rZrjpQth4T9QliYiUXanPApr2zt/3AB/uvZlMoj9o6N0Jv/hSMH3Rp6IrTESkzGI/Anj/jlvJeP/wxoHjsO7b0RQkIlIhsQ+AGf17iy/o7apsISIiFRb7ADicnVN8QVNHZQsREamw2AfAowuuYyBRM7wxXQtLvhVNQSIiFRL7k8BbZi8F4PJXbqF5YB/99fOovfKvdQJYRKpe7EcAEITAynev5s39d/Ivl6zWH38RiQUFQCibSjKrIcNTrx2IuhQRkYpQABSY11TLU68dYDCnR0OLSPVTABQ4p7mWYydyekOYiMRCyQFgZkkze8bM/jWcX2Rmj5vZVjO728wyYXs2nN8WLl9Y6r4n2znNwdVAT77aE3ElIiLlNxkjgC8Dmwvm/wG4yd0XAweAa8P2a4ED7n4ecFPYb0qZUZOmqTbNhld1HkBEql9JAWBmHcDHgB+G8wZ8CLg37LIKuCqcXhbOEy5fEvafUuY21vDEKz24e9SliIiUVakjgJuBrwFDZ01nAgfdfTCc7wLaw+l2YCdAuLw37D+lnNNcQ/eRfl5941jUpYiIlNWEA8DMPg7sc/enCpuLdPUzWFa43RVmtsHMNnR3d0+0vAmb31oHwKNbK79vEZFKKmUE8D7gE2b2KvBTgkM/NwPNZjZ0h3EHsDuc7gLmA4TLm4ARZ1vdfaW7d7p7Z1tbWwnlTUxzbXAe4JGt+yu+bxGRSppwALj7N9y9w90XAtcAD7n7Z4CHgU+G3ZYDq8PpNeE84fKHfAoeaDcz5rfU8tjLbzCg+wFEpIqV4z6ArwNfNbNtBMf4bw/bbwdmhu1fBW4ow74nxYLWOo70D/LczoNRlyIiUjaT8jA4d/8V8KtwejtwaZE+fcDVk7G/cpvfWocZPLJ1P50LW6MuR0SkLHQncBE16SRzG2v41ZZ9UZciIlI2CoBRLJxZz8auXl7v7Yu6FBGRslAAjOLNbfUArN08yisjRUSmOQXAKFrrM7TWpXlw0+tRlyIiUhYKgFGYGYvaGnhs+xv0Hh+IuhwRkUmnABjDm9vqyeWdh17UYSARqT4KgDHMbayhsSbF6md2j99ZRGSaUQCMwcx4y5wZ/HrrfroP90ddjojIpFIAjOOCuTPIufOL5zQKEJHqogAYx8yGLHMas9z3dFfUpYiITCoFwBl4y5wZbNp9iC16V7CIVBEFwBl469xGUgnjf65/NepSREQmjQLgDNRmkiye3cB9T+/icJ/uCRCR6qAAOEMXdTRz7ESO+57eFXUpIiKTQgFwhuY21TC3sYZVv32VfH7KvcdGROSsKQDOwjvnN7F9/1E9IE5EqoIC4Cy8ZfYMmuvS/PND25iCb7MUETkrCoCzkEgY717Qwu929fJrvTReRKY5BcBZumDeDGbUpLj531/SKEBEpjUFwFlKJRL83sJWnt5xkAef17kAEZm+JhwAZjbfzB42s81m9ryZfTlsbzWztWa2NfxuCdvNzL5vZtvMbKOZXTJZP6LS3j6vkZn1Gf7+gc0M5PJRlyMiMiGljAAGgb9w97cClwPXm9nbgBuAde6+GFgXzgMsBRaHnxXAbSXsO1KJhPHe82by2hvHuHP9a1GXIyIyIRMOAHff4+5Ph9OHgc1AO7AMWBV2WwVcFU4vA37igfVAs5nNm3DlEVs0s54FrXV895cvsfeQXhwvItPPpJwDMLOFwMXA48Acd98DQUgAs8Nu7cDOgtW6wrZpycz4/fPb6BvI8Vdrno+6HBGRs1ZyAJhZA/Az4CvufmisrkXaRlxGY2YrzGyDmW3o7u4utbyyaq7LcOmiVh7Y9DoPPq+Xx4vI9FJSAJhZmuCP/53ufl/YvHfo0E74vS9s7wLmF6zeAYx4y4q7r3T3TnfvbGtrK6W8irhkQQttM7Lc8LON7NOhIBGZRkq5CsiA24HN7v5PBYvWAMvD6eXA6oL2z4VXA10O9A4dKprOkgnjyrfP5Uj/IF+5+1k9J0hEpo1SRgDvAz4LfMjMng0/HwW+A1xhZluBK8J5gPuB7cA24AfAdSXse0pprc/wgcVt/PblN7jl4W1RlyMickZSE13R3R+l+HF9gCVF+jtw/UT3N9W9/ZxGdh08zn9f+xKL5zRw5YXT9gInEYkJ3Qk8ScyMJRfMZl5TDV+5+1k27eqNuiQRkTEpACZRKpngY++YRyaV4HN3PMHL3UeiLklEZFQKgElWn01x1Tvb6R/I8Sc/WM+ON45FXZKISFEKgDJoqc9w1cXtHDo+yKdWPsa2fYejLklEZAQFQJnMasjyRxe3c7hvgP9422M89dqBqEsSERlGAVBGbTOyXP3u+SQM/uQH6/n5M3qhvIhMHQqAMmuqTfPJd3cwqyHLV+5+lhtXb+LEoB4hLSLRUwBUQF0mxR9d3M7FC5pZ9dhr/PGtv2HL6zovICLRUgBUSDJhfGBxGx97xzy27z/Kx//Hr7nl4W16oYyIREYBUGHnzW7gM5ctYOHMev7xwS1cefMj/GrLvvFXFBGZZAqACNRlUnz0HfP4w4vm0XP0BJ//0ZN8/o4n2Nh1MOrSRCRGJvwsICnduW0NLJhZx3M7e1n/yht84p9/w++f38YXlyzmkgUtUZcnIlVOARCxVCLBu9/UwoXtjTzX1cvjr/Tw8K2/5Z0dTXz2PQv5+EXzqEkng84b74F134beLmjqgCXfgos+Fe0PEJFpy4KHdE5NnZ2dvmHDhgmt+9Lew/y/jdPvdQMnBvO8sOcQm3b18sbREzTVpln2rnP4/IwnWPTYN7GB46c6p2vhD7+vEBCRYczsKXfvHK+fzgFMMZlUgnfNb+Yzly3gjy9uZ3Zjlv/9+A4yv/rb4X/8AQaOByMCEZEJ0CGgKcrMmN9ax/zWOvoHc7Q//kbRft7bxe6Dx2lvrq1whSIy3SkApoFsKsnh7Bwa+0e+eH5Xfibv/85DzGuq4bJFrfzeolYuWdDCebMbSCc1wBOR0SkApolHF1zHFS//Hen8qRfPDyRqeHT+f+U/WBu7Dh7n3zfv4+fP7gYgk0xw/twZXNjexNvPaeSt8xo5r62Bprp0VD9BRKYYBcA0sWX2UgDev+NWZvTv5XB2Do8uuI49s5fyLuBd85txdw4eH2DvoT66D/fTfaSfnz+zi7ue2HFyOy11ac6b3cCb24LPm2bW0d5SS0dzHY21KcxGe8uniFQbBcA0smX20pNBUIyZ0VKXoaUuwwVzgzZ353DfIPuP9HPg2AAHjp1gz8E+Xth9iKMncsPWr8skaW+upaOllnOaa5nXVMOshixtM4LPrIbgk0np0JJINah4AJjZlcD3gCTwQ3f/TqVriBMzo7E2TWPtyEM/fQM5eo8PcKhvgMN9gxw+Psjh/gFe2HOI9dt7OD6QK7JFaKxJ0TYjy8yGLE21aZpr0zSFn+a6YF/NdZmTbTNqUtRnUtSkExphiEwhFQ0AM0sCtwBXAF3Ak2a2xt1fqGQdEqhJJ6lJJ5nTWFN0+WAuz7ETufAzyLETOY6G38f6c+w6cJxX9h+lfyDH8YEcA7mx7ylJGNRmktRnUtRnU9RnkzRkg3Coy6ZoyCapy6SoTSfJphLUpJNk0wlqUsF3NjV8/uR3Qf900kgnEiQSChqZRiK6ybPSI4BLgW3uvh3AzH4KLAMUAFNQKpmgsTZRdPRQTC7v9A3k6B/MD/s+kcszMJhnIOfBdC7PicE8R/oGOXB0gMF8MD+Q8/A7T6m3JybNSCWDTzqRIJ1MBNPJRBASyUTR6VRBWyqRIJmA5NC3GYmEkUoE30kzkonwEy5LDi0Plw31P7U83J4F04mwzoSd+piBWbDMgETCCPIs+B7qc7IvRiJxqr9Z0G/oOxGOuoa2Y5xaPrSdoXYr2E6xWoZGcMF08J+1RnUl2ngP/OJLwX09AL07g3koewhUOgDagZ0F813AZRWuQcokmbDwX/albcfdyTsM5vPk8s5gzoPvvDOYz4+czzu5XDCfcyefd/Lu5PMMm88VtJ0YzHN8IHdyX0GfsP/JdSDvjo/yPTQ9de+lr6yhGBgKpaEGK2yjMDiGlg8F2akNDbWdWt+Gbf9k15PBdGpbBbs+uV87vZaCjZxe2+lBR5HlheueKttGto23HLjj4DeZnR/lJs8qC4Bi/1QY9v8fM1sBrABYsGDBhHd07qx6/ssHzp3w+iJnKl8QPCdDJp8n56eW5fLBJ18wXRhIubzjhSHDqSA8FTxBe9BWGEQAwwPr5LrDtuMj1g0C7PT2YBmc2v7QsmBPhdOE00GDe2Hb8L6EtQxfHsz4yeUjt0/YVrj9wr4Mq+X0bZ3amY9Y3yko/eR+C9dnxG899WMK/3AVe6JOsb7D/rMIv9t69o9cGYLDQWVW6QDoAuYXzHcAuws7uPtKYCUEzwKa6I5SyQQNuhFKRKa6mzqCwz6na+oo+64r/RfySWCxmS0yswxwDbCmwjWIiEwdS74VPNixULo2aC+zio4A3H3QzP4MeJDgMtA73P35StYgIjKlDB3nj8FVQLj7/cD9ld6viMiUddGnInmsuw6Si4jElAJARCSmFAAiIjGlABARiSkFgIhITCkARERiSgEgIhJTCgARkZgyL/YUoynCzLqB16KuYwJmAaM84anqxOm3gn5vNaum3/omd28br9OUDoDpysw2uHtn1HVUQpx+K+j3VrM4/dYhOgQkIhJTCgARkZhSAJTHyqgLqKA4/VbQ761mcfqtgM4BiIjElkYAIiIxpQCYJGY238weNrPNZva8mX056prKzcySZvaMmf1r1LWUm5k1m9m9ZvZi+N/xe6KuqZzM7M/D/x1vMrO7zKwm6pomk5ndYWb7zGxTQVurma01s63hd0uUNVaCAmDyDAJ/4e5vBS4Hrjezt0VcU7l9GdgcdREV8j3g39z9AuCdVPHvNrN24EtAp7tfSPD2vmuirWrS/Ri48rS2G4B17r4YWBfOVzUFwCRx9z3u/nQ4fZjgD0R7tFWVj5l1AB8Dfhh1LeVmZo3AB4DbAdz9hLsfjLaqsksBtWaWAuqA3RHXM6nc/RGg57TmZcCqcHoVcFVFi4qAAqAMzGwhcDHweLSVlNXNwNeAfNSFVMC5QDfwo/CQ1w/NrD7qosrF3XcB3wV2AHuAXnf/ZbRVVcQcd98DwT/ogNkR11N2CoBJZmYNwM+Ar7j7oajrKQcz+ziwz92firqWCkkBlwC3ufvFwFGq+PBAeOx7GbAIOAeoN7P/FG1VUg4KgElkZmmCP/53uvt9UddTRu8DPmFmrwI/BT5kZv8r2pLKqgvocvehEd29BIFQrT4MvOLu3e4+ANwHvDfimiphr5nNAwi/90VcT9kpACaJmRnBMeLN7v5PUddTTu7+DXfvcPeFBCcHH3L3qv0Xoru/Duw0s/PDpiXACxGWVG47gMvNrC783/USqvikd4E1wPJwejmwOsJaKiIVdQFV5H3AZ4HfmdmzYds33f3+CGuSyfNF4E4zywDbgS9EXE/ZuPvjZnYv8DTB1W3PUGV3yZrZXcAHgVlm1gXcCHwHuMfMriUIwaujq7AydCewiEhM6RCQiEhMKQBERGJKASAiElMKABGRmFIAiIjElAJARCSmFAAiIjGlABARian/D+hfAG5Faoo0AAAAAElFTkSuQmCC\n", + "image/png": "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", "text/plain": [ "
" ] diff --git a/mining/src/mempool/model/frontier/search_tree.rs b/mining/src/mempool/model/frontier/search_tree.rs index fc18b2118..edf34c271 100644 --- a/mining/src/mempool/model/frontier/search_tree.rs +++ b/mining/src/mempool/model/frontier/search_tree.rs @@ -157,8 +157,9 @@ type InnerTree = BPlusTree>; /// is recomputed from subtree weights for each item insertion/removal /// /// Computing the prefix weight is a crucial operation if the tree is used for random sampling and -/// the tree is highly imbalanced in terms of weight variance. See [`Frontier::sample_inplace`] for -/// more details. +/// the tree is highly imbalanced in terms of weight variance. +/// See [`Frontier::sample_inplace()`](crate::mempool::model::frontier::Frontier::sample_inplace) +/// for more details. pub struct SearchTree { tree: InnerTree, } diff --git a/mining/src/mempool/remove_transaction.rs b/mining/src/mempool/remove_transaction.rs index 960ebc264..3aac8a20b 100644 --- a/mining/src/mempool/remove_transaction.rs +++ b/mining/src/mempool/remove_transaction.rs @@ -4,7 +4,7 @@ use crate::mempool::{ Mempool, }; use kaspa_consensus_core::tx::TransactionId; -use kaspa_core::{debug, warn}; +use kaspa_core::debug; use kaspa_utils::iter::IterExtensions; impl Mempool { @@ -43,8 +43,8 @@ impl Mempool { TxRemovalReason::Muted => {} TxRemovalReason::DoubleSpend => match removed_transactions.len() { 0 => {} - 1 => warn!("Removed transaction ({}) {}{}", reason, removed_transactions[0], extra_info), - n => warn!( + 1 => debug!("Removed transaction ({}) {}{}", reason, removed_transactions[0], extra_info), + n => debug!( "Removed {} transactions ({}): {}{}", n, reason, diff --git a/notify/src/address/tracker.rs b/notify/src/address/tracker.rs index f103b163c..a2b1c64dd 100644 --- a/notify/src/address/tracker.rs +++ b/notify/src/address/tracker.rs @@ -384,11 +384,11 @@ impl Inner { } } -/// Tracker of a set of [`Address`](kaspa_addresses::Address), indexing and counting registrations +/// Tracker of a set of [`Address`], indexing and counting registrations /// /// #### Implementation design /// -/// Each [`Address`](kaspa_addresses::Address) is stored internally as a [`ScriptPubKey`](kaspa_consensus_core::tx::ScriptPublicKey). +/// Each [`Address`] is stored internally as a [`ScriptPubKey`](kaspa_consensus_core::tx::ScriptPublicKey). /// This prevents inter-network duplication and optimizes UTXOs filtering efficiency. /// /// But consequently the address network prefix gets lost and must be globally provided when querying for addresses by indexes. diff --git a/notify/src/notifier.rs b/notify/src/notifier.rs index 220fd261b..6927ea1a1 100644 --- a/notify/src/notifier.rs +++ b/notify/src/notifier.rs @@ -75,8 +75,8 @@ pub type DynNotify = Arc>; /// /// - a vector of [`DynCollector`] /// - a vector of [`Subscriber`] -/// - a pool of [`Broadcaster`] -/// - a map of [`Listener`] +/// - a pool of `Broadcaster` +/// - a map of `Listener` /// /// Collectors and subscribers form the scaffold. They are provided to the ctor, are immutable and share its /// lifespan. Both do materialize a connection to the notifier _parents_, collectors for incoming notifications diff --git a/protocol/flows/src/flowcontext/transactions.rs b/protocol/flows/src/flowcontext/transactions.rs index d3112f0af..110b378b7 100644 --- a/protocol/flows/src/flowcontext/transactions.rs +++ b/protocol/flows/src/flowcontext/transactions.rs @@ -73,7 +73,7 @@ impl TransactionsSpread { /// within transaction Inv messages. /// /// The broadcast itself may happen only during a subsequent call to this function since it is done at most - /// every [`BROADCAST_INTERVAL`] milliseconds or when the queue length is larger than the Inv message + /// every `BROADCAST_INTERVAL` milliseconds or when the queue length is larger than the Inv message /// capacity. /// /// _GO-KASPAD: EnqueueTransactionIDsForPropagation_ diff --git a/protocol/p2p/src/core/connection_handler.rs b/protocol/p2p/src/core/connection_handler.rs index a8ec431e4..54d387043 100644 --- a/protocol/p2p/src/core/connection_handler.rs +++ b/protocol/p2p/src/core/connection_handler.rs @@ -9,7 +9,7 @@ use kaspa_core::{debug, info}; use kaspa_utils::networking::NetAddress; use kaspa_utils_tower::{ counters::TowerConnectionCounters, - middleware::{measure_request_body_size_layer, CountBytesBody, MapResponseBodyLayer, ServiceBuilder}, + middleware::{BodyExt, CountBytesBody, MapRequestBodyLayer, MapResponseBodyLayer, ServiceBuilder}, }; use std::net::ToSocketAddrs; use std::pin::Pin; @@ -20,7 +20,6 @@ use tokio::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender}; use tokio::sync::oneshot::{channel as oneshot_channel, Sender as OneshotSender}; use tokio_stream::wrappers::ReceiverStream; use tokio_stream::StreamExt; -use tonic::codegen::Body; use tonic::transport::{Error as TonicError, Server as TonicServer}; use tonic::{Request, Response, Status as TonicStatus, Streaming}; @@ -80,7 +79,7 @@ impl ConnectionHandler { // TODO: check whether we should set tcp_keepalive let serve_result = TonicServer::builder() - .layer(measure_request_body_size_layer(bytes_rx, |b| b)) + .layer(MapRequestBodyLayer::new(move |body| CountBytesBody::new(body, bytes_rx.clone()).boxed_unsync())) .layer(MapResponseBodyLayer::new(move |body| CountBytesBody::new(body, bytes_tx.clone()))) .add_service(proto_server) .serve_with_shutdown(serve_address.into(), termination_receiver.map(drop)) @@ -110,9 +109,7 @@ impl ConnectionHandler { let channel = ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| CountBytesBody::new(body, self.counters.bytes_rx.clone()))) - .layer(measure_request_body_size_layer(self.counters.bytes_tx.clone(), |body| { - body.map_err(|e| tonic::Status::from_error(Box::new(e))).boxed_unsync() - })) + .layer(MapRequestBodyLayer::new(move |body| CountBytesBody::new(body, self.counters.bytes_tx.clone()).boxed_unsync())) .service(channel); let mut client = ProtoP2pClient::new(channel) diff --git a/rothschild/src/main.rs b/rothschild/src/main.rs index d303f1da0..35d08493b 100644 --- a/rothschild/src/main.rs +++ b/rothschild/src/main.rs @@ -17,7 +17,10 @@ use kaspa_rpc_core::{api::rpc::RpcApi, notify::mode::NotificationMode, RpcUtxoEn 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, Rng}, + Keypair, +}; use tokio::time::{interval, MissedTickBehavior}; const DEFAULT_SEND_AMOUNT: u64 = 10 * SOMPI_PER_KASPA; @@ -40,6 +43,9 @@ pub struct Args { pub rpc_server: String, pub threads: u8, pub unleashed: bool, + pub addr: Option, + pub priority_fee: u64, + pub randomize_fee: bool, } impl Args { @@ -51,6 +57,9 @@ impl Args { rpc_server: m.get_one::("rpcserver").cloned().unwrap_or("localhost:16210".to_owned()), threads: m.get_one::("threads").cloned().unwrap(), unleashed: m.get_one::("unleashed").cloned().unwrap_or(false), + addr: m.get_one::("addr").cloned(), + priority_fee: m.get_one::("priority-fee").cloned().unwrap_or(0), + randomize_fee: m.get_one::("randomize-fee").cloned().unwrap_or(false), } } } @@ -85,6 +94,25 @@ pub fn cli() -> Command { .help("The number of threads to use for TX generation. Set to 0 to use 1 thread per core. Default is 2."), ) .arg(Arg::new("unleashed").long("unleashed").action(ArgAction::SetTrue).hide(true).help("Allow higher TPS")) + .arg(Arg::new("addr").long("to-addr").short('a').value_name("addr").help("address to send to")) + .arg( + Arg::new("priority-fee") + .long("priority-fee") + .short('f') + .value_name("priority-fee") + .default_value("0") + .value_parser(clap::value_parser!(u64)) + .help("Transaction priority fee"), + ) + .arg( + Arg::new("randomize-fee") + .long("randomize-fee") + .short('r') + .value_name("randomize-fee") + .action(ArgAction::SetTrue) + .default_value("false") + .help("Randomize transaction priority fee"), + ) } async fn new_rpc_client(subscription_context: &SubscriptionContext, address: &str) -> GrpcClient { @@ -111,6 +139,11 @@ struct ClientPoolArg { utxos_len: usize, } +struct TxsFeeConfig { + priority_fee: u64, + randomize_fee: bool, +} + #[tokio::main] async fn main() { kaspa_core::log::init_logger(None, ""); @@ -150,9 +183,31 @@ async fn main() { let kaspa_addr = Address::new(ADDRESS_PREFIX, ADDRESS_VERSION, &schnorr_key.x_only_public_key().0.serialize()); + let kaspa_to_addr = args.addr.as_ref().map_or_else(|| kaspa_addr.clone(), |addr_str| Address::try_from(addr_str.clone()).unwrap()); + + let fee_config = TxsFeeConfig { priority_fee: args.priority_fee, randomize_fee: args.randomize_fee }; + rayon::ThreadPoolBuilder::new().num_threads(args.threads as usize).build_global().unwrap(); - info!("Using Rothschild with private key {} and address {}", schnorr_key.display_secret(), String::from(&kaspa_addr)); + let mut log_message = format!( + "Using Rothschild with:\n\ + \tprivate key: {}\n\ + \tfrom address: {}", + schnorr_key.display_secret(), + String::from(&kaspa_addr) + ); + if args.addr.is_some() { + log_message.push_str(&format!("\n\tto address: {}", String::from(&kaspa_to_addr))); + } + if args.priority_fee != 0 { + log_message.push_str(&format!( + "\n\tpriority fee: {} SOMPS {}", + fee_config.priority_fee, + if fee_config.randomize_fee { "[randomize]" } else { "" } + )); + } + info!("{}", log_message); + let info = rpc_client.get_block_dag_info().await.unwrap(); let coinbase_maturity = match info.network.suffix { Some(11) => TESTNET11_PARAMS.coinbase_maturity, @@ -249,13 +304,14 @@ async fn main() { let has_funds = maybe_send_tx( txs_to_send, &tx_sender, - kaspa_addr.clone(), + kaspa_to_addr.clone(), &mut utxos, &mut pending, schnorr_key, stats.clone(), maximize_inputs, &mut next_available_utxo_index, + &fee_config, ) .await; if !has_funds { @@ -369,6 +425,7 @@ async fn maybe_send_tx( stats: Arc>, maximize_inputs: bool, next_available_utxo_index: &mut usize, + fee_config: &TxsFeeConfig, ) -> bool { let num_outs = if maximize_inputs { 1 } else { 2 }; @@ -377,7 +434,7 @@ async fn maybe_send_tx( let selected_utxos_groups = (0..txs_to_send) .map(|_| { let (selected_utxos, selected_amount) = - select_utxos(utxos, DEFAULT_SEND_AMOUNT, num_outs, maximize_inputs, next_available_utxo_index); + select_utxos(utxos, DEFAULT_SEND_AMOUNT, num_outs, maximize_inputs, next_available_utxo_index, fee_config); if selected_amount == 0 { return None; } @@ -473,10 +530,12 @@ fn select_utxos( num_outs: u64, maximize_utxos: bool, next_available_utxo_index: &mut usize, + fee_config: &TxsFeeConfig, ) -> (Vec<(TransactionOutpoint, UtxoEntry)>, u64) { const MAX_UTXOS: usize = 84; let mut selected_amount: u64 = 0; let mut selected = Vec::new(); + let mut rng = thread_rng(); while next_available_utxo_index < &mut utxos.len() { let (outpoint, entry) = utxos[*next_available_utxo_index].clone(); @@ -484,11 +543,16 @@ fn select_utxos( selected.push((outpoint, entry)); let fee = required_fee(selected.len(), num_outs); + let priority_fee = if fee_config.randomize_fee && fee_config.priority_fee > 0 { + rng.gen_range(0..fee_config.priority_fee) + } else { + fee_config.priority_fee + }; *next_available_utxo_index += 1; - if selected_amount >= min_amount + fee && (!maximize_utxos || selected.len() == MAX_UTXOS) { - return (selected, selected_amount - fee); + if selected_amount >= min_amount + fee + priority_fee && (!maximize_utxos || selected.len() == MAX_UTXOS) { + return (selected, selected_amount - fee - priority_fee); } if selected.len() > MAX_UTXOS { diff --git a/rpc/core/src/api/connection.rs b/rpc/core/src/api/connection.rs index 5b4254288..fba2aa71a 100644 --- a/rpc/core/src/api/connection.rs +++ b/rpc/core/src/api/connection.rs @@ -1,3 +1,7 @@ +//! +//! Generic connection trait representing a connection to a client (where available). +//! + use std::sync::Arc; pub trait RpcConnection: Send + Sync { diff --git a/rpc/core/src/api/ctl.rs b/rpc/core/src/api/ctl.rs index 49241e7d9..d7705127b 100644 --- a/rpc/core/src/api/ctl.rs +++ b/rpc/core/src/api/ctl.rs @@ -1,3 +1,7 @@ +//! +//! Client-side RPC helper for handling connection and disconnection events. +//! + use crate::error::RpcResult; use std::sync::{Arc, Mutex}; use workflow_core::channel::Multiplexer; diff --git a/rpc/core/src/api/mod.rs b/rpc/core/src/api/mod.rs index 1373bd6e0..a75056a84 100644 --- a/rpc/core/src/api/mod.rs +++ b/rpc/core/src/api/mod.rs @@ -1,3 +1,7 @@ +//! +//! API module for the RPC server. Implements core RPC primitives. +//! + pub mod connection; pub mod ctl; pub mod notifications; diff --git a/rpc/core/src/api/notifications.rs b/rpc/core/src/api/notifications.rs index e07a7c4d9..503af0de8 100644 --- a/rpc/core/src/api/notifications.rs +++ b/rpc/core/src/api/notifications.rs @@ -1,3 +1,7 @@ +//! +//! RPC notifications that can be sent to clients. +//! + use crate::model::message::*; use derive_more::Display; use kaspa_notify::{ diff --git a/rpc/core/src/api/ops.rs b/rpc/core/src/api/ops.rs index 822798a1d..26ca356eb 100644 --- a/rpc/core/src/api/ops.rs +++ b/rpc/core/src/api/ops.rs @@ -1,3 +1,7 @@ +//! +//! RPC Operations used to identify RPC methods during transport and in various RPC-related macros. +//! + use borsh::{BorshDeserialize, BorshSerialize}; use kaspa_notify::events::EventType; use serde::{Deserialize, Serialize}; diff --git a/rpc/core/src/api/rpc.rs b/rpc/core/src/api/rpc.rs index 85713e547..cadc9e00c 100644 --- a/rpc/core/src/api/rpc.rs +++ b/rpc/core/src/api/rpc.rs @@ -1,8 +1,10 @@ -//! The client API +//! +//! The main [`RpcApi`] trait that defines all RPC methods available in the Rusty Kaspa p2p node. //! //! Rpc = External RPC Service -//! All data provided by the RCP server can be trusted by the client -//! No data submitted by the client to the server can be trusted +//! All data provided by the RPC server can be trusted by the client +//! No data submitted by the client to the node can be trusted +//! use crate::api::connection::DynRpcConnection; use crate::{model::*, notify::connection::ChannelConnection, RpcResult}; diff --git a/rpc/core/src/convert/block.rs b/rpc/core/src/convert/block.rs index 8888fe2bb..8cb0ab01e 100644 --- a/rpc/core/src/convert/block.rs +++ b/rpc/core/src/convert/block.rs @@ -1,3 +1,5 @@ +//! Conversion of Block related types + use std::sync::Arc; use crate::{RpcBlock, RpcError, RpcRawBlock, RpcResult, RpcTransaction}; diff --git a/rpc/core/src/convert/mod.rs b/rpc/core/src/convert/mod.rs index dee1988d5..bc5c0e64b 100644 --- a/rpc/core/src/convert/mod.rs +++ b/rpc/core/src/convert/mod.rs @@ -1,3 +1,7 @@ +//! +//! Data conversion utilities and structs for the RPC layer. +//! + pub mod block; pub mod notification; pub mod scope; diff --git a/rpc/core/src/convert/notification.rs b/rpc/core/src/convert/notification.rs index 362dd5ed9..6251cc1cd 100644 --- a/rpc/core/src/convert/notification.rs +++ b/rpc/core/src/convert/notification.rs @@ -1,3 +1,5 @@ +//! Conversion of Notification related types + use crate::{ convert::utxo::utxo_set_into_rpc, BlockAddedNotification, FinalityConflictNotification, FinalityConflictResolvedNotification, NewBlockTemplateNotification, Notification, PruningPointUtxoSetOverrideNotification, RpcAcceptedTransactionIds, diff --git a/rpc/core/src/convert/scope.rs b/rpc/core/src/convert/scope.rs index e38f09a1f..6d94de326 100644 --- a/rpc/core/src/convert/scope.rs +++ b/rpc/core/src/convert/scope.rs @@ -1,3 +1,5 @@ +//! Conversion of Notification Scope related types + use crate::{ NotifyBlockAddedRequest, NotifyFinalityConflictRequest, NotifyNewBlockTemplateRequest, NotifyPruningPointUtxoSetOverrideRequest, NotifySinkBlueScoreChangedRequest, NotifyUtxosChangedRequest, NotifyVirtualChainChangedRequest, diff --git a/rpc/core/src/convert/tx.rs b/rpc/core/src/convert/tx.rs index 20d41d674..9b69ca168 100644 --- a/rpc/core/src/convert/tx.rs +++ b/rpc/core/src/convert/tx.rs @@ -1,3 +1,5 @@ +//! Conversion of Transaction related types + use crate::{RpcError, RpcResult, RpcTransaction, RpcTransactionInput, RpcTransactionOutput}; use kaspa_consensus_core::tx::{Transaction, TransactionInput, TransactionOutput}; diff --git a/rpc/core/src/convert/utxo.rs b/rpc/core/src/convert/utxo.rs index a0376580d..5fc09f690 100644 --- a/rpc/core/src/convert/utxo.rs +++ b/rpc/core/src/convert/utxo.rs @@ -1,3 +1,5 @@ +//! Conversion functions for UTXO related types. + use crate::RpcUtxoEntry; use crate::RpcUtxosByAddressesEntry; use kaspa_addresses::Prefix; diff --git a/rpc/core/src/error.rs b/rpc/core/src/error.rs index 235ea639e..0e2bfee22 100644 --- a/rpc/core/src/error.rs +++ b/rpc/core/src/error.rs @@ -1,3 +1,7 @@ +//! +//! [`RpcError`] enum used by RPC primitives. +//! + use kaspa_consensus_core::{subnets::SubnetworkConversionError, tx::TransactionId}; use kaspa_utils::networking::IpAddress; use std::{net::AddrParseError, num::TryFromIntError}; diff --git a/rpc/core/src/lib.rs b/rpc/core/src/lib.rs index 66e4ece3a..a2ece77d4 100644 --- a/rpc/core/src/lib.rs +++ b/rpc/core/src/lib.rs @@ -1,3 +1,16 @@ +//! # RPC Core +//! +//! This crate provides foundational primitives used in Rusty Kaspa node RPC subsystem. +//! These include the main [`RpcApi`](api::rpc::RpcApi) trait, [`RpcApiOps`](crate::api::ops::RpcApiOps) +//! enum used in RPC method dispatching, and various data structures used in RPC method arguments. +//! +//! This crate acts as a foundation for [`kaspa_grpc_client`](https://docs.rs/kaspa_grpc_client) and +//! [`kaspa_wrpc_client`](https://docs.rs/kaspa_wrpc_client) crates, which provide gRPC and WebSocket +//! RPC client implementations. This crate is also used by WASM bindings to provide [WASM RpcClient +//! implementation](https://docs.rs/kaspa-wrpc-client/latest/kaspa_wrpc_client/wasm/struct.RpcClient.html) +//! (based on wRPC). +//! + // This attribute is required by BorshSerialize/Deserialize #![recursion_limit = "256"] @@ -9,6 +22,7 @@ pub mod notify; pub mod wasm; pub mod prelude { + //! Re-exports of the most commonly used types and traits in this crate. pub use super::api::notifications::*; pub use super::model::script_class::*; pub use super::model::*; diff --git a/rpc/core/src/model/mod.rs b/rpc/core/src/model/mod.rs index beef03257..a7c255624 100644 --- a/rpc/core/src/model/mod.rs +++ b/rpc/core/src/model/mod.rs @@ -1,3 +1,6 @@ +//! This module contains RPC-specific data structures +//! used in RPC methods. + pub mod address; pub mod block; pub mod blue_work; diff --git a/rpc/core/src/model/tx.rs b/rpc/core/src/model/tx.rs index c2b91f187..0c17e26f5 100644 --- a/rpc/core/src/model/tx.rs +++ b/rpc/core/src/model/tx.rs @@ -4,7 +4,7 @@ use kaspa_consensus_core::tx::{ ScriptPublicKey, ScriptVec, TransactionId, TransactionIndexType, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry, }; -use kaspa_utils::serde_bytes_fixed_ref; +use kaspa_utils::{hex::ToHex, serde_bytes_fixed_ref}; use serde::{Deserialize, Serialize}; use workflow_serializer::prelude::*; @@ -131,7 +131,7 @@ impl Deserializer for RpcTransactionOutpoint { } /// Represents a Kaspa transaction input -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransactionInput { pub previous_outpoint: RpcTransactionOutpoint, @@ -142,6 +142,18 @@ pub struct RpcTransactionInput { pub verbose_data: Option, } +impl std::fmt::Debug for RpcTransactionInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RpcTransactionInput") + .field("previous_outpoint", &self.previous_outpoint) + .field("signature_script", &self.signature_script.to_hex()) + .field("sequence", &self.sequence) + .field("sig_op_count", &self.sig_op_count) + .field("verbose_data", &self.verbose_data) + .finish() + } +} + impl From for RpcTransactionInput { fn from(input: TransactionInput) -> Self { Self { @@ -277,7 +289,7 @@ impl Deserializer for RpcTransactionOutputVerboseData { } /// Represents a Kaspa transaction -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcTransaction { pub version: u16, @@ -292,6 +304,22 @@ pub struct RpcTransaction { pub verbose_data: Option, } +impl std::fmt::Debug for RpcTransaction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RpcTransaction") + .field("version", &self.version) + .field("lock_time", &self.lock_time) + .field("subnetwork_id", &self.subnetwork_id) + .field("gas", &self.gas) + .field("payload", &self.payload.to_hex()) + .field("mass", &self.mass) + .field("inputs", &self.inputs) // Inputs and outputs are placed purposely at the end for better debug visibility + .field("outputs", &self.outputs) + .field("verbose_data", &self.verbose_data) + .finish() + } +} + impl Serializer for RpcTransaction { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { store!(u16, &1, writer)?; diff --git a/rpc/core/src/notify/mod.rs b/rpc/core/src/notify/mod.rs index 088483e8f..e6dc1be06 100644 --- a/rpc/core/src/notify/mod.rs +++ b/rpc/core/src/notify/mod.rs @@ -1,3 +1,7 @@ +//! +//! Notification structures used by the RPC subsystem. +//! + pub mod channel; pub mod collector; pub mod connection; diff --git a/rpc/core/src/wasm/convert.rs b/rpc/core/src/wasm/convert.rs index 7bc68171b..319f74bf0 100644 --- a/rpc/core/src/wasm/convert.rs +++ b/rpc/core/src/wasm/convert.rs @@ -1,3 +1,7 @@ +//! +//! WASM specific conversion functions +//! + use crate::model::*; use kaspa_consensus_client::*; use std::sync::Arc; diff --git a/rpc/core/src/wasm/message.rs b/rpc/core/src/wasm/message.rs index 4e330c347..85c085702 100644 --- a/rpc/core/src/wasm/message.rs +++ b/rpc/core/src/wasm/message.rs @@ -1,3 +1,7 @@ +//! +//! WASM interfaces and conversion to and from RPC messages. +//! + #![allow(non_snake_case)] use crate::error::RpcError as Error; use crate::error::RpcResult as Result; diff --git a/rpc/core/src/wasm/mod.rs b/rpc/core/src/wasm/mod.rs index 6552baa42..e3bcdc024 100644 --- a/rpc/core/src/wasm/mod.rs +++ b/rpc/core/src/wasm/mod.rs @@ -1,3 +1,5 @@ +//! WASM related conversions + pub mod convert; cfg_if::cfg_if! { diff --git a/rpc/grpc/client/src/lib.rs b/rpc/grpc/client/src/lib.rs index 74db82c4e..b7e53bb5e 100644 --- a/rpc/grpc/client/src/lib.rs +++ b/rpc/grpc/client/src/lib.rs @@ -38,7 +38,7 @@ use kaspa_rpc_core::{ use kaspa_utils::{channel::Channel, triggers::DuplexTrigger}; use kaspa_utils_tower::{ counters::TowerConnectionCounters, - middleware::{measure_request_body_size_layer, CountBytesBody, MapResponseBodyLayer, ServiceBuilder}, + middleware::{BodyExt, CountBytesBody, MapRequestBodyLayer, MapResponseBodyLayer, ServiceBuilder}, }; use regex::Regex; use std::{ @@ -50,7 +50,6 @@ use std::{ }; use tokio::sync::Mutex; use tonic::codec::CompressionEncoding; -use tonic::codegen::Body; use tonic::Streaming; mod connection_event; @@ -102,7 +101,7 @@ impl GrpcClient { /// `url`: the server to connect to /// /// `subscription_context`: it is advised to provide a clone of the same instance if multiple clients dealing with - /// [`UtxosChangedNotifications`] are connected concurrently in order to optimize the memory footprint. + /// `UtxosChangedNotifications` are connected concurrently in order to optimize the memory footprint. /// /// `reconnect`: features an automatic reconnection to the server, reactivating all subscriptions on success. /// @@ -544,9 +543,7 @@ impl Inner { let bytes_tx = &counters.bytes_tx; let channel = ServiceBuilder::new() .layer(MapResponseBodyLayer::new(move |body| CountBytesBody::new(body, bytes_rx.clone()))) - .layer(measure_request_body_size_layer(bytes_tx.clone(), |body| { - body.map_err(|e| tonic::Status::from_error(Box::new(e))).boxed_unsync() - })) + .layer(MapRequestBodyLayer::new(move |body| CountBytesBody::new(body, bytes_tx.clone()).boxed_unsync())) .service(channel); // Build the gRPC client with an interceptor setting the request timeout diff --git a/rpc/grpc/core/proto/rpc.proto b/rpc/grpc/core/proto/rpc.proto index 8c3e7b3b2..e218681b6 100644 --- a/rpc/grpc/core/proto/rpc.proto +++ b/rpc/grpc/core/proto/rpc.proto @@ -374,8 +374,13 @@ message GetSubnetworkResponseMessage{ RPCError error = 1000; } -// GetVirtualChainFromBlockRequestMessage requests the virtual selected -// parent chain from some startHash to this kaspad's current virtual +/// GetVirtualChainFromBlockRequestMessage requests the virtual selected +/// parent chain from some startHash to this kaspad's current virtual +/// Note: +/// this call batches the response to: +/// a. the network's `mergeset size limit * 10` amount of added chain blocks, if `includeAcceptedTransactionIds = false` +/// b. or `mergeset size limit * 10` amount of merged blocks, if `includeAcceptedTransactionIds = true` +/// c. it does not batch the removed chain blocks, only the added ones. message GetVirtualChainFromBlockRequestMessage{ string startHash = 1; bool includeAcceptedTransactionIds = 2; diff --git a/rpc/grpc/core/src/convert/message.rs b/rpc/grpc/core/src/convert/message.rs index c0e75cf03..67ac60650 100644 --- a/rpc/grpc/core/src/convert/message.rs +++ b/rpc/grpc/core/src/convert/message.rs @@ -3,7 +3,7 @@ //! Response payloads in protowire do always contain an error field and generally a set of //! fields providing the requested data. //! -//! Responses in rpc core are expressed as RpcResult, where Xxx is the called +//! Responses in rpc core are expressed as `RpcResult`, where `Xxx` is the called //! RPC method. //! //! The general conversion convention from protowire to rpc core is to consider the error diff --git a/rpc/grpc/server/src/connection_handler.rs b/rpc/grpc/server/src/connection_handler.rs index d581ea441..fd13cf9bb 100644 --- a/rpc/grpc/server/src/connection_handler.rs +++ b/rpc/grpc/server/src/connection_handler.rs @@ -29,7 +29,7 @@ use kaspa_rpc_core::{ use kaspa_utils::networking::NetAddress; use kaspa_utils_tower::{ counters::TowerConnectionCounters, - middleware::{measure_request_body_size_layer, CountBytesBody, MapResponseBodyLayer}, + middleware::{BodyExt, CountBytesBody, MapRequestBodyLayer, MapResponseBodyLayer}, }; use std::fmt::Debug; use std::{ @@ -144,7 +144,7 @@ impl ConnectionHandler { let serve_result = TonicServer::builder() // .http2_keepalive_interval(Some(GRPC_KEEP_ALIVE_PING_INTERVAL)) // .http2_keepalive_timeout(Some(GRPC_KEEP_ALIVE_PING_TIMEOUT)) - .layer(measure_request_body_size_layer(bytes_rx, |b| b)) + .layer(MapRequestBodyLayer::new(move |body| CountBytesBody::new(body, bytes_rx.clone()).boxed_unsync())) .layer(MapResponseBodyLayer::new(move |body| CountBytesBody::new(body, bytes_tx.clone()))) .add_service(protowire_server) .serve_with_shutdown( diff --git a/rpc/service/src/converter/consensus.rs b/rpc/service/src/converter/consensus.rs index a5e5d3b51..c744300e5 100644 --- a/rpc/service/src/converter/consensus.rs +++ b/rpc/service/src/converter/consensus.rs @@ -162,8 +162,9 @@ impl ConsensusConverter { &self, consensus: &ConsensusProxy, chain_path: &ChainPath, + merged_blocks_limit: Option, ) -> RpcResult> { - let acceptance_data = consensus.async_get_blocks_acceptance_data(chain_path.added.clone()).await.unwrap(); + let acceptance_data = consensus.async_get_blocks_acceptance_data(chain_path.added.clone(), merged_blocks_limit).await.unwrap(); Ok(chain_path .added .iter() diff --git a/rpc/service/src/service.rs b/rpc/service/src/service.rs index 2c22fd6bb..d75ff770b 100644 --- a/rpc/service/src/service.rs +++ b/rpc/service/src/service.rs @@ -539,7 +539,7 @@ NOTE: This error usually indicates an RPC conversion error between the node and ) -> RpcResult { let allow_orphan = self.config.unsafe_rpc && request.allow_orphan; if !self.config.unsafe_rpc && request.allow_orphan { - warn!("SubmitTransaction RPC command called with AllowOrphan enabled while node in safe RPC mode -- switching to ForbidOrphan."); + debug!("SubmitTransaction RPC command called with AllowOrphan enabled while node in safe RPC mode -- switching to ForbidOrphan."); } let transaction: Transaction = request.transaction.try_into()?; @@ -609,13 +609,26 @@ NOTE: This error usually indicates an RPC conversion error between the node and request: GetVirtualChainFromBlockRequest, ) -> RpcResult { let session = self.consensus_manager.consensus().session().await; - let virtual_chain = session.async_get_virtual_chain_from_block(request.start_hash).await?; + + // batch_size is set to 10 times the mergeset_size_limit. + // this means batch_size is 2480 on 10 bps, and 1800 on mainnet. + // this bounds by number of merged blocks, if include_accepted_transactions = true + // else it returns the batch_size amount on pure chain blocks. + // Note: batch_size does not bound removed chain blocks, only added chain blocks. + let batch_size = (self.config.mergeset_size_limit * 10) as usize; + let mut virtual_chain_batch = session.async_get_virtual_chain_from_block(request.start_hash, Some(batch_size)).await?; let accepted_transaction_ids = if request.include_accepted_transaction_ids { - self.consensus_converter.get_virtual_chain_accepted_transaction_ids(&session, &virtual_chain).await? + let accepted_transaction_ids = self + .consensus_converter + .get_virtual_chain_accepted_transaction_ids(&session, &virtual_chain_batch, Some(batch_size)) + .await?; + // bound added to the length of the accepted transaction ids, which is bounded by merged blocks + virtual_chain_batch.added.truncate(accepted_transaction_ids.len()); + accepted_transaction_ids } else { vec![] }; - Ok(GetVirtualChainFromBlockResponse::new(virtual_chain.removed, virtual_chain.added, accepted_transaction_ids)) + Ok(GetVirtualChainFromBlockResponse::new(virtual_chain_batch.removed, virtual_chain_batch.added, accepted_transaction_ids)) } async fn get_block_count_call( diff --git a/rpc/wrpc/client/src/client.rs b/rpc/wrpc/client/src/client.rs index 71147fd67..3ac04fa98 100644 --- a/rpc/wrpc/client/src/client.rs +++ b/rpc/wrpc/client/src/client.rs @@ -1,3 +1,5 @@ +//! Kaspa wRPC client implementation. + use crate::imports::*; use crate::parse::parse_host; use crate::{error::Error, node::NodeDescriptor}; @@ -246,14 +248,17 @@ impl RpcResolver for Inner { const WRPC_CLIENT: &str = "wrpc-client"; -/// [`KaspaRpcClient`] allows connection to the Kaspa wRPC Server via -/// binary Borsh or JSON protocols. +/// # [`KaspaRpcClient`] connects to Kaspa wRPC endpoint via binary Borsh or JSON protocols. /// /// RpcClient has two ways to interface with the underlying RPC subsystem: /// [`Interface`] that has a [`notification()`](Interface::notification) /// method to register closures that will be invoked on server-side -/// notifications and the [`RpcClient::call`] method that allows async -/// method invocation server-side. +/// notifications and the [`RpcClient::call`] method that allows server-side +/// async method invocation. +/// +/// The node address can be supplied via a URL or a [`Resolver`] that +/// can be used to resolve a public node address dynamically. [`Resolver`] can also +/// be configured to operate against custom node clusters. /// #[derive(Clone)] pub struct KaspaRpcClient { diff --git a/rpc/wrpc/client/src/error.rs b/rpc/wrpc/client/src/error.rs index 781455ddd..657027ed0 100644 --- a/rpc/wrpc/client/src/error.rs +++ b/rpc/wrpc/client/src/error.rs @@ -1,3 +1,5 @@ +//! [`Error`](enum@Error) variants for the wRPC client library. + use thiserror::Error; use wasm_bindgen::JsError; use wasm_bindgen::JsValue; diff --git a/rpc/wrpc/client/src/lib.rs b/rpc/wrpc/client/src/lib.rs index b3f26c425..ac004eccb 100644 --- a/rpc/wrpc/client/src/lib.rs +++ b/rpc/wrpc/client/src/lib.rs @@ -1,3 +1,19 @@ +//! +//! # wRPC Client for Rusty Kaspa p2p Node +//! +//! This crate provides a WebSocket RPC client for Rusty Kaspa p2p node. It is based on the +//! [wRPC](https://docs.rs/workflow-rpc) crate that offers WebSocket RPC implementation +//! for Rust based on Borsh and Serde JSON serialization. wRPC is a lightweight RPC framework +//! meant to function as an IPC (Inter-Process Communication) mechanism for Rust applications. +//! +//! Rust examples on using wRPC client can be found in the +//! [examples](https://github.com/kaspanet/rusty-kaspa/tree/master/rpc/wrpc/examples) folder. +//! +//! WASM bindings for wRPC client can be found in the [`kaspa-wrpc-wasm`](https://docs.rs/kaspa-wrpc-wasm) crate. +//! +//! The main struct managing Kaspa RPC client connections is the [`KaspaRpcClient`]. +//! + pub mod client; pub mod error; mod imports; diff --git a/rpc/wrpc/client/src/node.rs b/rpc/wrpc/client/src/node.rs index ca7e19c87..f775bfd18 100644 --- a/rpc/wrpc/client/src/node.rs +++ b/rpc/wrpc/client/src/node.rs @@ -1,3 +1,5 @@ +//! Node connection endpoint as provided by the [`Resolver`]. + use crate::imports::*; /// diff --git a/rpc/wrpc/client/src/parse.rs b/rpc/wrpc/client/src/parse.rs index 35db2c768..5a497c507 100644 --- a/rpc/wrpc/client/src/parse.rs +++ b/rpc/wrpc/client/src/parse.rs @@ -1,3 +1,5 @@ +//! wRPC URL parsing and validation utilities. + use std::fmt::Display; use std::net::{Ipv4Addr, Ipv6Addr}; use std::num::ParseIntError; diff --git a/rpc/wrpc/client/src/prelude.rs b/rpc/wrpc/client/src/prelude.rs index 6a410b723..a4598e537 100644 --- a/rpc/wrpc/client/src/prelude.rs +++ b/rpc/wrpc/client/src/prelude.rs @@ -1,3 +1,5 @@ +//! Re-exports of the most commonly used types and traits. + pub use crate::client::{ConnectOptions, ConnectStrategy}; pub use crate::{KaspaRpcClient, Resolver, WrpcEncoding}; pub use kaspa_consensus_core::network::{NetworkId, NetworkType}; diff --git a/rpc/wrpc/client/src/resolver.rs b/rpc/wrpc/client/src/resolver.rs index 8dcb19447..170fe12dd 100644 --- a/rpc/wrpc/client/src/resolver.rs +++ b/rpc/wrpc/client/src/resolver.rs @@ -1,3 +1,7 @@ +//! +//! Module implementing [`Resolver`] client for obtaining public Kaspa wRPC endpoints. +//! + use std::sync::OnceLock; use crate::error::Error; @@ -78,7 +82,12 @@ impl Inner { } /// -/// Resolver is a client for obtaining public Kaspa wRPC endpoints. +/// # Resolver - a client for obtaining public Kaspa wRPC endpoints. +/// +/// This client operates against [Kaspa Resolver](https://github.com/aspectron/kaspa-resolver) service +/// that provides load-balancing and failover capabilities for Kaspa wRPC endpoints. The default +/// configuration allows access to public Kaspa nodes, while custom configurations can be supplied +/// if you are running your own custom Kaspa node cluster. /// #[derive(Debug, Clone)] pub struct Resolver { @@ -92,10 +101,15 @@ impl Default for Resolver { } impl Resolver { + /// Create a new [`Resolver`] client with the specified list of resolver URLs and an optional `tls` flag. + /// The `tls` flag can be used to enforce secure connection to the node. pub fn new(urls: Option>>, tls: bool) -> Self { Self { inner: Arc::new(Inner::new(urls, tls)) } } + /// Obtain a list of URLs in the resolver client. (This function + /// returns `None` if the resolver is configured to use public + /// node endpoints.) pub fn urls(&self) -> Option>> { if self.inner.public { None @@ -104,11 +118,12 @@ impl Resolver { } } + /// Obtain the `tls` flag in the resolver client. pub fn tls(&self) -> bool { self.inner.tls } - pub fn tls_as_str(&self) -> &'static str { + fn tls_as_str(&self) -> &'static str { if self.inner.tls { "tls" } else { @@ -140,6 +155,7 @@ impl Resolver { format!("{url}/v{CURRENT_VERSION}/kaspa/{network_id}/{tls}/wrpc/{encoding}") } + // query a single resolver service async fn fetch_node_info(&self, url: &str, encoding: Encoding, network_id: NetworkId) -> Result { let url = self.make_url(url, encoding, network_id); let node = @@ -147,7 +163,8 @@ impl Resolver { Ok(node) } - pub async fn fetch(&self, encoding: Encoding, network_id: NetworkId) -> Result { + // query multiple resolver services in random order + async fn fetch(&self, encoding: Encoding, network_id: NetworkId) -> Result { let mut urls = self.inner.urls.clone(); urls.shuffle(&mut thread_rng()); @@ -161,10 +178,12 @@ impl Resolver { Err(Error::Custom(format!("Failed to connect: {:?}", errors))) } + /// Obtain a Kaspa p2p [`NodeDescriptor`] from the resolver based on the supplied [`Encoding`] and [`NetworkId`]. pub async fn get_node(&self, encoding: Encoding, network_id: NetworkId) -> Result { self.fetch(encoding, network_id).await } + /// Returns a Kaspa wRPC URL from the resolver based on the supplied [`Encoding`] and [`NetworkId`]. 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/result.rs b/rpc/wrpc/client/src/result.rs index 32f663388..8427fd12f 100644 --- a/rpc/wrpc/client/src/result.rs +++ b/rpc/wrpc/client/src/result.rs @@ -1 +1,3 @@ +//! The [`Result`] type alias bound to the [`Error`](super::error::Error) enum used in this crate. + pub type Result = std::result::Result; diff --git a/rpc/wrpc/server/src/connection.rs b/rpc/wrpc/server/src/connection.rs index 97a4e8adc..e118d161d 100644 --- a/rpc/wrpc/server/src/connection.rs +++ b/rpc/wrpc/server/src/connection.rs @@ -134,7 +134,7 @@ impl Connection { { match encoding { Encoding::Borsh => workflow_rpc::server::protocol::borsh::create_serialized_notification_message(op, msg), - Encoding::SerdeJson => workflow_rpc::server::protocol::borsh::create_serialized_notification_message(op, msg), + Encoding::SerdeJson => workflow_rpc::server::protocol::serde_json::create_serialized_notification_message(op, msg), } } } diff --git a/rpc/wrpc/wasm/src/client.rs b/rpc/wrpc/wasm/src/client.rs index 81487172f..ccd9cb284 100644 --- a/rpc/wrpc/wasm/src/client.rs +++ b/rpc/wrpc/wasm/src/client.rs @@ -1,3 +1,10 @@ +//! +//! # WASM bindings for the [Kaspa p2p Node RPC client](KaspaRpcClient). +//! +//! This module provides a WASM interface for the Kaspa p2p Node RPC client +//! - [`RpcClient`]. +//! + #![allow(non_snake_case)] use crate::imports::*; @@ -130,7 +137,7 @@ impl TryFrom for NotificationEvent { } } -pub struct Inner { +pub(crate) struct Inner { client: Arc, resolver: Option, notification_task: AtomicBool, @@ -969,6 +976,11 @@ build_wrpc_wasm_bindgen_interface!( /// Obtains basic information about the synchronization status of the Kaspa node. /// Returned information: Syncing status. GetSyncStatus, + /// Feerate estimates + GetFeeEstimate, + /// Retrieves the current network configuration. + /// Returned information: Current network configuration. + GetCurrentNetwork, ], [ // functions with `request` argument @@ -1003,13 +1015,8 @@ build_wrpc_wasm_bindgen_interface!( /// score timestamp estimate. /// Returned information: DAA score timestamp estimate. GetDaaScoreTimestampEstimate, - /// Feerate estimates - GetFeeEstimate, /// Feerate estimates (experimental) GetFeeEstimateExperimental, - /// 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, diff --git a/rpc/wrpc/wasm/src/lib.rs b/rpc/wrpc/wasm/src/lib.rs index e80b3baac..61d0de19b 100644 --- a/rpc/wrpc/wasm/src/lib.rs +++ b/rpc/wrpc/wasm/src/lib.rs @@ -1,3 +1,7 @@ +//! +//! WASM bindings for the [Rusty Kaspa p2p Node wRPC Client](kaspa-wrpc-client) +//! + #![allow(unused_imports)] use cfg_if::cfg_if; diff --git a/rpc/wrpc/wasm/src/notify.rs b/rpc/wrpc/wasm/src/notify.rs index 23781e314..c586f24dc 100644 --- a/rpc/wrpc/wasm/src/notify.rs +++ b/rpc/wrpc/wasm/src/notify.rs @@ -1,3 +1,7 @@ +//! Notification types and interfaces for wRPC events. + +#![allow(non_snake_case)] + use crate::imports::*; use kaspa_rpc_macros::declare_typescript_wasm_interface as declare; diff --git a/rpc/wrpc/wasm/src/resolver.rs b/rpc/wrpc/wasm/src/resolver.rs index 2ffc7ea56..7abfdb688 100644 --- a/rpc/wrpc/wasm/src/resolver.rs +++ b/rpc/wrpc/wasm/src/resolver.rs @@ -1,3 +1,7 @@ +//! [`Resolver`](NativeResolver) bindings for obtaining public Kaspa wRPC URL endpoints. + +#![allow(non_snake_case)] + use crate::client::{RpcClient, RpcConfig}; use crate::imports::*; use js_sys::Array; diff --git a/simpa/src/main.rs b/simpa/src/main.rs index 1d14a3c68..c66656be3 100644 --- a/simpa/src/main.rs +++ b/simpa/src/main.rs @@ -83,7 +83,7 @@ struct Args { ram_scale: f64, /// Logging level for all subsystems {off, error, warn, info, debug, trace} - /// -- You may also specify =,=,... to set the log level for individual subsystems + /// -- You may also specify `=,=,...` to set the log level for individual subsystems #[arg(long = "loglevel", default_value = format!("info,{}=trace", env!("CARGO_PKG_NAME")))] log_level: String, diff --git a/testing/integration/src/daemon_integration_tests.rs b/testing/integration/src/daemon_integration_tests.rs index a92320294..460cf049c 100644 --- a/testing/integration/src/daemon_integration_tests.rs +++ b/testing/integration/src/daemon_integration_tests.rs @@ -106,7 +106,13 @@ async fn daemon_mining_test() { assert_eq!(dag_info.sink, last_block_hash.unwrap()); // Check that acceptance data contains the expected coinbase tx ids - let vc = rpc_client2.get_virtual_chain_from_block(kaspa_consensus::params::SIMNET_GENESIS.hash, true).await.unwrap(); + let vc = rpc_client2 + .get_virtual_chain_from_block( + kaspa_consensus::params::SIMNET_GENESIS.hash, // + true, + ) + .await + .unwrap(); assert_eq!(vc.removed_chain_block_hashes.len(), 0); assert_eq!(vc.added_chain_block_hashes.len(), 10); assert_eq!(vc.accepted_transaction_ids.len(), 10); diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 4e57548e7..3d1bb5438 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,3 +1,9 @@ +//! +//! # Kaspa Utilities +//! +//! General purpose utilities and various type extensions used across the Rusty Kaspa codebase. +//! + pub mod any; pub mod arc; pub mod binary_heap; diff --git a/utils/src/option.rs b/utils/src/option.rs index ff4779dc1..3e619f46f 100644 --- a/utils/src/option.rs +++ b/utils/src/option.rs @@ -1,5 +1,5 @@ pub trait OptionExtensions { - /// Substitute for unstable [Option::is_non_or] + /// Substitute for unstable [`Option::is_none_or`] fn is_none_or_ex(&self, f: impl FnOnce(&T) -> bool) -> bool; } diff --git a/utils/tower/Cargo.toml b/utils/tower/Cargo.toml index 2a2f5f796..010f8843b 100644 --- a/utils/tower/Cargo.toml +++ b/utils/tower/Cargo.toml @@ -14,9 +14,11 @@ cfg-if.workspace = true log.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +bytes.workspace = true futures.workspace = true -hyper.workspace = true +http-body.workspace = true +http-body-util.workspace = true pin-project-lite.workspace = true tokio.workspace = true tower-http.workspace = true -tower.workspace = true \ No newline at end of file +tower.workspace = true diff --git a/utils/tower/src/middleware.rs b/utils/tower/src/middleware.rs index 727d8ca47..8d0fa77c3 100644 --- a/utils/tower/src/middleware.rs +++ b/utils/tower/src/middleware.rs @@ -1,9 +1,6 @@ -use futures::ready; -use hyper::{ - body::{Bytes, HttpBody, SizeHint}, - HeaderMap, -}; -use log::*; +use bytes::Bytes; +use http_body::{Body, Frame, SizeHint}; +use log::trace; use pin_project_lite::pin_project; use std::{ pin::Pin, @@ -11,11 +8,12 @@ use std::{ atomic::{AtomicUsize, Ordering}, Arc, }, - task::{Context, Poll}, + task::{ready, Context, Poll}, }; + +pub use http_body_util::BodyExt; pub use tower::ServiceBuilder; -pub use tower_http::map_request_body::MapRequestBodyLayer; -pub use tower_http::map_response_body::MapResponseBodyLayer; +pub use tower_http::{map_request_body::MapRequestBodyLayer, map_response_body::MapResponseBodyLayer}; pin_project! { pub struct CountBytesBody { @@ -31,32 +29,29 @@ impl CountBytesBody { } } -impl HttpBody for CountBytesBody +impl Body for CountBytesBody where - B: HttpBody + Default, + B: Body + Default, { type Data = B::Data; type Error = B::Error; - fn poll_data(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>> { + fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, Self::Error>>> { let this = self.project(); - let counter: Arc = this.counter.clone(); - match ready!(this.inner.poll_data(cx)) { - Some(Ok(chunk)) => { - debug!("[SIZE MW] response body chunk size = {}", chunk.len()); - let _previous = counter.fetch_add(chunk.len(), Ordering::Relaxed); - debug!("[SIZE MW] total count: {}", _previous); + match ready!(this.inner.poll_frame(cx)) { + Some(Ok(frame)) => { + if let Some(chunk) = frame.data_ref() { + trace!("[SIZE MW] body chunk size = {}", chunk.len()); + let _previous = this.counter.fetch_add(chunk.len(), Ordering::Relaxed); + trace!("[SIZE MW] total count: {}", _previous); + } - Poll::Ready(Some(Ok(chunk))) + Poll::Ready(Some(Ok(frame))) } x => Poll::Ready(x), } } - fn poll_trailers(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, Self::Error>> { - self.project().inner.poll_trailers(cx) - } - fn is_end_stream(&self) -> bool { self.inner.is_end_stream() } @@ -68,43 +63,9 @@ where impl Default for CountBytesBody where - B: HttpBody + Default, + B: Body + Default, { fn default() -> Self { Self { inner: Default::default(), counter: Default::default() } } } - -pub fn measure_request_body_size_layer( - bytes_sent_counter: Arc, - f: F, -) -> MapRequestBodyLayer B2 + Clone> -where - B1: HttpBody + Unpin + Send + 'static, - ::Error: Send, - F: Fn(hyper::body::Body) -> B2 + Clone, -{ - MapRequestBodyLayer::new(move |mut body: B1| { - let (mut tx, new_body) = hyper::Body::channel(); - let bytes_sent_counter = bytes_sent_counter.clone(); - tokio::spawn(async move { - while let Some(Ok(chunk)) = body.data().await { - debug!("[SIZE MW] request body chunk size = {}", chunk.len()); - let _previous = bytes_sent_counter.fetch_add(chunk.len(), Ordering::Relaxed); - debug!("[SIZE MW] total count: {}", _previous); - if let Err(_err) = tx.send_data(chunk).await { - // error can occurs only if the channel is already closed - debug!("[SIZE MW] error sending data: {}", _err) - } - } - - if let Ok(Some(trailers)) = body.trailers().await { - if let Err(_err) = tx.send_trailers(trailers).await { - // error can occurs only if the channel is already closed - debug!("[SIZE MW] error sending trailers: {}", _err) - } - } - }); - f(new_body) - }) -} diff --git a/wallet/bip32/src/address_type.rs b/wallet/bip32/src/address_type.rs index 63ea00361..3aecfdb75 100644 --- a/wallet/bip32/src/address_type.rs +++ b/wallet/bip32/src/address_type.rs @@ -1,5 +1,10 @@ +//! +//! Address type (`Receive` or `Change`) used in HD wallet address derivation. +//! + use std::fmt; +/// Address type used in HD wallet address derivation. pub enum AddressType { Receive = 0, Change, diff --git a/wallet/bip32/src/lib.rs b/wallet/bip32/src/lib.rs index a406067f6..1926728c4 100644 --- a/wallet/bip32/src/lib.rs +++ b/wallet/bip32/src/lib.rs @@ -32,6 +32,8 @@ pub use xkey::ExtendedKey; pub use xprivate_key::ExtendedPrivateKey; pub use xpublic_key::ExtendedPublicKey; +/// Extension for [`secp256k1::SecretKey`] that provides access +/// to [`secp256k1::PublicKey`] and the public key string representation. pub trait SecretKeyExt { fn get_public_key(&self) -> secp256k1::PublicKey; fn as_str(&self, attrs: ExtendedKeyAttrs, prefix: Prefix) -> Zeroizing; diff --git a/wallet/bip32/src/mnemonic/mod.rs b/wallet/bip32/src/mnemonic/mod.rs index 611d88b05..89262b7e2 100644 --- a/wallet/bip32/src/mnemonic/mod.rs +++ b/wallet/bip32/src/mnemonic/mod.rs @@ -7,7 +7,6 @@ mod bits; mod language; mod phrase; -//#[cfg(feature = "bip39")] pub(crate) mod seed; pub use self::{language::Language, phrase::Mnemonic, phrase::WordCount}; diff --git a/wallet/bip32/src/mnemonic/phrase.rs b/wallet/bip32/src/mnemonic/phrase.rs index 95fd92189..eaa7e7096 100644 --- a/wallet/bip32/src/mnemonic/phrase.rs +++ b/wallet/bip32/src/mnemonic/phrase.rs @@ -229,8 +229,6 @@ impl Mnemonic { } /// Convert this mnemonic phrase into the BIP39 seed value. - //#[cfg(feature = "bip39")] - //#[cfg_attr(docsrs, doc(cfg(feature = "bip39")))] pub fn to_seed(&self, password: &str) -> Seed { let salt = Zeroizing::new(format!("mnemonic{password}")); let mut seed = [0u8; Seed::SIZE]; diff --git a/wallet/bip32/src/mnemonic/seed.rs b/wallet/bip32/src/mnemonic/seed.rs index a4c4025c6..7fd57f19a 100644 --- a/wallet/bip32/src/mnemonic/seed.rs +++ b/wallet/bip32/src/mnemonic/seed.rs @@ -4,7 +4,6 @@ use zeroize::Zeroize; /// BIP39 seeds. // TODO(tarcieri): support for 32-byte seeds -#[cfg_attr(docsrs, doc(cfg(feature = "bip39")))] pub struct Seed(pub(crate) [u8; Seed::SIZE]); impl Seed { diff --git a/wallet/bip32/src/private_key.rs b/wallet/bip32/src/private_key.rs index 0d4769ee4..d5dbc3d14 100644 --- a/wallet/bip32/src/private_key.rs +++ b/wallet/bip32/src/private_key.rs @@ -4,6 +4,7 @@ use crate::Result; pub use secp256k1::SecretKey; use secp256k1::{scalar::Scalar, Secp256k1, SignOnly}; +/// Trait for private key types which can be derived using BIP32. pub trait PrivateKey: Sized { /// Public key type which corresponds to this private key. type PublicKey: PublicKey; diff --git a/wallet/bip32/src/public_key.rs b/wallet/bip32/src/public_key.rs index 28a121811..56fb17de5 100644 --- a/wallet/bip32/src/public_key.rs +++ b/wallet/bip32/src/public_key.rs @@ -3,7 +3,7 @@ use ripemd::{Digest, Ripemd160}; use secp256k1::{scalar::Scalar, Secp256k1, VerifyOnly}; use sha2::Sha256; -/// Trait for key types which can be derived using BIP32. +/// Trait for public key types which can be derived using BIP32. pub trait PublicKey: Sized { /// Initialize this key from bytes. fn from_bytes(bytes: PublicKeyBytes) -> Result; diff --git a/wallet/core/src/account/descriptor.rs b/wallet/core/src/account/descriptor.rs index c549b739d..c3bf97cc1 100644 --- a/wallet/core/src/account/descriptor.rs +++ b/wallet/core/src/account/descriptor.rs @@ -11,6 +11,11 @@ use kaspa_wallet_macros::declare_typescript_wasm_interface as declare; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +/// +/// Structure that represents a wallet account. This structure contains +/// properties that are common to all wallet accounts as well as +/// account-specific properties stored in a BTreeMap by each account. +/// /// @category Wallet API #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct AccountDescriptor { diff --git a/wallet/core/src/account/kind.rs b/wallet/core/src/account/kind.rs index 20e863d77..511c1f4ed 100644 --- a/wallet/core/src/account/kind.rs +++ b/wallet/core/src/account/kind.rs @@ -8,6 +8,11 @@ use std::hash::Hash; use std::str::FromStr; use workflow_wasm::convert::CastFromJs; +/// +/// Account kind is a string signature that represents an account type. +/// Account kind is used to identify the account type during +/// serialization, deserialization and various API calls. +/// /// @category Wallet SDK #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash, CastFromJs)] #[wasm_bindgen] diff --git a/wallet/core/src/account/pskb.rs b/wallet/core/src/account/pskb.rs index 8fc46088b..e71d7e479 100644 --- a/wallet/core/src/account/pskb.rs +++ b/wallet/core/src/account/pskb.rs @@ -1,3 +1,8 @@ +//! +//! Tools for interfacing wallet accounts with PSKBs. +//! (Partial Signed Kaspa Transaction Bundles). +//! + pub use crate::error::Error; use crate::imports::*; use crate::tx::PaymentOutputs; diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs index 3b96abd1a..e27cb2b29 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -118,15 +118,18 @@ pub struct RetainContextRequest { #[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, +pub struct RetainContextResponse {} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetContextRequest { + pub name: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetContextResponse { + pub data: Option>, } #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] diff --git a/wallet/core/src/api/mod.rs b/wallet/core/src/api/mod.rs index f0963d610..979ef1c72 100644 --- a/wallet/core/src/api/mod.rs +++ b/wallet/core/src/api/mod.rs @@ -1,4 +1,6 @@ //! +//! # Wallet API +//! //! Wallet API module that provides a unified interface for all wallet operations. //! diff --git a/wallet/core/src/api/traits.rs b/wallet/core/src/api/traits.rs index 08ebd65f6..357665e77 100644 --- a/wallet/core/src/api/traits.rs +++ b/wallet/core/src/api/traits.rs @@ -21,13 +21,29 @@ pub trait WalletApi: Send + Sync + AnySync { async fn register_notifications(self: Arc, channel: Receiver) -> Result; async fn unregister_notifications(self: Arc, channel_id: u64) -> Result<()>; + /// Wrapper around [`retain_context_call()`](Self::retain_context_call). async fn retain_context(self: Arc, name: &str, data: Option>) -> Result<()> { self.retain_context_call(RetainContextRequest { name: name.to_string(), data }).await?; Ok(()) } + /// Obtain earlier retained context data using the context `name` as a key. + async fn get_context(self: Arc, name: &str) -> Result>> { + Ok(self.get_context_call(GetContextRequest { name: name.to_string() }).await?.data) + } + + /// Allows user to store string key-associated context data in the wallet subsystem runtime. + /// The context data persists only during the wallet instance runtime. + /// This can be useful if you have a front-end that connects to a + /// persistent wallet instance operating in the backend (such as a browser + /// extension popup connecting to the background page) and you need to store + /// any type of runtime data in the backend (but are limited to using only + /// the wallet interface). async fn retain_context_call(self: Arc, request: RetainContextRequest) -> Result; + /// Obtain context data stored using [`retain_context()`](Self::retain_context). + async fn get_context_call(self: Arc, request: GetContextRequest) -> Result; + /// Wrapper around [`get_status_call()`](Self::get_status_call). async fn get_status(self: Arc, name: Option<&str>) -> Result { Ok(self.get_status_call(GetStatusRequest { name: name.map(String::from) }).await?) @@ -42,7 +58,7 @@ 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; - /// Synchronous connect call (blocking, single attempt, requires sync). + /// Synchronous connect call (blocking, single attempt, requires node sync). async fn connect(self: Arc, url: Option, network_id: &NetworkId) -> Result<()> { let retry_on_error = false; let block_async_connect = true; @@ -55,6 +71,7 @@ pub trait WalletApi: Send + Sync + AnySync { /// comprised of the `url` and a `network_id`. async fn connect_call(self: Arc, request: ConnectRequest) -> Result; + /// Request the wallet RPC subsystem to disconnect from the node. async fn disconnect(self: Arc) -> Result<()> { self.disconnect_call(DisconnectRequest {}).await?; Ok(()) @@ -80,6 +97,7 @@ pub trait WalletApi: Send + Sync + AnySync { /// Ping the wallet service. Accepts an optional `u64` value that is returned in the response. async fn ping_call(self: Arc, request: PingRequest) -> Result; + /// Wrapper around [`batch_call()`](Self::batch_call). async fn batch(self: Arc) -> Result<()> { self.batch_call(BatchRequest {}).await?; Ok(()) @@ -94,6 +112,7 @@ pub trait WalletApi: Send + Sync + AnySync { /// async fn batch_call(self: Arc, request: BatchRequest) -> Result; + /// Wrapper around [`flush_call()`](Self::flush_call). async fn flush(self: Arc, wallet_secret: Secret) -> Result<()> { self.flush_call(FlushRequest { wallet_secret }).await?; Ok(()) @@ -268,6 +287,7 @@ pub trait WalletApi: Send + Sync + AnySync { /// around this call. async fn accounts_rename_call(self: Arc, request: AccountsRenameRequest) -> Result; + /// Wrapper around [`accounts_select_call()`](Self::accounts_select_call) async fn accounts_select(self: Arc, account_id: Option) -> Result<()> { self.accounts_select_call(AccountsSelectRequest { account_id }).await?; Ok(()) @@ -404,6 +424,7 @@ pub trait WalletApi: Send + Sync + AnySync { async fn accounts_estimate_call(self: Arc, request: AccountsEstimateRequest) -> Result; /// Get a range of transaction records for a specific account id. + /// Wrapper around [`transactions_data_get_call()`](Self::transactions_data_get_call). async fn transactions_data_get_range( self: Arc, account_id: AccountId, @@ -413,8 +434,8 @@ pub trait WalletApi: Send + Sync + AnySync { self.transactions_data_get_call(TransactionsDataGetRequest::with_range(account_id, network_id, range)).await } + /// Get a range of transaction records for a specific account id. async fn transactions_data_get_call(self: Arc, request: TransactionsDataGetRequest) -> Result; - // async fn transaction_get_call(self: Arc, request: TransactionGetRequest) -> Result; /// Replaces the note of a transaction with a new note. Note is meant /// to explicitly store a user-supplied string. The note is treated @@ -439,6 +460,7 @@ pub trait WalletApi: Send + Sync + AnySync { request: TransactionsReplaceMetadataRequest, ) -> Result; + // TODO async fn address_book_enumerate_call( self: Arc, request: AddressBookEnumerateRequest, diff --git a/wallet/core/src/api/transport.rs b/wallet/core/src/api/transport.rs index 4de2d7824..c9e5f6de6 100644 --- a/wallet/core/src/api/transport.rs +++ b/wallet/core/src/api/transport.rs @@ -71,6 +71,7 @@ impl WalletApi for WalletClient { Disconnect, ChangeNetworkId, RetainContext, + GetContext, Batch, Flush, WalletEnumerate, @@ -118,7 +119,7 @@ pub trait EventHandler: Send + Sync { /// [`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`] +/// serializations created using the [`Codec`] interface. The [`WalletServer`] /// is a counter-part to [`WalletClient`]. pub struct WalletServer { // pub wallet_api: Arc, @@ -147,6 +148,7 @@ impl WalletServer { Disconnect, ChangeNetworkId, RetainContext, + GetContext, Batch, Flush, WalletEnumerate, diff --git a/wallet/core/src/compat/mod.rs b/wallet/core/src/compat/mod.rs index 79c8e11dd..093b8845c 100644 --- a/wallet/core/src/compat/mod.rs +++ b/wallet/core/src/compat/mod.rs @@ -1,3 +1,7 @@ +//! +//! Compatibility layer for legacy wallets. +//! + pub mod gen0; pub use gen0::*; pub mod gen1; diff --git a/wallet/core/src/cryptobox.rs b/wallet/core/src/cryptobox.rs index fa9b188f3..83845eee9 100644 --- a/wallet/core/src/cryptobox.rs +++ b/wallet/core/src/cryptobox.rs @@ -1,3 +1,7 @@ +//! +//! Re-export of the `crypto_box` crate that can be used to encrypt and decrypt messages. +//! + use crate::imports::*; use crypto_box::{ aead::{Aead, AeadCore, OsRng}, @@ -5,8 +9,13 @@ use crypto_box::{ }; pub use crypto_box::{PublicKey, SecretKey}; -// https://docs.rs/crypto_box/0.9.1/crypto_box/ - +/// +/// Primitives for encrypting and decrypting messages using the `crypto_box` crate. +/// This exists primarily for the purposes of [WASM bindings](crate::wasm::cryptobox::CryptoBox) +/// to allow access to the `crypto_box` encryption functionality from within web wallets. +/// +/// +/// pub struct CryptoBox { public_key: PublicKey, codec: ChaChaBox, diff --git a/wallet/core/src/events.rs b/wallet/core/src/events.rs index 63d7d5bca..37816d8b2 100644 --- a/wallet/core/src/events.rs +++ b/wallet/core/src/events.rs @@ -245,6 +245,12 @@ impl Events { } } +/// +/// Event kind representing [`Events`] variant. +/// Used primarily by WASM bindings to identify event types +/// by their string representation. Can be obtained from the +/// event via [`Events::kind()`]. +/// #[derive(Clone, Copy, Debug, Serialize, Eq, PartialEq, Hash)] #[serde(rename_all = "kebab-case")] pub enum EventKind { diff --git a/wallet/core/src/factory.rs b/wallet/core/src/factory.rs index 515093fdc..178e331e5 100644 --- a/wallet/core/src/factory.rs +++ b/wallet/core/src/factory.rs @@ -6,6 +6,7 @@ use crate::imports::*; use crate::result::Result; use std::sync::OnceLock; +/// Wallet account loading factory. #[async_trait] pub trait Factory { fn name(&self) -> String; @@ -22,6 +23,7 @@ type FactoryMap = AHashMap static EXTERNAL: OnceLock> = OnceLock::new(); static INITIALIZED: AtomicBool = AtomicBool::new(false); +/// Global factory registry accessor. pub fn factories() -> &'static FactoryMap { static FACTORIES: OnceLock = OnceLock::new(); FACTORIES.get_or_init(|| { @@ -41,6 +43,7 @@ pub fn factories() -> &'static FactoryMap { }) } +/// Registers a new global account factory. pub fn register(kind: AccountKind, factory: Arc) { if INITIALIZED.load(Ordering::Relaxed) { panic!("Factory registrations must occur before the framework initialization"); diff --git a/wallet/core/src/lib.rs b/wallet/core/src/lib.rs index bca89d4e7..09cc3ca7f 100644 --- a/wallet/core/src/lib.rs +++ b/wallet/core/src/lib.rs @@ -1,16 +1,21 @@ //! -//! Kaspa Wallet Core - Multi-platform Rust framework for Kaspa Wallet. +//! # Kaspa Wallet Core +//! +//! Multi-platform Rust framework for Kaspa Wallet. //! //! This framework provides a series of APIs and primitives //! to simplify building applications that interface with //! the Kaspa p2p network. //! -//! Included are low-level primitives +//! For key generation and derivation, please see the +//! [`kaspa_wallet_keys`] crate. +//! +//! This crate included are low-level primitives //! such as [`UtxoProcessor`](crate::utxo::UtxoProcessor) //! and [`UtxoContext`](crate::utxo::UtxoContext) that provide //! various levels of automation as well as higher-level //! APIs such as [`Wallet`](crate::wallet::Wallet), -//! [`Account`](crate::account::Account) (managed via +//! [`Account`](crate::account::Account) (managed via the //! [`WalletApi`](crate::api::WalletApi) trait) //! that offer a fully-featured wallet implementation //! backed by a multi-platform data storage layer capable of @@ -28,15 +33,28 @@ //! to satisfy the requested amount exceeds the maximum //! allowed transaction mass. //! +//! Key generation and derivation is available in the +//! [`kaspa_wallet_keys`] crate. +//! //! The framework can operate -//! within native Rust applications as well as within the NodeJS -//! and browser environments via WASM32. +//! within native Rust applications as well as within NodeJS, Bun +//! and browser environments via the WASM32 SDK. //! -//! For JavaScript / TypeScript environments, there are two +//! WASM32 SDK documentation is available at: +//! +//! +//! For NodeJS JavaScript and TypeScript environments, there are two //! available NPM modules: //! - //! - //! +//! NOTE: for security reasons (to mitigate potential upstream vendor +//! attacks) it is always recommended to build WASM SDK from source or +//! download pre-built redistributables. +//! +//! Latest development builds of the WASM32 SDK can be found at: +//! +//! //! 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 @@ -54,36 +72,6 @@ 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; @@ -107,9 +95,6 @@ pub mod tx; pub mod utils; pub mod utxo; pub mod wallet; -// } - -// } #[cfg(any(feature = "wasm32-sdk", feature = "wasm32-core"))] pub mod wasm; diff --git a/wallet/core/src/message.rs b/wallet/core/src/message.rs index 160c8f040..01dc78676 100644 --- a/wallet/core/src/message.rs +++ b/wallet/core/src/message.rs @@ -5,6 +5,7 @@ use kaspa_hashes::{Hash, PersonalMessageSigningHash}; use secp256k1::{Error, XOnlyPublicKey}; +/// A personal message (text) that can be signed. #[derive(Clone)] pub struct PersonalMessage<'a>(pub &'a str); diff --git a/wallet/core/src/metrics.rs b/wallet/core/src/metrics.rs index 87a3f9913..b0edb1f88 100644 --- a/wallet/core/src/metrics.rs +++ b/wallet/core/src/metrics.rs @@ -1,6 +1,13 @@ +//! +//! Primitives for network metrics. +//! + use crate::imports::*; -// use kaspa_metrics_core::MetricsSnapshot; +/// Metrics posted by the wallet subsystem. +/// See [`UtxoProcessor::start_metrics`] to enable metrics processing. +/// This struct contains mempool size that can be used to estimate +/// current network congestion. #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(tag = "type", content = "data")] #[serde(rename_all = "kebab-case")] @@ -8,37 +15,19 @@ 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 - // } } +/// [`MetricsUpdate`] variant identifier. #[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 0ca019434..741ea0b1e 100644 --- a/wallet/core/src/prelude.rs +++ b/wallet/core/src/prelude.rs @@ -1,6 +1,5 @@ //! -//! A module which is typically glob imported. -//! Contains most commonly used imports. +//! Re-exports of the most commonly used types and traits in this crate. //! pub use crate::account::descriptor::AccountDescriptor; diff --git a/wallet/core/src/rpc.rs b/wallet/core/src/rpc.rs index 999e09e30..b75f4b5b2 100644 --- a/wallet/core/src/rpc.rs +++ b/wallet/core/src/rpc.rs @@ -6,13 +6,17 @@ use std::sync::Arc; pub use kaspa_rpc_core::api::ctl::RpcCtl; pub use kaspa_rpc_core::api::rpc::RpcApi; -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; +/// Type alias for [`dyn RpcApi`](RpcApi). +pub type DynRpcApi = dyn RpcApi; +/// Type alias for a concrete [`Channel`](kaspa_utils::channel::Channel) +/// used for handling RPC [`Notification`](kaspa_rpc_core::Notification) events. +pub type NotificationChannel = kaspa_utils::channel::Channel; + /// RPC adaptor class that holds the [`RpcApi`] /// and [`RpcCtl`] instances. #[derive(Clone)] diff --git a/wallet/core/src/settings.rs b/wallet/core/src/settings.rs index 35fde4486..f861c5a13 100644 --- a/wallet/core/src/settings.rs +++ b/wallet/core/src/settings.rs @@ -13,6 +13,7 @@ use std::path::PathBuf; use workflow_core::enums::Describe; use workflow_store::fs; +/// Wallet settings enumeration. #[derive(Describe, Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] #[serde(rename_all = "lowercase")] pub enum WalletSettings { @@ -36,6 +37,8 @@ pub trait DefaultSettings: Sized { async fn defaults() -> Vec<(Self, Value)>; } +/// Platform neutral settings store (stores the settings K:V map +/// in a file or the browser `localStorage`). #[derive(Debug, Clone)] pub struct SettingsStore where @@ -170,10 +173,12 @@ where } } +/// Returns the wallet data storage folder `~/.kaspa`. pub fn application_folder() -> Result { Ok(fs::resolve_path(storage::local::default_storage_folder())?) } +/// If missing, creates the wallet data storage folder `~/.kaspa`. pub async fn ensure_application_folder() -> Result<()> { let path = application_folder()?; log_info!("Creating application folder: `{}`", path.display()); diff --git a/wallet/core/src/tx/payment.rs b/wallet/core/src/tx/payment.rs index e28c75a22..c164e0d78 100644 --- a/wallet/core/src/tx/payment.rs +++ b/wallet/core/src/tx/payment.rs @@ -30,12 +30,19 @@ export interface IPaymentOutput { #[wasm_bindgen] extern "C" { + /// WASM (TypeScript) type representing a single payment output (`IPaymentOutput`). + /// @category Wallet SDK #[wasm_bindgen(typescript_type = "IPaymentOutput")] pub type IPaymentOutput; + /// WASM (TypeScript) type representing multiple payment outputs (`IPaymentOutput[]`). + /// @category Wallet SDK #[wasm_bindgen(typescript_type = "IPaymentOutput[]")] pub type IPaymentOutputArray; } +/// A Rust data structure representing a payment destination. +/// A payment destination is used to signal Generator where to send the funds. +/// The destination can be a change address or a set of [`PaymentOutput`]. #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub enum PaymentDestination { Change, @@ -51,6 +58,9 @@ impl PaymentDestination { } } +/// A Rust data structure representing a single payment +/// output containing a destination address and amount. +/// /// @category Wallet SDK #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)] #[wasm_bindgen(inspectable)] diff --git a/wallet/core/src/utxo/processor.rs b/wallet/core/src/utxo/processor.rs index b72b9784a..f6480f333 100644 --- a/wallet/core/src/utxo/processor.rs +++ b/wallet/core/src/utxo/processor.rs @@ -604,9 +604,7 @@ impl UtxoProcessor { 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 }; + let metrics = MetricsUpdate::WalletMetrics { mempool_size }; self.try_notify(Events::Metrics { network_id: self.network_id()?, metrics })?; } } diff --git a/wallet/core/src/wallet/api.rs b/wallet/core/src/wallet/api.rs index adeb00075..93becef42 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -1,5 +1,5 @@ //! -//! [`WalletApi`] trait implementation for [`Wallet`]. +//! [`WalletApi`] trait implementation for the [`Wallet`] struct. //! use crate::api::{message::*, traits::WalletApi}; @@ -62,15 +62,17 @@ impl WalletApi for super::Wallet { 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); + async fn get_context_call(self: Arc, request: GetContextRequest) -> Result { + let GetContextRequest { name } = request; + let data = self.inner.retained_contexts.lock().unwrap().get(&name).map(|data| (**data).clone()); + Ok(GetContextResponse { data }) } // ------------------------------------------------------------------------------------- diff --git a/wallet/core/src/wallet/args.rs b/wallet/core/src/wallet/args.rs index a5fa378bb..f0168f740 100644 --- a/wallet/core/src/wallet/args.rs +++ b/wallet/core/src/wallet/args.rs @@ -3,7 +3,6 @@ //! use crate::imports::*; -// use crate::secret::Secret; use crate::storage::interface::CreateArgs; use crate::storage::{Hint, PrvKeyDataId}; use borsh::{BorshDeserialize, BorshSerialize}; diff --git a/wallet/core/src/wallet/maps.rs b/wallet/core/src/wallet/maps.rs index 430f54e56..232e6fbeb 100644 --- a/wallet/core/src/wallet/maps.rs +++ b/wallet/core/src/wallet/maps.rs @@ -5,6 +5,7 @@ use crate::imports::*; +/// A thread-safe map of [`AccountId`] to [`Account`] instances. #[derive(Default, Clone)] pub struct ActiveAccountMap(Arc>>>); diff --git a/wallet/core/src/wallet/mod.rs b/wallet/core/src/wallet/mod.rs index d5f4dfadd..d7c9b6c76 100644 --- a/wallet/core/src/wallet/mod.rs +++ b/wallet/core/src/wallet/mod.rs @@ -1,6 +1,14 @@ //! -//! Kaspa wallet runtime implementation. +//! # Kaspa wallet runtime implementation. //! +//! This module contains a Rust implementation of the Kaspa wallet that +//! can be used in native Rust as well as WASM32 (Browser, NodeJs, Bun) +//! environments. +//! +//! This wallet is not meant to be used directly, but rather through the +//! use of the [`WalletApi`] trait. +//! + pub mod api; pub mod args; pub mod maps; @@ -81,7 +89,8 @@ pub enum WalletBusMessage { Discovery { record: TransactionRecord }, } -pub struct Inner { +/// Internal wallet state. +struct Inner { active_accounts: ActiveAccountMap, legacy_accounts: ActiveAccountMap, listener_id: Mutex>, @@ -189,10 +198,6 @@ impl Wallet { self } - pub fn inner(&self) -> &Arc { - &self.inner - } - // // Mutex used to protect concurrent access to accounts // at the wallet api level. This is a global lock that diff --git a/wallet/core/src/wasm/cryptobox.rs b/wallet/core/src/wasm/cryptobox.rs index 118020fe4..957d4fc35 100644 --- a/wallet/core/src/wasm/cryptobox.rs +++ b/wallet/core/src/wasm/cryptobox.rs @@ -106,7 +106,7 @@ impl std::ops::Deref for CryptoBoxPublicKey { /// /// 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 /// diff --git a/wallet/core/src/wasm/signer.rs b/wallet/core/src/wasm/signer.rs index e00729ef5..157f06d90 100644 --- a/wallet/core/src/wasm/signer.rs +++ b/wallet/core/src/wasm/signer.rs @@ -50,7 +50,7 @@ pub fn js_sign_transaction(tx: &Transaction, signer: &PrivateKeyArrayT, verify_s } } -pub fn sign_transaction<'a>(tx: &'a Transaction, private_keys: &[[u8; 32]], verify_sig: bool) -> Result<&'a Transaction> { +fn sign_transaction<'a>(tx: &'a Transaction, private_keys: &[[u8; 32]], verify_sig: bool) -> Result<&'a Transaction> { let tx = sign(tx, private_keys)?; if verify_sig { let (cctx, utxos) = tx.tx_and_utxos()?; @@ -97,7 +97,7 @@ pub fn sign_script_hash(script_hash: JsValue, privkey: &PrivateKey) -> Result Result> { +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(); diff --git a/wallet/keys/src/derivation/gen0/mod.rs b/wallet/keys/src/derivation/gen0/mod.rs index d25032626..0de5da286 100644 --- a/wallet/keys/src/derivation/gen0/mod.rs +++ b/wallet/keys/src/derivation/gen0/mod.rs @@ -1,4 +1,4 @@ -//! Derivation management for legacy account derivation scheme `972` +//! Derivation management for legacy account derivation scheme based on `'972` derivation path (deprecated). mod hd; pub use hd::{PubkeyDerivationManagerV0, WalletDerivationManagerV0}; diff --git a/wallet/keys/src/derivation/gen1/mod.rs b/wallet/keys/src/derivation/gen1/mod.rs index 1822c4d7a..5ec859b59 100644 --- a/wallet/keys/src/derivation/gen1/mod.rs +++ b/wallet/keys/src/derivation/gen1/mod.rs @@ -1,3 +1,4 @@ -/// Derivation management for the Kaspa standard derivation scheme `111111'` +//! Derivation management for the Kaspa standard derivation scheme `'111111'` + mod hd; pub use hd::{PubkeyDerivationManager, WalletDerivationManager}; diff --git a/wallet/keys/src/derivation/mod.rs b/wallet/keys/src/derivation/mod.rs index a63201194..cfa80b4f7 100644 --- a/wallet/keys/src/derivation/mod.rs +++ b/wallet/keys/src/derivation/mod.rs @@ -1,3 +1,7 @@ +//! +//! Derivation utilities used by the integrated Kaspa Wallet API. +//! + pub mod gen0; pub mod gen1; pub mod traits; diff --git a/wallet/keys/src/derivation_path.rs b/wallet/keys/src/derivation_path.rs index df220ee44..a5389ca37 100644 --- a/wallet/keys/src/derivation_path.rs +++ b/wallet/keys/src/derivation_path.rs @@ -1,7 +1,13 @@ +//! +//! Implementation of the [`DerivationPath`] manager for arbitrary derivation paths. +//! + use crate::imports::*; use workflow_wasm::prelude::*; +/// /// Key derivation path +/// /// @category Wallet SDK #[derive(Clone, CastFromJs)] #[wasm_bindgen] diff --git a/wallet/keys/src/keypair.rs b/wallet/keys/src/keypair.rs index f4b39f3d3..2cc3d5760 100644 --- a/wallet/keys/src/keypair.rs +++ b/wallet/keys/src/keypair.rs @@ -2,6 +2,8 @@ //! [`keypair`](mod@self) module encapsulates [`Keypair`] and [`PrivateKey`]. //! The [`Keypair`] provides access to the secret and public keys. //! +//! # JavaScript Example +//! //! ```javascript //! //! let keypair = Keypair.random(); @@ -56,7 +58,8 @@ impl Keypair { } /// Get the [`Address`] of this Keypair's [`PublicKey`]. - /// Receives a [`NetworkType`] to determine the prefix of the address. + /// Receives a [`NetworkType`](kaspa_consensus_core::network::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
{ @@ -67,7 +70,8 @@ impl Keypair { } /// Get `ECDSA` [`Address`] of this Keypair's [`PublicKey`]. - /// Receives a [`NetworkType`] to determine the prefix of the address. + /// Receives a [`NetworkType`](kaspa_consensus_core::network::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
{ diff --git a/wallet/keys/src/lib.rs b/wallet/keys/src/lib.rs index bec8747d0..86984e36a 100644 --- a/wallet/keys/src/lib.rs +++ b/wallet/keys/src/lib.rs @@ -1,3 +1,10 @@ +//! +//! # Kaspa Wallet Keys +//! +//! This crate provides tools for creating and managing Kaspa wallet keys. +//! This includes extended key generation and derivation. +//! + pub mod derivation; pub mod derivation_path; pub mod error; diff --git a/wallet/keys/src/prelude.rs b/wallet/keys/src/prelude.rs index 1aed7c535..5d3af82dd 100644 --- a/wallet/keys/src/prelude.rs +++ b/wallet/keys/src/prelude.rs @@ -1,3 +1,7 @@ +//! +//! Re-exports of the most commonly used types and traits in this crate. +//! + pub use crate::derivation_path::*; pub use crate::keypair::*; pub use crate::privatekey::*; diff --git a/wallet/keys/src/privatekey.rs b/wallet/keys/src/privatekey.rs index 84e2d2e3b..554bdf36e 100644 --- a/wallet/keys/src/privatekey.rs +++ b/wallet/keys/src/privatekey.rs @@ -68,7 +68,8 @@ impl PrivateKey { } /// Get the [`Address`] of the PublicKey generated from this PrivateKey. - /// Receives a [`NetworkType`] to determine the prefix of the address. + /// Receives a [`NetworkType`](kaspa_consensus_core::network::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
{ @@ -80,7 +81,8 @@ impl PrivateKey { } /// Get `ECDSA` [`Address`] of the PublicKey generated from this PrivateKey. - /// Receives a [`NetworkType`] to determine the prefix of the address. + /// Receives a [`NetworkType`](kaspa_consensus_core::network::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
{ diff --git a/wallet/keys/src/privkeygen.rs b/wallet/keys/src/privkeygen.rs index ff2f3bd8f..474dec8ec 100644 --- a/wallet/keys/src/privkeygen.rs +++ b/wallet/keys/src/privkeygen.rs @@ -1,3 +1,7 @@ +//! +//! [`PrivateKeyGenerator`] helper for generating private key derivations from an extended private key (XPrv). +//! + use crate::derivation::gen1::WalletDerivationManager; use crate::imports::*; diff --git a/wallet/keys/src/pubkeygen.rs b/wallet/keys/src/pubkeygen.rs index c05ae844f..a61eeb5ad 100644 --- a/wallet/keys/src/pubkeygen.rs +++ b/wallet/keys/src/pubkeygen.rs @@ -1,3 +1,7 @@ +//! +//! [`PublicKeyGenerator`] helper for generating public key derivations from an extended public key (XPub). +//! + use crate::derivation::gen1::WalletDerivationManager; use crate::derivation::traits::WalletDerivationManagerTrait; use crate::imports::*; diff --git a/wallet/keys/src/publickey.rs b/wallet/keys/src/publickey.rs index d63eca491..235eb8080 100644 --- a/wallet/keys/src/publickey.rs +++ b/wallet/keys/src/publickey.rs @@ -1,6 +1,6 @@ //! -//! [`keypair`](mod@self) module encapsulates [`Keypair`] and [`PrivateKey`]. -//! The [`Keypair`] provides access to the secret and public keys. +//! [`keypair`](mod@self) module encapsulates [`Keypair`](crate::keypair::Keypair) and [`PrivateKey`]. +//! The [`Keypair`](crate::keypair::Keypair) provides access to the secret and public keys. //! //! ```javascript //! diff --git a/wallet/keys/src/secret.rs b/wallet/keys/src/secret.rs index 99d94f5be..d934ce4ec 100644 --- a/wallet/keys/src/secret.rs +++ b/wallet/keys/src/secret.rs @@ -1,10 +1,10 @@ //! -//! Secret container for sensitive data. Performs zeroization on drop. +//! Secret container for sensitive data. Performs data erasure (zeroization) on drop. //! use crate::imports::*; -/// Secret container for sensitive data. Performs memory zeroization on drop. +/// Secret container for sensitive data. Performs memory erasure (zeroization) on drop. #[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct Secret(Vec); diff --git a/wallet/keys/src/types.rs b/wallet/keys/src/types.rs index c006d2872..11c567834 100644 --- a/wallet/keys/src/types.rs +++ b/wallet/keys/src/types.rs @@ -1,5 +1,5 @@ //! -//! Type aliases used by the wallet framework. +//! Key-related type aliases used by the wallet framework. //! use std::sync::Arc; diff --git a/wallet/keys/src/xprv.rs b/wallet/keys/src/xprv.rs index 3e120841b..c19e0b9cc 100644 --- a/wallet/keys/src/xprv.rs +++ b/wallet/keys/src/xprv.rs @@ -1,3 +1,7 @@ +//! +//! Extended private key ([`XPrv`]). +//! + use kaspa_bip32::{ChainCode, KeyFingerprint}; use crate::imports::*; diff --git a/wallet/keys/src/xpub.rs b/wallet/keys/src/xpub.rs index 551881d8e..8706f3fc9 100644 --- a/wallet/keys/src/xpub.rs +++ b/wallet/keys/src/xpub.rs @@ -1,3 +1,7 @@ +//! +//! Extended public key ([`XPub`]). +//! + use kaspa_bip32::{ChainCode, KeyFingerprint, Prefix}; use std::{fmt, str::FromStr}; diff --git a/wallet/pskt/src/bundle.rs b/wallet/pskt/src/bundle.rs index 6e8dc8350..6c926c666 100644 --- a/wallet/pskt/src/bundle.rs +++ b/wallet/pskt/src/bundle.rs @@ -13,6 +13,11 @@ use kaspa_txscript::{extract_script_pub_key_address, pay_to_address_script, pay_ use serde::{Deserialize, Serialize}; use std::ops::Deref; +/// +/// Bundle is a [`PSKT`] bundle - a sequence of PSKT transactions +/// meant for batch processing and transport as a +/// single serialized payload. +/// #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Bundle(pub Vec); diff --git a/wallet/pskt/src/convert.rs b/wallet/pskt/src/convert.rs index 18acf94ed..a3956c3bb 100644 --- a/wallet/pskt/src/convert.rs +++ b/wallet/pskt/src/convert.rs @@ -1,3 +1,9 @@ +//! +//! Conversion functions for converting between +//! the [`kaspa_consensus_client`], [`kaspa_consensus_core`] +//! and [`kaspa_wallet_pskt`](crate) types. +//! + use crate::error::Error; use crate::input::{Input, InputBuilder}; use crate::output::{Output, OutputBuilder}; diff --git a/wallet/pskt/src/error.rs b/wallet/pskt/src/error.rs index 11303ae4a..f3fd83570 100644 --- a/wallet/pskt/src/error.rs +++ b/wallet/pskt/src/error.rs @@ -1,3 +1,5 @@ +//! Error types for the PSKT crate. + use kaspa_txscript_errors::TxScriptError; use crate::input::InputBuilderError; diff --git a/wallet/pskt/src/global.rs b/wallet/pskt/src/global.rs index b79798776..ad98f11d3 100644 --- a/wallet/pskt/src/global.rs +++ b/wallet/pskt/src/global.rs @@ -1,3 +1,5 @@ +//! Global PSKT data. + use crate::pskt::{KeySource, Version}; use crate::utils::combine_if_no_conflicts; use derive_builder::Builder; diff --git a/wallet/pskt/src/input.rs b/wallet/pskt/src/input.rs index c99ae2542..8d01a7d48 100644 --- a/wallet/pskt/src/input.rs +++ b/wallet/pskt/src/input.rs @@ -1,3 +1,5 @@ +//! PSKT input structure. + use crate::pskt::{KeySource, PartialSigs}; use crate::utils::{combine_if_no_conflicts, Error as CombineMapErr}; use derive_builder::Builder; diff --git a/wallet/pskt/src/output.rs b/wallet/pskt/src/output.rs index e873ce4a6..36b09edae 100644 --- a/wallet/pskt/src/output.rs +++ b/wallet/pskt/src/output.rs @@ -1,3 +1,5 @@ +//! PSKT output structure. + use crate::pskt::KeySource; use crate::utils::combine_if_no_conflicts; use derive_builder::Builder; diff --git a/wallet/pskt/src/pskt.rs b/wallet/pskt/src/pskt.rs index 245609803..73f87a628 100644 --- a/wallet/pskt/src/pskt.rs +++ b/wallet/pskt/src/pskt.rs @@ -1,3 +1,7 @@ +//! +//! Partially Signed Kaspa Transaction (PSKT) +//! + use kaspa_bip32::{secp256k1, DerivationPath, KeyFingerprint}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -76,6 +80,23 @@ impl Signature { } } +/// +/// A Partially Signed Kaspa Transaction (PSKT) is a standardized format +/// that allows multiple participants to collaborate in creating and signing +/// a Kaspa transaction. PSKT enables the exchange of incomplete transaction +/// data between different wallets or entities, allowing each participant +/// to add their signature or inputs in stages. This facilitates more complex +/// transaction workflows, such as multi-signature setups or hardware wallet +/// interactions, by ensuring that sensitive data remains secure while +/// enabling cooperation across different devices or platforms without +/// exposing private keys. +/// +/// Please note that due to transaction mass limits and potential of +/// a wallet aggregating large UTXO sets, the PSKT [`Bundle`](crate::bundle::Bundle) primitive +/// is used to represent a collection of PSKTs and should be used for +/// PSKT serialization and transport. PSKT is an internal implementation +/// primitive that represents each transaction in the bundle. +/// #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PSKT { diff --git a/wallet/pskt/src/role.rs b/wallet/pskt/src/role.rs index 84f55bb04..2d6daa47d 100644 --- a/wallet/pskt/src/role.rs +++ b/wallet/pskt/src/role.rs @@ -1,3 +1,5 @@ +//! PSKT roles. + /// Initializes the PSKT with 0 inputs and 0 outputs. /// Reference: [BIP-370: Creator](https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#creator) pub enum Creator {} diff --git a/wallet/pskt/src/utils.rs b/wallet/pskt/src/utils.rs index 28b7959ed..357b61bc7 100644 --- a/wallet/pskt/src/utils.rs +++ b/wallet/pskt/src/utils.rs @@ -1,3 +1,5 @@ +//! Utility functions for the PSKT module. + use std::collections::BTreeMap; // todo optimize without cloning @@ -6,7 +8,7 @@ where V: Eq + Clone, K: Ord + Clone, { - if lhs.len() > rhs.len() { + if lhs.len() >= rhs.len() { if let Some((field, rhs, lhs)) = rhs.iter().map(|(k, v)| (k, v, lhs.get(k))).find(|(_, v, rhs_v)| rhs_v.is_some_and(|rv| rv != *v)) { diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index 77c5e16ea..d8b0f06a9 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -1,5 +1,5 @@ /*! -# `rusty-kaspa WASM32 bindings` +# Rusty Kaspa WASM32 bindings [github](https://github.com/kaspanet/rusty-kaspa/tree/master/wasm) [crates.io](https://crates.io/crates/kaspa-wasm) @@ -13,9 +13,9 @@ codebase within JavaScript 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/) +- [**Integrating with Kaspa** guide](https://kaspa.aspectron.org/) +- [Rust SDK documentation (**Rustdoc**)](https://docs.rs/kaspa-wasm/) +- [TypeScript documentation (**JSDoc**)](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 @@ -25,9 +25,10 @@ to the 'snake_case' convention in Rust. The APIs are currently separated into the following groups (this will be expanded in the future): -- **Transaction API** — Bindings for primitives related to transactions. -- **RPC API** — [RPC interface bindings](rpc) for the Kaspa node using WebSocket (wRPC) connections. -- **Wallet API** — API for async core wallet processing tasks. +- **Consensus Client API** — Bindings for primitives related to transactions. +- **RPC API** — [RPC interface bindings](kaspa_wrpc_wasm::client) for the Kaspa node using WebSocket (wRPC) connections. +- **Wallet SDK** — API for async core wallet processing tasks. +- **Wallet API** — A rust implementation of the fully-featured wallet usable in the native Rust, Browser or NodeJs and Bun environments. ## NPM Modules @@ -43,6 +44,9 @@ of a native WebSocket in NodeJs environment, while the `kaspa` module includes `websocket` package dependency simulating the W3C WebSocket and due to this supports RPC. +NOTE: for security reasons it is always recommended to build WASM SDK from source or +download pre-built redistributables from releases or development builds. + ## Examples JavaScript examples for using this framework can be found at: @@ -54,8 +58,19 @@ For pre-built browser-compatible WASM32 redistributables of this framework please see the releases section of the Rusty Kaspa repository at . +## Development Builds + +The latest development builds from . +Development builds typically contain fixes and improvements that are not yet available in +stable releases. Additional information can be found at +. + ## Using RPC +No special handling is required to use the RPC client +in **Browser** or **Bun** environments due to the fact that +these environments provide native WebSocket support. + **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). @@ -123,7 +138,7 @@ const rpc = new RpcClient({ })(); ``` -For more details, please follow the [**integrating with Kaspa**](https://kaspa.aspectron.org/) guide. +For more details, please follow the [**Integrating with Kaspa**](https://kaspa.aspectron.org/) guide. */