From bd67bfc92e301d408ebc3cc16a5ed0090fe68600 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Thu, 2 Jul 2026 04:29:54 -0400 Subject: [PATCH] GoblinPay: receive-only Grin payment server A self-hostable Grin payment server for shops, creators, and sites: show a code, Grin lands in your wallet, with a verifiable Grin payment proof on receive. Workspace crates (gp-core / gp-nostr / gp-server / gp-wallet / gp-goblin-sender), a WooCommerce connector, a hosted /pay/ checkout, and NIP-44 v3 gift-wrapped payment DMs carried over the Nym mixnet. All secrets are read from the environment; none are committed. --- .gitignore | 15 + Cargo.lock | 12209 ++++++++++++++++ Cargo.toml | 29 + README.md | 184 + ci.sh | 7 + connectors/woocommerce/INSTALL.md | 86 + connectors/woocommerce/README.md | 75 + connectors/woocommerce/assets/js/blocks.js | 41 + .../woocommerce/goblinpay-woocommerce.php | 516 + .../woocommerce/includes/class-blocks.php | 51 + crates/gp-core/Cargo.toml | 37 + crates/gp-core/src/config.rs | 801 + crates/gp-core/src/db.rs | 73 + crates/gp-core/src/derive.rs | 166 + crates/gp-core/src/endpub.rs | 356 + crates/gp-core/src/ids.rs | 90 + crates/gp-core/src/invoice.rs | 430 + crates/gp-core/src/lib.rs | 26 + crates/gp-core/src/matching.rs | 513 + crates/gp-core/src/qr.rs | 130 + crates/gp-core/src/rates.rs | 513 + crates/gp-core/src/store.rs | 99 + crates/gp-core/src/webhook.rs | 383 + crates/gp-goblin-sender/Cargo.toml | 24 + crates/gp-goblin-sender/src/main.rs | 464 + crates/gp-nostr/Cargo.toml | 53 + crates/gp-nostr/src/identity.rs | 309 + crates/gp-nostr/src/ingest.rs | 692 + crates/gp-nostr/src/lib.rs | 206 + crates/gp-nostr/src/nym/dns.rs | 235 + crates/gp-nostr/src/nym/mod.rs | 22 + crates/gp-nostr/src/nym/nymproc.rs | 192 + crates/gp-nostr/src/nym/transport.rs | 150 + crates/gp-nostr/src/protocol.rs | 180 + crates/gp-nostr/src/receipt.rs | 254 + crates/gp-nostr/src/relays.rs | 35 + crates/gp-nostr/src/service.rs | 491 + crates/gp-nostr/src/wrap.rs | 399 + crates/gp-server/Cargo.toml | 50 + crates/gp-server/askama.toml | 3 + crates/gp-server/src/admin.rs | 386 + crates/gp-server/src/auth.rs | 28 + crates/gp-server/src/checkout.rs | 245 + crates/gp-server/src/directory.rs | 151 + crates/gp-server/src/ingest.rs | 141 + crates/gp-server/src/invoices.rs | 233 + crates/gp-server/src/lib.rs | 13 + crates/gp-server/src/main.rs | 297 + crates/gp-server/src/payments.rs | 261 + crates/gp-server/src/record.rs | 91 + crates/gp-server/src/webhookd.rs | 87 + crates/gp-server/tests/checkout_e2e.rs | 235 + crates/gp-server/tests/nostr_ingest_e2e.rs | 291 + crates/gp-wallet/Cargo.toml | 50 + crates/gp-wallet/src/confirm.rs | 219 + crates/gp-wallet/src/lib.rs | 523 + crates/gp-wallet/src/proof.rs | 230 + crates/gp-wallet/tests/goblin_roundtrip.rs | 185 + crates/gp-wallet/tests/proof_roundtrip.rs | 173 + migrations/0001_init.sql | 25 + migrations/0002_reply.sql | 6 + migrations/0003_confirm.sql | 26 + migrations/0004_invoices.sql | 32 + migrations/0005_endpubs.sql | 29 + migrations/0006_webhook.sql | 20 + migrations/0007_rates.sql | 11 + rustfmt.toml | 3 + static/goblin-mark.svg | 7 + static/style.css | 106 + templates/admin.html | 69 + templates/base.html | 12 + templates/index.html | 8 + templates/pay.html | 52 + templates/pay_result.html | 28 + 74 files changed, 24862 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100755 ci.sh create mode 100644 connectors/woocommerce/INSTALL.md create mode 100644 connectors/woocommerce/README.md create mode 100644 connectors/woocommerce/assets/js/blocks.js create mode 100644 connectors/woocommerce/goblinpay-woocommerce.php create mode 100644 connectors/woocommerce/includes/class-blocks.php create mode 100644 crates/gp-core/Cargo.toml create mode 100644 crates/gp-core/src/config.rs create mode 100644 crates/gp-core/src/db.rs create mode 100644 crates/gp-core/src/derive.rs create mode 100644 crates/gp-core/src/endpub.rs create mode 100644 crates/gp-core/src/ids.rs create mode 100644 crates/gp-core/src/invoice.rs create mode 100644 crates/gp-core/src/lib.rs create mode 100644 crates/gp-core/src/matching.rs create mode 100644 crates/gp-core/src/qr.rs create mode 100644 crates/gp-core/src/rates.rs create mode 100644 crates/gp-core/src/store.rs create mode 100644 crates/gp-core/src/webhook.rs create mode 100644 crates/gp-goblin-sender/Cargo.toml create mode 100644 crates/gp-goblin-sender/src/main.rs create mode 100644 crates/gp-nostr/Cargo.toml create mode 100644 crates/gp-nostr/src/identity.rs create mode 100644 crates/gp-nostr/src/ingest.rs create mode 100644 crates/gp-nostr/src/lib.rs create mode 100644 crates/gp-nostr/src/nym/dns.rs create mode 100644 crates/gp-nostr/src/nym/mod.rs create mode 100644 crates/gp-nostr/src/nym/nymproc.rs create mode 100644 crates/gp-nostr/src/nym/transport.rs create mode 100644 crates/gp-nostr/src/protocol.rs create mode 100644 crates/gp-nostr/src/receipt.rs create mode 100644 crates/gp-nostr/src/relays.rs create mode 100644 crates/gp-nostr/src/service.rs create mode 100644 crates/gp-nostr/src/wrap.rs create mode 100644 crates/gp-server/Cargo.toml create mode 100644 crates/gp-server/askama.toml create mode 100644 crates/gp-server/src/admin.rs create mode 100644 crates/gp-server/src/auth.rs create mode 100644 crates/gp-server/src/checkout.rs create mode 100644 crates/gp-server/src/directory.rs create mode 100644 crates/gp-server/src/ingest.rs create mode 100644 crates/gp-server/src/invoices.rs create mode 100644 crates/gp-server/src/lib.rs create mode 100644 crates/gp-server/src/main.rs create mode 100644 crates/gp-server/src/payments.rs create mode 100644 crates/gp-server/src/record.rs create mode 100644 crates/gp-server/src/webhookd.rs create mode 100644 crates/gp-server/tests/checkout_e2e.rs create mode 100644 crates/gp-server/tests/nostr_ingest_e2e.rs create mode 100644 crates/gp-wallet/Cargo.toml create mode 100644 crates/gp-wallet/src/confirm.rs create mode 100644 crates/gp-wallet/src/lib.rs create mode 100644 crates/gp-wallet/src/proof.rs create mode 100644 crates/gp-wallet/tests/goblin_roundtrip.rs create mode 100644 crates/gp-wallet/tests/proof_roundtrip.rs create mode 100644 migrations/0001_init.sql create mode 100644 migrations/0002_reply.sql create mode 100644 migrations/0003_confirm.sql create mode 100644 migrations/0004_invoices.sql create mode 100644 migrations/0005_endpubs.sql create mode 100644 migrations/0006_webhook.sql create mode 100644 migrations/0007_rates.sql create mode 100644 rustfmt.toml create mode 100644 static/goblin-mark.svg create mode 100644 static/style.css create mode 100644 templates/admin.html create mode 100644 templates/base.html create mode 100644 templates/index.html create mode 100644 templates/pay.html create mode 100644 templates/pay_result.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e36e8a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/target +*.db +*.db-shm +*.db-wal +# Runtime wallet/node secrets, keys, and logs — NEVER commit (payment server) +.owner_api_secret +.foreign_api_secret +grin-wallet.toml +grin-wallet.log +*.log +.env +secrets/ +/data +/wallet_data +*.seed diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5cf71f3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,12209 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.13.0", + "bytes 1.12.0", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite 0.2.17", + "tokio 1.52.3", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "bitflags 2.13.0", + "bytes 1.12.0", + "bytestring", + "derive_more 2.1.1", + "encoding_rs", + "foldhash 0.2.0", + "futures-core", + "h2 0.3.27", + "http 0.2.12", + "httparse", + "httpdate 1.0.3", + "itoa 1.0.18", + "language-tags", + "mime", + "percent-encoding", + "pin-project-lite 0.2.17", + "smallvec", + "tokio 1.52.3", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "actix-router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +dependencies = [ + "bytestring", + "cfg-if 1.0.4", + "http 0.2.12", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio 1.52.3", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio 1.2.1", + "socket2 0.5.10", + "tokio 1.52.3", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.17", +] + +[[package]] +name = "actix-tls" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more 0.1.9", + "pin-project-lite 0.2.17", + "rustls-pki-types", + "tokio 1.52.3", + "tokio-rustls 0.26.4", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite 0.2.17", +] + +[[package]] +name = "actix-web" +version = "4.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "bytes 1.12.0", + "bytestring", + "cfg-if 1.0.4", + "derive_more 2.1.1", + "encoding_rs", + "foldhash 0.2.0", + "futures-core", + "futures-util", + "impl-more 0.3.1", + "itoa 1.0.18", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite 0.2.17", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.4", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array 0.14.7", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.4", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr", + "ghash", + "subtle 2.6.1", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr", + "polyval", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "aes-keywrap" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b6f24a1f796bc46415a1d0d18dc0a8203ccba088acf5def3291c4f61225522" +dependencies = [ + "aes 0.9.1", + "byteorder", +] + +[[package]] +name = "age" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815e87cc8c39227cfff259f9550bd9f1c1a082370eccf4e9a176327fb7f906c9" +dependencies = [ + "age-core", + "base64 0.13.1", + "bech32 0.8.1", + "chacha20poly1305 0.9.1", + "cookie-factory", + "hkdf 0.11.0", + "hmac 0.11.0", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand 0.7.3", + "rand 0.8.6", + "rust-embed", + "scrypt 0.8.1", + "sha2 0.9.9", + "subtle 2.6.1", + "x25519-dalek 1.1.1", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70afa630ef12a4fc666277713efbe6da2bc87bb3f3af0f1149415b701362c615" +dependencies = [ + "base64 0.13.1", + "chacha20poly1305 0.9.1", + "cookie-factory", + "hkdf 0.11.0", + "nom", + "rand 0.8.6", + "secrecy 0.8.0", + "sha2 0.9.9", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if 1.0.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits 0.2.19", + "rayon", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint 0.4.6", + "num-traits 0.2.19", + "paste", + "rayon", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint 0.4.6", + "num-traits 0.2.19", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits 0.2.19", + "rand 0.8.6", + "rayon", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa 1.0.18", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2 1.0.106", + "quote 1.0.46", + "rustc-hash 2.1.2", + "serde", + "serde_derive", + "syn 2.0.118", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow 0.7.15", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite 0.2.17", + "tokio 1.52.3", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite 0.2.17", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "async-utility" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" +dependencies = [ + "futures-util", + "gloo-timers", + "tokio 1.52.3", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-wsocket" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" +dependencies = [ + "async-utility", + "futures 0.3.32", + "futures-util", + "js-sys", + "tokio 1.52.3", + "tokio-rustls 0.26.4", + "tokio-socks 0.5.3", + "tokio-tungstenite 0.26.2", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "atomic-destructor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.5.1", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if 1.0.4", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bech32" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + +[[package]] +name = "bip32" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db40d3dfbeab4e031d78c844642fa0caa0b0db11ce1607ac9d2986dff1405c69" +dependencies = [ + "bs58 0.5.1", + "hmac 0.12.1", + "k256", + "rand_core 0.6.4", + "ripemd", + "secp256k1 0.27.0", + "sha2 0.10.9", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", + "zeroize", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +dependencies = [ + "hex-conservative 0.3.2", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", + "serde", +] + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac 0.7.0", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec 0.7.7", + "constant_time_eq 0.4.2", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec 0.7.7", + "cc", + "cfg-if 1.0.4", + "constant_time_eq 0.4.2", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding 0.1.5", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "bnum" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.9", + "tinyvec", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" +dependencies = [ + "git2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "bytecodec" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "bytestring" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" +dependencies = [ + "bytes 1.12.0", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "celes" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55028d5b1eebb35237512a3838ce5583211434a233c8bb179551a7197ffb7bd4" +dependencies = [ + "phf 0.13.1", + "serde", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + +[[package]] +name = "chacha20" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +dependencies = [ + "cfg-if 1.0.4", + "cipher 0.3.0", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.4", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +dependencies = [ + "aead 0.4.3", + "chacha20 0.8.2", + "cipher 0.3.0", + "poly1305 0.7.2", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.2", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305 0.8.0", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits 0.2.19", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "coarsetime" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.52.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes 1.12.0", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures 0.3.32", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-models" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657f625ff361906f779745d08375ae3cc9fef87a35fba5f22874cf773010daf4" +dependencies = [ + "hax-lib", + "pastey", + "rand 0.9.2", +] + +[[package]] +name = "cosmos-sdk-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" +dependencies = [ + "prost", + "tendermint-proto", +] + +[[package]] +name = "cosmrs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e74fa7a22930fe0579bef560f2d64b78415d4c47b9dd976c0635136809471d" +dependencies = [ + "bip32", + "cosmos-sdk-proto", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature 2.2.0", + "subtle-encoding", + "tendermint", + "tendermint-rpc", + "thiserror 1.0.69", +] + +[[package]] +name = "cosmwasm-core" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9899c6499b006d10e5dc64052e642d365f239ba00339615e2714c50c6aa86389" + +[[package]] +name = "cosmwasm-crypto" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a3f5419d8f6ee9ae698db5a3d5d34ecd4ff82e8b5694bba7a620403c862717" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-serialize", + "cosmwasm-core", + "curve25519-dalek 4.1.3", + "digest 0.10.7", + "ecdsa", + "ed25519-zebra", + "k256", + "num-traits 0.2.19", + "p256", + "rand_core 0.6.4", + "rayon", + "sha2 0.10.9", + "thiserror 1.0.69", +] + +[[package]] +name = "cosmwasm-derive" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a625e259b6ab0cae1a758adf9a68a11ecddd023d1ab3d9c5d1785c144663c81" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "cosmwasm-schema" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6984ab21b47a096e17ae4c73cea2123a704d4b6686c39421247ad67020d76f95" +dependencies = [ + "cosmwasm-schema-derive", + "schemars", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "cosmwasm-schema-derive" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01c9214319017f6ebd8e299036e1f717fa9bb6724e758f7d6fb2477599d1a29" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "cosmwasm-std" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf82335c14bd94eeb4d3c461b7aa419ecd7ea13c2efe24b97cd972bdb8044e7d" +dependencies = [ + "base64 0.22.1", + "bech32 0.11.1", + "bnum", + "cosmwasm-core", + "cosmwasm-crypto", + "cosmwasm-derive", + "derive_more 1.0.0", + "hex", + "rand_core 0.6.4", + "rmp-serde", + "schemars", + "serde", + "serde-json-wasm", + "sha2 0.10.9", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "croaring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611eaefca84c93e431ad82dfb848f6e05a99e25148384f45a3852b0fbe1c8086" +dependencies = [ + "byteorder", + "croaring-sys", +] + +[[package]] +name = "croaring-sys" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5fed89265a702f0085844237a7ebbadf8a7c42de6304fddca30a5013f9aecb" +dependencies = [ + "cc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array 0.14.7", + "subtle 2.6.1", +] + +[[package]] +name = "ct-codecs" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fb0c6640b4507ebd99ff67677009e381ba5eee1d14df78de4a3d16eb123c39" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b85542f99a2dfa2a1b8e192662741c9859a846b296bef1c92ef9b58b5a216" +dependencies = [ + "byteorder", + "digest 0.8.1", + "rand_core 0.5.1", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "serde", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + +[[package]] +name = "cw-controllers" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c1804013d21060b994dea28a080f9eab78a3bcb6b617f05e7634b0600bf7b1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw-storage-plus" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07dfee7f12f802431a856984a32bce1cb7da1e6c006b5409e3981035ce562dec" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04852cd38f044c0751259d5f78255d07590d136b8a86d4e09efdd7666bd6d27" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "semver", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw20" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42212b6bf29bbdda693743697c621894723f35d3db0d5df930be22903d0e27c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e53c2057526c65d9c88be8b2a564729ebad7a3d87ee97b97665a71446f913a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw20", + "schemars", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cw4" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d33f5c8a6b6cd1bd24e212d7f44967697bfa3c4f9cc3f9a8e1c58f5fe5db032d" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.4", + "hashbrown 0.14.5", + "lock_api 0.4.14", + "once_cell", + "parking_lot_core 0.9.12", + "serde", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.1.0", +] + +[[package]] +name = "defmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl 2.1.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", + "unicode-xid 0.2.6", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2 1.0.106", + "quote 1.0.46", + "rustc_version", + "syn 2.0.118", + "unicode-xid 0.2.6", +] + +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle 2.6.1", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi 0.3.9", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf 0.11.3", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "easy-jsonrpc-mw" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b1a91569d50e3bba3c9febb22ef54d78c6e8a8d8dd91ae859896c8ba05f4e3" +dependencies = [ + "easy-jsonrpc-proc-macro-mw", + "jsonrpc-core", + "rand 0.6.5", + "serde", + "serde_json", +] + +[[package]] +name = "easy-jsonrpc-proc-macro-mw" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6368dbd2c6685fb84fc6e6a4749917ddc98905793fd06341c7e11a2504f2724" +dependencies = [ + "heck 0.3.3", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect 0.2.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "serde", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-compact" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c24599140dc39d7a81e4476e7573d41bbc18e07c803900298e522a5fbcfbfb6" +dependencies = [ + "ct-codecs", + "getrandom 0.4.3", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519 1.5.3", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "hashbrown 0.14.5", + "hex", + "rand_core 0.6.4", + "sha2 0.10.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.7", + "group", + "hkdf 0.12.4", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "enum_primitive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" +dependencies = [ + "num-traits 0.1.43", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if 1.0.4", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite 0.2.17", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "eyre", + "paste", +] + +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api 0.4.14", + "parking_lot 0.12.5", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.17", + "slab", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "serde", + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug 0.3.1", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "git2" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" +dependencies = [ + "bitflags 2.13.0", + "libc", + "libgit2-sys", + "log", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.4.2", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gp-core" +version = "0.1.0" +dependencies = [ + "hex", + "hmac 0.12.1", + "log", + "qrcode", + "rand 0.9.2", + "reqwest 0.13.4", + "secp256k1 0.31.1", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx", + "subtle 2.6.1", + "tokio 1.52.3", +] + +[[package]] +name = "gp-goblin-sender" +version = "0.1.0" +dependencies = [ + "grin_core 5.4.1", + "grin_keychain 5.4.1", + "grin_util 5.4.1", + "grin_wallet_impls 5.4.0-alpha.1", + "grin_wallet_libwallet 5.4.0-alpha.1", + "rand 0.6.5", + "serde_json", +] + +[[package]] +name = "gp-nostr" +version = "0.1.0" +dependencies = [ + "async-wsocket", + "gp-core", + "hickory-proto", + "log", + "nip44", + "nostr-relay-pool", + "nostr-sdk", + "rand 0.9.2", + "secp256k1 0.31.1", + "serde", + "serde_json", + "smolmix", + "tokio 1.52.3", + "tokio-tungstenite 0.26.2", +] + +[[package]] +name = "gp-server" +version = "0.1.0" +dependencies = [ + "actix-web", + "askama", + "env_logger", + "gp-core", + "gp-nostr", + "gp-wallet", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_keychain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "nip44", + "nostr-sdk", + "rand 0.6.5", + "reqwest 0.13.4", + "rustls 0.23.41", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "sqlx", + "tokio 1.52.3", +] + +[[package]] +name = "gp-wallet" +version = "0.1.0" +dependencies = [ + "ed25519-dalek 1.0.1", + "gp-core", + "grin_api 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_keychain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_wallet_impls 5.4.1", + "grin_wallet_libwallet 5.4.1", + "rand 0.6.5", + "serde", + "serde_json", +] + +[[package]] +name = "grin_api" +version = "5.4.1" +dependencies = [ + "async-stream", + "bytes 1.12.0", + "easy-jsonrpc-mw", + "futures 0.3.32", + "grin_chain 5.4.1", + "grin_core 5.4.1", + "grin_p2p 5.4.1", + "grin_pool 5.4.1", + "grin_store 5.4.1", + "grin_util 5.4.1", + "http 0.2.12", + "hyper 0.14.32", + "hyper-rustls 0.23.2", + "hyper-timeout", + "lazy_static", + "log", + "regex", + "ring 0.16.20", + "rustls 0.20.9", + "rustls-pemfile 1.0.4", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "tokio 1.52.3", + "tokio-rustls 0.23.4", + "url", +] + +[[package]] +name = "grin_api" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51cb8a36f1df7f552e7033b07800c5e54e350b06ab42ab77c209b0da2ede9547" +dependencies = [ + "async-stream", + "bytes 1.12.0", + "easy-jsonrpc-mw", + "futures 0.3.32", + "grin_chain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_p2p 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_pool 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_store 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.12", + "hyper 0.14.32", + "hyper-rustls 0.23.2", + "hyper-timeout", + "lazy_static", + "log", + "regex", + "ring 0.16.20", + "rustls 0.20.9", + "rustls-pemfile 1.0.4", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "tokio 1.52.3", + "tokio-rustls 0.23.4", + "url", +] + +[[package]] +name = "grin_chain" +version = "5.4.1" +dependencies = [ + "bit-vec", + "bitflags 1.3.2", + "byteorder", + "chrono", + "croaring", + "enum_primitive", + "grin_core 5.4.1", + "grin_keychain 5.4.1", + "grin_store 5.4.1", + "grin_util 5.4.1", + "lazy_static", + "log", + "lru-cache", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_chain" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22a388060be19bb1fe8bcaae54e5b61726db8dae4676ff2f3f082752843bc02" +dependencies = [ + "bit-vec", + "bitflags 1.3.2", + "byteorder", + "chrono", + "croaring", + "enum_primitive", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_keychain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_store 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "log", + "lru-cache", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_core" +version = "5.4.1" +dependencies = [ + "blake2-rfc", + "byteorder", + "bytes 0.5.6", + "chrono", + "croaring", + "enum_primitive", + "grin_keychain 5.4.1", + "grin_util 5.4.1", + "lazy_static", + "log", + "lru-cache", + "num", + "num-bigint 0.2.6", + "rand 0.6.5", + "serde", + "serde_derive", + "siphasher 0.3.11", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "grin_core" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b286b10a906330569326077779fa6bcde456f5c77e54bc87aefbda308ee9b19e" +dependencies = [ + "blake2-rfc", + "byteorder", + "bytes 0.5.6", + "chrono", + "croaring", + "enum_primitive", + "grin_keychain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "log", + "lru-cache", + "num", + "num-bigint 0.2.6", + "rand 0.6.5", + "serde", + "serde_derive", + "siphasher 0.3.11", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "grin_keychain" +version = "5.4.1" +dependencies = [ + "blake2-rfc", + "byteorder", + "digest 0.9.0", + "grin_util 5.4.1", + "hmac 0.11.0", + "lazy_static", + "log", + "pbkdf2 0.8.0", + "rand 0.6.5", + "ripemd160", + "serde", + "serde_derive", + "serde_json", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "grin_keychain" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afff7e0a383366082aad384e7b4f285519a4940a569cac793bb941354ce0e368" +dependencies = [ + "blake2-rfc", + "byteorder", + "digest 0.9.0", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hmac 0.11.0", + "lazy_static", + "log", + "pbkdf2 0.8.0", + "rand 0.6.5", + "ripemd160", + "serde", + "serde_derive", + "serde_json", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "grin_p2p" +version = "5.4.1" +dependencies = [ + "bitflags 1.3.2", + "built", + "bytes 0.5.6", + "chrono", + "enum_primitive", + "grin_chain 5.4.1", + "grin_core 5.4.1", + "grin_store 5.4.1", + "grin_util 5.4.1", + "log", + "lru-cache", + "num", + "rand 0.6.5", + "serde", + "serde_derive", + "tempfile", +] + +[[package]] +name = "grin_p2p" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69be142b76bd6472d2faab27dd3e0024a2c6818ed0e1ccc5a0e9788590949744" +dependencies = [ + "bitflags 1.3.2", + "bytes 0.5.6", + "chrono", + "enum_primitive", + "grin_chain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_store 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "lru-cache", + "num", + "rand 0.6.5", + "serde", + "serde_derive", + "tempfile", +] + +[[package]] +name = "grin_pool" +version = "5.4.1" +dependencies = [ + "blake2-rfc", + "chrono", + "grin_core 5.4.1", + "grin_keychain 5.4.1", + "grin_util 5.4.1", + "log", + "rand 0.6.5", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_pool" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f770835a6f78de214d650f9fec066d55b854b3689c905d63e931eba05179587a" +dependencies = [ + "blake2-rfc", + "chrono", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_keychain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "rand 0.6.5", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_secp256k1zkp" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7bb95f155b1eede2648a1b9afbba82bc3d9f2af0518b478767559a572bd973" +dependencies = [ + "arrayvec 0.7.7", + "cc", + "libc", + "rand 0.5.6", + "serde", + "serde_json", + "zeroize", +] + +[[package]] +name = "grin_store" +version = "5.4.1" +dependencies = [ + "byteorder", + "croaring", + "grin_core 5.4.1", + "grin_util 5.4.1", + "heed", + "libc", + "log", + "memmap", + "serde", + "serde_derive", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_store" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14188a453dbbb505bc068763beb4d8d9e9c18f88d8f0d950b967a13d5500882f" +dependencies = [ + "byteorder", + "croaring", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "lmdb-zero", + "log", + "memmap", + "serde", + "serde_derive", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_util" +version = "5.4.1" +dependencies = [ + "anyhow", + "backtrace", + "base64 0.12.3", + "byteorder", + "grin_secp256k1zkp", + "lazy_static", + "log", + "log4rs", + "parking_lot 0.10.2", + "rand 0.6.5", + "serde", + "serde_derive", + "walkdir", + "zeroize", + "zip", +] + +[[package]] +name = "grin_util" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff9437662d26682f7051a83b4da4544d42cb89966c857011768995dc7dec965" +dependencies = [ + "anyhow", + "backtrace", + "base64 0.12.3", + "byteorder", + "grin_secp256k1zkp", + "lazy_static", + "log", + "log4rs", + "parking_lot 0.10.2", + "rand 0.6.5", + "serde", + "serde_derive", + "walkdir", + "zeroize", + "zip", +] + +[[package]] +name = "grin_wallet_config" +version = "5.4.0-alpha.1" +dependencies = [ + "dirs 2.0.2", + "grin_core 5.4.1", + "grin_util 5.4.1", + "grin_wallet_util 5.4.0-alpha.1", + "rand 0.6.5", + "serde", + "serde_derive", + "toml 0.5.11", +] + +[[package]] +name = "grin_wallet_config" +version = "5.4.1" +source = "git+https://github.com/mimblewimble/grin-wallet?tag=v5.4.1#5c20635a24a1afa48c167775081015cae6321a4f" +dependencies = [ + "dirs 2.0.2", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_wallet_util 5.4.1", + "rand 0.6.5", + "serde", + "serde_derive", + "toml 0.5.11", +] + +[[package]] +name = "grin_wallet_impls" +version = "5.4.0-alpha.1" +dependencies = [ + "base64 0.12.3", + "blake2-rfc", + "byteorder", + "chrono", + "data-encoding", + "ed25519-dalek 1.0.1", + "futures 0.3.32", + "grin_api 5.4.1", + "grin_chain 5.4.1", + "grin_core 5.4.1", + "grin_keychain 5.4.1", + "grin_store 5.4.1", + "grin_util 5.4.1", + "grin_wallet_config 5.4.0-alpha.1", + "grin_wallet_libwallet 5.4.0-alpha.1", + "grin_wallet_util 5.4.0-alpha.1", + "lazy_static", + "log", + "rand 0.6.5", + "regex", + "reqwest 0.10.10", + "ring 0.16.20", + "serde", + "serde_derive", + "serde_json", + "sysinfo 0.29.11", + "thiserror 1.0.69", + "timer", + "tokio 0.2.25", + "url", + "uuid 0.8.2", + "x25519-dalek 0.6.0", +] + +[[package]] +name = "grin_wallet_impls" +version = "5.4.1" +source = "git+https://github.com/mimblewimble/grin-wallet?tag=v5.4.1#5c20635a24a1afa48c167775081015cae6321a4f" +dependencies = [ + "base64 0.12.3", + "blake2-rfc", + "byteorder", + "chrono", + "data-encoding", + "ed25519-dalek 1.0.1", + "futures 0.3.32", + "grin_api 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_chain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_keychain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_store 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_wallet_config 5.4.1", + "grin_wallet_libwallet 5.4.1", + "grin_wallet_util 5.4.1", + "lazy_static", + "log", + "rand 0.6.5", + "regex", + "reqwest 0.10.10", + "ring 0.16.20", + "serde", + "serde_derive", + "serde_json", + "sysinfo 0.29.11", + "thiserror 1.0.69", + "timer", + "tokio 0.2.25", + "url", + "uuid 0.8.2", + "x25519-dalek 0.6.0", +] + +[[package]] +name = "grin_wallet_libwallet" +version = "5.4.0-alpha.1" +dependencies = [ + "age", + "base64 0.9.3", + "bech32 0.7.3", + "blake2-rfc", + "bs58 0.3.1", + "byteorder", + "chacha20 0.8.2", + "chrono", + "curve25519-dalek 2.1.3", + "ed25519-dalek 1.0.1", + "grin_core 5.4.1", + "grin_keychain 5.4.1", + "grin_store 5.4.1", + "grin_util 5.4.1", + "grin_wallet_config 5.4.0-alpha.1", + "grin_wallet_util 5.4.0-alpha.1", + "hmac 0.12.1", + "lazy_static", + "log", + "num-bigint 0.2.6", + "rand 0.6.5", + "regex", + "secrecy 0.6.0", + "serde", + "serde_derive", + "serde_json", + "sha2 0.10.9", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror 1.0.69", + "uuid 0.8.2", + "x25519-dalek 0.6.0", +] + +[[package]] +name = "grin_wallet_libwallet" +version = "5.4.1" +source = "git+https://github.com/mimblewimble/grin-wallet?tag=v5.4.1#5c20635a24a1afa48c167775081015cae6321a4f" +dependencies = [ + "age", + "base64 0.9.3", + "bech32 0.7.3", + "blake2-rfc", + "bs58 0.3.1", + "byteorder", + "chacha20 0.8.2", + "chrono", + "curve25519-dalek 2.1.3", + "ed25519-dalek 1.0.1", + "grin_core 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_keychain 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_store 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_wallet_config 5.4.1", + "grin_wallet_util 5.4.1", + "hmac 0.12.1", + "lazy_static", + "log", + "num-bigint 0.2.6", + "rand 0.6.5", + "regex", + "secrecy 0.6.0", + "serde", + "serde_derive", + "serde_json", + "sha2 0.10.9", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror 1.0.69", + "uuid 0.8.2", + "x25519-dalek 0.6.0", +] + +[[package]] +name = "grin_wallet_util" +version = "5.4.0-alpha.1" +dependencies = [ + "data-encoding", + "ed25519-dalek 1.0.1", + "grin_util 5.4.1", + "rand 0.6.5", + "serde", + "serde_derive", + "sha3 0.8.2", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_wallet_util" +version = "5.4.1" +source = "git+https://github.com/mimblewimble/grin-wallet?tag=v5.4.1#5c20635a24a1afa48c167775081015cae6321a4f" +dependencies = [ + "data-encoding", + "ed25519-dalek 1.0.1", + "grin_util 5.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5", + "serde", + "serde_derive", + "sha3 0.8.2", + "thiserror 1.0.69", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 1.9.3", + "slab", + "tokio 0.2.25", + "tokio-util 0.3.1", + "tracing", + "tracing-futures", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes 1.12.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.14.0", + "slab", + "tokio 1.52.3", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes 1.12.0", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.2", + "indexmap 2.14.0", + "slab", + "tokio 1.52.3", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "handlebars" +version = "3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error", + "serde", + "serde_json", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "hax-lib" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" +dependencies = [ + "hax-lib-macros", + "num-bigint 0.4.6", + "num-traits 0.2.19", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "serde", + "serde_json", + "uuid 1.23.4", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "heed" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" +dependencies = [ + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec 0.7.7", +] + +[[package]] +name = "hex-conservative" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +dependencies = [ + "arrayvec 0.7.7", +] + +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "bytes 1.12.0", + "cfg-if 1.0.4", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "h2 0.4.15", + "hickory-proto", + "http 1.4.2", + "idna", + "ipnet", + "jni", + "rand 0.10.1", + "rustls 0.23.41", + "thiserror 2.0.18", + "tinyvec", + "tokio 1.52.3", + "tokio-rustls 0.26.4", + "tracing", + "url", + "webpki-roots 1.0.8", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring 0.17.14", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if 1.0.4", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot 0.12.5", + "rand 0.10.1", + "resolv-conf", + "rustls 0.23.41", + "smallvec", + "system-configuration 0.7.0", + "thiserror 2.0.18", + "tokio 1.52.3", + "tokio-rustls 0.26.4", + "tracing", + "webpki-roots 1.0.8", +] + +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha1-compact" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823" + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019ece39bbefc17f13f677a690328cb978dbf6790e141a3c24e66372cb38588b" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes 1.12.0", + "fnv", + "itoa 1.0.18", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes 1.12.0", + "itoa 1.0.18", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http 0.2.12", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes 1.12.0", + "http 0.2.12", + "pin-project-lite 0.2.17", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes 1.12.0", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes 1.12.0", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "pin-project-lite 0.2.17", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpcodec" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f49d64351430cabd543943b79d48aaf0bc95a41d9ccf5b8774c2cfd23422775" +dependencies = [ + "bytecodec", + "trackable 0.2.24", +] + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "ctutils", + "typenum", +] + +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.2.7", + "http 0.2.12", + "http-body 0.3.1", + "httparse", + "httpdate 0.3.2", + "itoa 0.4.8", + "pin-project", + "socket2 0.3.19", + "tokio 0.2.25", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes 1.12.0", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate 1.0.3", + "itoa 1.0.18", + "pin-project-lite 0.2.17", + "socket2 0.5.10", + "tokio 1.52.3", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes 1.12.0", + "futures-channel", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "httparse", + "httpdate 1.0.3", + "itoa 1.0.18", + "pin-project-lite 0.2.17", + "smallvec", + "tokio 1.52.3", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" +dependencies = [ + "bytes 0.5.6", + "futures-util", + "hyper 0.13.10", + "log", + "rustls 0.18.1", + "tokio 0.2.25", + "tokio-rustls 0.14.1", + "webpki 0.21.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.20.9", + "rustls-native-certs 0.6.3", + "tokio 1.52.3", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "rustls 0.21.12", + "tokio 1.52.3", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.2", + "hyper 1.10.1", + "hyper-util", + "rustls 0.23.41", + "tokio 1.52.3", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.32", + "pin-project-lite 0.2.17", + "tokio 1.52.3", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes 0.5.6", + "hyper 0.13.10", + "native-tls", + "tokio 0.2.25", + "tokio-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes 1.12.0", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "hyper 1.10.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite 0.2.17", + "socket2 0.6.4", + "tokio 1.52.3", + "tower-service", + "tracing", +] + +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a86226a7a16632de6723449ee5fe70bac5af718bc642ee9ca2f0f6e14fa1fa" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "lazy_static", + "log", + "parking_lot 0.12.5", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a3d3569737dfaac7fc1c4078e6af07471c3060b8e570bcd83cdd5f4685395" +dependencies = [ + "dashmap", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "lazy_static", + "proc-macro-error", + "proc-macro2 1.0.106", + "quote 1.0.46", + "strsim 0.10.0", + "syn 2.0.118", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "impl-more" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg 1.5.1", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding 0.3.3", + "generic-array 0.14.7", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.4", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +dependencies = [ + "defmt 1.1.0", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if 1.0.4", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if 1.0.4", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc15eef5f8b6bef5ac5f7440a957ff95d036e2f98706947741bfc93d1976db4c" +dependencies = [ + "futures 0.1.31", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jwt-simple" +version = "0.12.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a4207b9d1f423858853fb9ee8b34aebc2b661f3b446ca4efda46c4b3700a2f" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.6", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.4", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.9", + "signature 2.2.0", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libcrux-aesgcm" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f2a019dab4097585a7d4f5b9deebe46cd1e628b16a5bc4cb0ce35e1da334e6" +dependencies = [ + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-chacha20poly1305" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc08d044676af21343b32b988411fa98dbb5cf65a03c9df478ced221bbdfdb1b" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-poly1305", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-curve25519" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1e5fd8476a6ed609d24ef42aee5ab6f99f7c65d054f92412da9f499e423299" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-traits", +] + +[[package]] +name = "libcrux-ecdh" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f73ce79337c762eb38bbac91e4c9b9e60cf318e8501b812750c640814d45e" +dependencies = [ + "libcrux-curve25519", + "libcrux-p256", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-ed25519" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835919315b7042fe9e03b6458efe0db94bf2aa7b873934dbee5b5463a8124b43" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", + "rand_core 0.9.5", + "tls_codec", +] + +[[package]] +name = "libcrux-hacl-rs" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2637dc87d158e1f1b550fd9b226443e84153fded4de69028d897b534d16d22e6" +dependencies = [ + "libcrux-macros", +] + +[[package]] +name = "libcrux-hkdf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1a89ca0c89be3a268a921e47105fb7873badf7267f5e3ebf4ea46baedd73ef" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-hmac", + "libcrux-secrets", +] + +[[package]] +name = "libcrux-hmac" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7a242707d65960770bd7e14e4f18a92bdf0b967777dd404887db8d087a643b" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-sha2", +] + +[[package]] +name = "libcrux-intrinsics" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b5db005ff8001e026b73a6842ee81bbef8ec5ff0e1915a67ae65fd2a9fafa5" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-kem" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12631592f491d22fd1a176d32b2c6edfb673998fd3987e9d95f8fa79ad2a737b" +dependencies = [ + "libcrux-curve25519", + "libcrux-ecdh", + "libcrux-ml-kem", + "libcrux-p256", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338" +dependencies = [ + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "libcrux-ml-dsa" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a72929ed421cc3bf16a946b3e7d2a58d215b0b5c2a12be26b53629f081bf49b2" +dependencies = [ + "core-models", + "hax-lib", + "libcrux-intrinsics", + "libcrux-macros", + "libcrux-platform", + "libcrux-sha3", + "tls_codec", +] + +[[package]] +name = "libcrux-ml-kem" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14ab3e477de9df6ee1273a114018ff62c4996ca9220070c4e5cb1743f94a67d" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-sha3", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-p256" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4778ba25cb08bb8a96bd100e19ed9aecf78337198fd176036e21042b2dd99bc" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-secrets", + "libcrux-sha2", + "libcrux-traits", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-poly1305" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02491808ee5b9db8cb65fad64ae0be812db64beef179d945c00c7787dc7dfcf9" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", +] + +[[package]] +name = "libcrux-psq" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ade7aa5e1b4b400c716b313cbf69070988dd005f92e961c2da4c3c42fbea4" +dependencies = [ + "libcrux-aesgcm", + "libcrux-chacha20poly1305", + "libcrux-ecdh", + "libcrux-ed25519", + "libcrux-hkdf", + "libcrux-hmac", + "libcrux-kem", + "libcrux-ml-dsa", + "libcrux-ml-kem", + "libcrux-sha2", + "libcrux-traits", + "rand 0.9.2", + "tls_codec", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" +dependencies = [ + "hax-lib", +] + +[[package]] +name = "libcrux-sha2" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d253473f259fc74a280c43f29c464f7e374abdf28b4942234dc707f529d4b7" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-traits", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ae0b7d0e1cc4793a609fd0ff2ca3b3a3fabae523770c619a3d4bc86417b0d7" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e4fa89f3f5e34b47f928b22b1b78395a0d4ec23b1f583db635f128159d65f" +dependencies = [ + "libcrux-secrets", + "rand 0.9.2", +] + +[[package]] +name = "libgit2-sys" +version = "0.18.5+1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "liblmdb-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feed38a3a580f60bf61aaa067b0ff4123395966839adeaf67258a9e50c4d2e49" +dependencies = [ + "gcc", + "libc", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "bitflags 2.13.0", + "libc", + "plain", + "redox_syscall 0.9.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2 0.8.1", + "chacha", + "keystream", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lmdb-master-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b" +dependencies = [ + "cc", + "doxygen-rs", + "libc", +] + +[[package]] +name = "lmdb-zero" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13416eee745b087c22934f35f1f24da22da41ba2a5ce197143d168ce055cc58d" +dependencies = [ + "bitflags 0.9.1", + "libc", + "liblmdb-sys", + "supercow", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +dependencies = [ + "serde_core", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derive_more 2.1.1", + "flate2", + "fnv", + "humantime", + "libc", + "log", + "log-mdc", + "mock_instant", + "parking_lot 0.12.5", + "rand 0.9.2", + "serde", + "serde-value", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "thread-id", + "typemap-ors", + "unicode-segmentation", + "winapi 0.3.9", +] + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if 1.0.4", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio 0.6.23", + "miow 0.3.7", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ml-dsa" +version = "0.1.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163f15320f3fba11760c373af52d7f69d638482c2c350d877fb06513b1c3137c" +dependencies = [ + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8 0.11.0", + "sha3 0.11.0", + "signature 3.0.0", +] + +[[package]] +name = "mock_instant" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb517913cfcfb9eeda59f36020269075a152701a01606c612f547e4890be399" + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits 0.2.19", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot 0.12.5", + "portable-atomic", + "smallvec", + "tagptr", + "uuid 1.23.4", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe 0.2.1", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "negentropy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" + +[[package]] +name = "net2" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nip44" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chacha20 0.9.1", + "constant_time_eq 0.4.2", + "hkdf 0.12.4", + "hmac 0.12.1", + "rand_core 0.9.5", + "secp256k1 0.31.1", + "sha2 0.10.9", + "thiserror 2.0.18", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nostr" +version = "0.44.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cf5d15d70d1f8f4059e5f79923ac15891eb691d2843d01191e0585fb064d70" +dependencies = [ + "base64 0.22.1", + "bech32 0.11.1", + "bip39", + "bitcoin_hashes", + "cbc", + "chacha20 0.9.1", + "chacha20poly1305 0.10.1", + "getrandom 0.2.17", + "hex", + "instant", + "scrypt 0.11.0", + "secp256k1 0.29.1", + "serde", + "serde_json", + "unicode-normalization", + "url", +] + +[[package]] +name = "nostr-database" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" +dependencies = [ + "lru", + "nostr", + "tokio 1.52.3", +] + +[[package]] +name = "nostr-gossip" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +dependencies = [ + "nostr", +] + +[[package]] +name = "nostr-relay-pool" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91b2c039df4f96c4bf7dae52a74fd5516ad6dda83a11c0c69dea91b5255a4f37" +dependencies = [ + "async-utility", + "async-wsocket", + "atomic-destructor", + "hex", + "lru", + "negentropy", + "nostr", + "nostr-database", + "tokio 1.52.3", + "tracing", +] + +[[package]] +name = "nostr-sdk" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" +dependencies = [ + "async-utility", + "nostr", + "nostr-database", + "nostr-gossip", + "nostr-relay-pool", + "tokio 1.52.3", + "tracing", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits 0.2.19", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.5.1", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits 0.2.19", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.5.1", + "num-traits 0.2.19", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg 1.5.1", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.5.1", + "num-bigint 0.2.6", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg 1.5.1", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "nym-api-requests" +version = "1.21.1" +dependencies = [ + "bs58 0.5.1", + "celes", + "cosmrs", + "cosmwasm-std", + "ecdsa", + "hex", + "humantime-serde", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-signer-check-types", + "nym-ecash-time", + "nym-kkt-ciphersuite", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-node-requests", + "nym-noise-keys", + "nym-serde-helpers", + "nym-ticketbooks-merkle", + "schemars", + "serde", + "serde_json", + "sha2 0.10.9", + "strum 0.28.0", + "strum_macros 0.28.0", + "tendermint", + "tendermint-rpc", + "thiserror 2.0.18", + "time", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-bandwidth-controller" +version = "1.21.1" +dependencies = [ + "async-trait", + "log", + "nym-credential-storage", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-task", + "nym-validator-client", + "rand 0.8.6", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-bin-common" +version = "1.21.1" +dependencies = [ + "const-str", + "log", + "schemars", + "serde", + "tracing", + "tracing-subscriber", + "utoipa", + "vergen", +] + +[[package]] +name = "nym-bls12_381-fork" +version = "0.8.0-forked" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce84633751030f960a2fd167b5270ec21da4c40d9b6400e1b56676a682fe6f3d" +dependencies = [ + "digest 0.10.7", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "serdect 0.3.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "nym-client-core" +version = "1.21.1" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bs58 0.5.1", + "cfg-if 1.0.4", + "futures 0.3.32", + "getrandom 0.3.3", + "gloo-timers", + "http-body-util", + "humantime", + "hyper 1.10.1", + "hyper-util", + "nym-bandwidth-controller", + "nym-client-core-config-types", + "nym-client-core-gateways-storage", + "nym-client-core-surb-storage", + "nym-credential-storage", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-gateway-client", + "nym-gateway-requests", + "nym-http-api-client", + "nym-id", + "nym-mixnet-client", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-nonexhaustive-delayqueue", + "nym-pemstore", + "nym-sphinx", + "nym-statistics-common", + "nym-task", + "nym-topology", + "nym-validator-client", + "nym-wasm-utils", + "rand 0.8.6", + "rand_chacha 0.3.1", + "serde", + "serde_json", + "sha2 0.10.9", + "si-scale", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "tokio-stream", + "tokio-tungstenite 0.20.1", + "tokio_with_wasm", + "tracing", + "tungstenite 0.20.1", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-client-core-config-types" +version = "1.21.1" +dependencies = [ + "humantime-serde", + "nym-config", + "nym-pemstore", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-statistics-common", + "serde", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "nym-client-core-gateways-storage" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "nym-crypto", + "nym-gateway-client", + "nym-gateway-requests", + "serde", + "sqlx", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "nym-client-core-surb-storage" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "dashmap", + "nym-crypto", + "nym-sphinx", + "nym-sqlx-pool-guard", + "nym-task", + "sqlx", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "tracing", +] + +[[package]] +name = "nym-coconut-dkg-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw2", + "cw4", + "nym-contracts-common", + "nym-multisig-contract-common", +] + +[[package]] +name = "nym-common" +version = "1.21.1" +dependencies = [ + "tracing", + "tracing-test", +] + +[[package]] +name = "nym-compact-ecash" +version = "1.21.1" +dependencies = [ + "bincode", + "bs58 0.5.1", + "cfg-if 1.0.4", + "digest 0.10.7", + "ff", + "group", + "itertools 0.14.0", + "nym-bls12_381-fork", + "nym-network-defaults", + "nym-pemstore", + "rand 0.8.6", + "serde", + "sha2 0.10.9", + "subtle 2.6.1", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "nym-config" +version = "1.21.1" +dependencies = [ + "dirs 6.0.0", + "handlebars", + "log", + "nym-network-defaults", + "serde", + "thiserror 2.0.18", + "toml 0.8.23", + "url", +] + +[[package]] +name = "nym-contracts-common" +version = "1.21.1" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", + "thiserror 2.0.18", + "vergen", +] + +[[package]] +name = "nym-credential-storage" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "log", + "nym-compact-ecash", + "nym-credentials", + "nym-ecash-time", + "nym-sqlx-pool-guard", + "serde", + "sqlx", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "zeroize", +] + +[[package]] +name = "nym-credential-utils" +version = "1.21.1" +dependencies = [ + "log", + "nym-bandwidth-controller", + "nym-client-core", + "nym-config", + "nym-credential-storage", + "nym-credentials", + "nym-credentials-interface", + "nym-ecash-time", + "nym-validator-client", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", +] + +[[package]] +name = "nym-credentials" +version = "1.21.1" +dependencies = [ + "bincode", + "cosmrs", + "log", + "nym-api-requests", + "nym-bls12_381-fork", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", + "nym-http-api-client", + "nym-network-defaults", + "nym-serde-helpers", + "nym-validator-client", + "serde", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-credentials-interface" +version = "1.21.1" +dependencies = [ + "nym-bls12_381-fork", + "nym-compact-ecash", + "nym-ecash-time", + "nym-network-defaults", + "nym-upgrade-mode-check", + "rand 0.8.6", + "serde", + "strum 0.28.0", + "strum_macros 0.28.0", + "thiserror 2.0.18", + "time", + "utoipa", +] + +[[package]] +name = "nym-crypto" +version = "1.21.1" +dependencies = [ + "aead 0.5.2", + "aes 0.8.4", + "aes-gcm-siv", + "base64 0.22.1", + "blake3", + "bs58 0.5.1", + "cipher 0.4.4", + "ctr", + "curve25519-dalek 4.1.3", + "digest 0.10.7", + "ed25519-dalek 2.2.0", + "generic-array 0.14.7", + "hkdf 0.12.4", + "hmac 0.12.1", + "jwt-simple", + "libcrux-curve25519", + "libcrux-psq", + "nym-pemstore", + "nym-sphinx-types", + "rand 0.8.6", + "rand 0.9.2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle-encoding", + "thiserror 2.0.18", + "x25519-dalek 2.0.1", + "zeroize", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "1.21.1" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-ecash-signer-check-types" +version = "1.21.1" +dependencies = [ + "nym-coconut-dkg-common", + "nym-crypto", + "semver", + "serde", + "thiserror 2.0.18", + "time", + "tracing", + "url", + "utoipa", +] + +[[package]] +name = "nym-ecash-time" +version = "1.21.1" +dependencies = [ + "nym-compact-ecash", + "time", +] + +[[package]] +name = "nym-exit-policy" +version = "1.21.1" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-gateway-client" +version = "1.21.1" +dependencies = [ + "futures 0.3.32", + "getrandom 0.2.17", + "gloo-utils", + "nym-bandwidth-controller", + "nym-credential-storage", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-gateway-requests", + "nym-http-api-client", + "nym-network-defaults", + "nym-pemstore", + "nym-sphinx", + "nym-statistics-common", + "nym-task", + "nym-validator-client", + "nym-wasm-utils", + "rand 0.8.6", + "serde", + "si-scale", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "tokio-stream", + "tokio-tungstenite 0.20.1", + "tracing", + "tungstenite 0.20.1", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-gateway-requests" +version = "1.21.1" +dependencies = [ + "bs58 0.5.1", + "futures 0.3.32", + "generic-array 0.14.7", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-pemstore", + "nym-serde-helpers", + "nym-sphinx", + "nym-statistics-common", + "nym-task", + "rand 0.8.6", + "serde", + "serde_json", + "strum 0.28.0", + "subtle 2.6.1", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "tracing", + "tungstenite 0.20.1", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-group-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cw-controllers", + "cw4", + "schemars", + "serde", +] + +[[package]] +name = "nym-http-api-client" +version = "1.21.1" +dependencies = [ + "async-trait", + "bincode", + "bytes 1.12.0", + "cfg-if 1.0.4", + "encoding_rs", + "fastrand", + "hickory-resolver", + "http 1.4.2", + "inventory", + "itertools 0.14.0", + "mime", + "nym-bin-common", + "nym-http-api-client-macro", + "nym-http-api-common", + "nym-network-defaults", + "once_cell", + "reqwest 0.13.4", + "rustls 0.23.41", + "serde", + "serde_json", + "serde_plain", + "serde_yaml", + "thiserror 2.0.18", + "tokio 1.52.3", + "tracing", + "url", + "wasmtimer", + "webpki-roots 0.26.11", +] + +[[package]] +name = "nym-http-api-client-macro" +version = "1.21.1" +dependencies = [ + "proc-macro-crate", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", + "uuid 1.23.4", +] + +[[package]] +name = "nym-http-api-common" +version = "1.21.1" +dependencies = [ + "bincode", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "nym-id" +version = "1.21.1" +dependencies = [ + "nym-credential-storage", + "nym-credentials", + "thiserror 2.0.18", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "nym-ip-packet-requests" +version = "1.21.1" +dependencies = [ + "bincode", + "bytes 1.12.0", + "nym-bin-common", + "nym-crypto", + "nym-service-provider-requests-common", + "nym-sphinx", + "rand 0.8.6", + "semver", + "serde", + "thiserror 2.0.18", + "time", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "nym-kkt-ciphersuite" +version = "1.21.1" +dependencies = [ + "num_enum", + "semver", + "strum 0.28.0", + "strum_macros 0.28.0", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-lp-data" +version = "1.21.1" +dependencies = [ + "bytes 1.12.0", + "num_enum", + "nym-common", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "nym-metrics" +version = "1.21.1" +dependencies = [ + "dashmap", + "lazy_static", + "prometheus", + "tracing", +] + +[[package]] +name = "nym-mixnet-client" +version = "1.21.1" +dependencies = [ + "dashmap", + "futures 0.3.32", + "nym-metrics", + "nym-noise", + "nym-sphinx", + "nym-task", + "strum 0.28.0", + "tokio 1.52.3", + "tokio-stream", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "nym-mixnet-contract-common" +version = "1.21.1" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "humantime-serde", + "nym-contracts-common", + "schemars", + "semver", + "serde", + "serde_repr", + "thiserror 2.0.18", + "time", + "utoipa", +] + +[[package]] +name = "nym-multisig-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw3", + "cw4", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-network-defaults" +version = "1.21.1" +dependencies = [ + "cargo_metadata 0.19.2", + "dotenvy", + "regex", + "schemars", + "serde", + "serde_json", + "tracing", + "url", + "utoipa", +] + +[[package]] +name = "nym-network-monitors-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-node-families-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-contracts-common", + "nym-mixnet-contract-common", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-node-requests" +version = "1.21.1" +dependencies = [ + "celes", + "humantime-serde", + "nym-bin-common", + "nym-crypto", + "nym-exit-policy", + "nym-kkt-ciphersuite", + "nym-noise-keys", + "nym-upgrade-mode-check", + "nym-wireguard-types", + "schemars", + "serde", + "serde_json", + "strum 0.28.0", + "strum_macros 0.28.0", + "thiserror 2.0.18", + "time", + "url", + "utoipa", +] + +[[package]] +name = "nym-noise" +version = "1.21.1" +dependencies = [ + "arc-swap", + "bytes 1.12.0", + "futures 0.3.32", + "nym-crypto", + "nym-noise-keys", + "pin-project", + "sha2 0.10.9", + "snow", + "strum 0.28.0", + "strum_macros 0.28.0", + "thiserror 2.0.18", + "tokio 1.52.3", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "nym-noise-keys" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "schemars", + "serde", + "utoipa", +] + +[[package]] +name = "nym-nonexhaustive-delayqueue" +version = "1.21.1" +dependencies = [ + "tokio 1.52.3", + "tokio-stream", + "tokio-util 0.7.18", + "wasmtimer", +] + +[[package]] +name = "nym-ordered-buffer" +version = "1.21.1" +dependencies = [ + "log", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-outfox" +version = "1.21.1" +dependencies = [ + "blake3", + "chacha20 0.9.1", + "chacha20poly1305 0.10.1", + "sphinx-packet", + "thiserror 2.0.18", + "x25519-dalek 2.0.1", + "zeroize", +] + +[[package]] +name = "nym-pemstore" +version = "1.21.1" +dependencies = [ + "pem", + "tracing", + "zeroize", +] + +[[package]] +name = "nym-performance-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sdk" +version = "1.21.1" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bip39", + "bytecodec", + "bytes 1.12.0", + "clap", + "dashmap", + "dirs 6.0.0", + "futures 0.3.32", + "http 1.4.2", + "httpcodec", + "log", + "nym-bandwidth-controller", + "nym-bin-common", + "nym-client-core", + "nym-credential-storage", + "nym-credential-utils", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-gateway-requests", + "nym-http-api-client", + "nym-ip-packet-requests", + "nym-lp-data", + "nym-network-defaults", + "nym-ordered-buffer", + "nym-service-providers-common", + "nym-socks5-client-core", + "nym-socks5-requests", + "nym-sphinx", + "nym-sphinx-addressing", + "nym-statistics-common", + "nym-task", + "nym-topology", + "nym-validator-client", + "rand 0.8.6", + "semver", + "serde", + "tap", + "tempfile", + "thiserror 2.0.18", + "tokio 1.52.3", + "tokio-stream", + "tokio-util 0.7.18", + "toml 0.8.23", + "tracing", + "tracing-subscriber", + "url", + "uuid 1.23.4", + "zeroize", +] + +[[package]] +name = "nym-serde-helpers" +version = "1.21.1" +dependencies = [ + "base64 0.22.1", + "bs58 0.5.1", + "hex", + "serde", + "time", +] + +[[package]] +name = "nym-service-provider-requests-common" +version = "1.21.1" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-service-providers-common" +version = "1.21.1" +dependencies = [ + "async-trait", + "log", + "nym-bin-common", + "nym-sphinx-anonymous-replies", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-socks5-client-core" +version = "1.21.1" +dependencies = [ + "anyhow", + "dirs 6.0.0", + "futures 0.3.32", + "log", + "nym-bandwidth-controller", + "nym-client-core", + "nym-config", + "nym-contracts-common", + "nym-credential-storage", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-service-providers-common", + "nym-socks5-proxy-helpers", + "nym-socks5-requests", + "nym-sphinx", + "nym-task", + "nym-validator-client", + "pin-project", + "rand 0.8.6", + "reqwest 0.13.4", + "schemars", + "serde", + "tap", + "thiserror 2.0.18", + "tokio 1.52.3", + "url", +] + +[[package]] +name = "nym-socks5-proxy-helpers" +version = "1.21.1" +dependencies = [ + "bytes 1.12.0", + "futures 0.3.32", + "log", + "nym-ordered-buffer", + "nym-socks5-requests", + "nym-task", + "tokio 1.52.3", + "tokio-util 0.7.18", +] + +[[package]] +name = "nym-socks5-requests" +version = "1.21.1" +dependencies = [ + "bincode", + "log", + "nym-exit-policy", + "nym-service-providers-common", + "nym-sphinx-addressing", + "serde", + "serde_json", + "tap", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-metrics", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-anonymous-replies", + "nym-sphinx-chunking", + "nym-sphinx-cover", + "nym-sphinx-forwarding", + "nym-sphinx-framing", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "rand_chacha 0.3.1", + "rand_distr", + "thiserror 2.0.18", + "tokio 1.52.3", + "tracing", +] + +[[package]] +name = "nym-sphinx-acknowledgements" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-pemstore", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "nym-sphinx-addressing" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-sphinx-types", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-anonymous-replies" +version = "1.21.1" +dependencies = [ + "bs58 0.5.1", + "nym-crypto", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "thiserror 2.0.18", + "tracing", + "wasm-bindgen", +] + +[[package]] +name = "nym-sphinx-chunking" +version = "1.21.1" +dependencies = [ + "dashmap", + "log", + "nym-crypto", + "nym-metrics", + "nym-sphinx-addressing", + "nym-sphinx-params", + "nym-sphinx-types", + "rand 0.8.6", + "serde", + "thiserror 2.0.18", + "utoipa", + "wasmtimer", +] + +[[package]] +name = "nym-sphinx-cover" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-chunking", + "nym-sphinx-forwarding", + "nym-sphinx-params", + "nym-sphinx-routing", + "nym-sphinx-types", + "nym-topology", + "rand 0.8.6", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-forwarding" +version = "1.21.1" +dependencies = [ + "nym-sphinx-addressing", + "nym-sphinx-anonymous-replies", + "nym-sphinx-params", + "nym-sphinx-types", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-framing" +version = "1.21.1" +dependencies = [ + "bytes 1.12.0", + "cfg-if 1.0.4", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-forwarding", + "nym-sphinx-params", + "nym-sphinx-types", + "thiserror 2.0.18", + "tokio-util 0.7.18", + "tracing", +] + +[[package]] +name = "nym-sphinx-params" +version = "1.21.1" +dependencies = [ + "nym-crypto", + "nym-sphinx-types", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-routing" +version = "1.21.1" +dependencies = [ + "nym-sphinx-addressing", + "nym-sphinx-types", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sphinx-types" +version = "1.21.1" +dependencies = [ + "nym-outfox", + "sphinx-packet", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-sqlx-pool-guard" +version = "1.21.1" +dependencies = [ + "proc_pidinfo", + "sqlx", + "tokio 1.52.3", + "tracing", + "windows 0.61.3", +] + +[[package]] +name = "nym-statistics-common" +version = "1.21.1" +dependencies = [ + "futures 0.3.32", + "log", + "nym-credentials-interface", + "nym-crypto", + "nym-metrics", + "nym-sphinx", + "nym-task", + "serde", + "serde_json", + "sha2 0.10.9", + "si-scale", + "strum 0.28.0", + "strum_macros 0.28.0", + "sysinfo 0.38.4", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "wasmtimer", +] + +[[package]] +name = "nym-task" +version = "1.21.1" +dependencies = [ + "cfg-if 1.0.4", + "futures 0.3.32", + "log", + "thiserror 2.0.18", + "tokio 1.52.3", + "tokio-util 0.7.18", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmtimer", +] + +[[package]] +name = "nym-ticketbooks-merkle" +version = "1.21.1" +dependencies = [ + "nym-credentials-interface", + "nym-serde-helpers", + "rs_merkle", + "schemars", + "serde", + "sha2 0.10.9", + "time", + "utoipa", +] + +[[package]] +name = "nym-topology" +version = "1.21.1" +dependencies = [ + "async-trait", + "nym-api-requests", + "nym-crypto", + "nym-mixnet-contract-common", + "nym-sphinx-addressing", + "nym-sphinx-types", + "rand 0.8.6", + "reqwest 0.13.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "tracing", +] + +[[package]] +name = "nym-upgrade-mode-check" +version = "1.21.1" +dependencies = [ + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.13.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-validator-client" +version = "1.21.1" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bip32", + "bip39", + "colored", + "cosmrs", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "cw3", + "cw4", + "eyre", + "flate2", + "futures 0.3.32", + "itertools 0.14.0", + "nym-api-requests", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-http-api-client", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-network-defaults", + "nym-network-monitors-contract-common", + "nym-node-families-contract-common", + "nym-performance-contract-common", + "nym-serde-helpers", + "nym-vesting-contract-common", + "prost", + "reqwest 0.13.4", + "serde", + "serde_json", + "sha2 0.10.9", + "tendermint-rpc", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "tracing", + "url", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-vesting-contract-common" +version = "1.21.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-contracts-common", + "nym-mixnet-contract-common", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "nym-wasm-utils" +version = "1.21.1" +dependencies = [ + "futures 0.3.32", + "getrandom 0.2.17", + "gloo-net", + "gloo-utils", + "js-sys", + "tungstenite 0.20.1", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "nym-wireguard-types" +version = "1.21.1" +dependencies = [ + "base64 0.22.1", + "nym-crypto", + "serde", + "thiserror 2.0.18", + "x25519-dalek 2.0.1", + "zeroize", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if 1.0.4", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.3", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api 0.4.14", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93f386bb233083c799e6e642a9d73db98c24a5deeb95ffc85bf281255dffc98" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "password-hash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1a5d4e9c205d2c1ae73b84aab6240e98218c0e72e63b50422cfb2d1ca952282" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "base64ct", + "crypto-mac 0.11.0", + "hmac 0.11.0", + "password-hash 0.2.1", + "sha2 0.9.9", +] + +[[package]] +name = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "peg" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +dependencies = [ + "peg-runtime", + "proc-macro2 1.0.106", + "quote 1.0.46", +] + +[[package]] +name = "peg-runtime" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" + +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.1", + "once_cell", + "regex", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.3", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.3", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug 0.3.1", + "universal-hash 0.4.0", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug 0.3.1", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "opaque-debug 0.3.1", + "universal-hash 0.5.1", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits 0.2.19", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc_pidinfo" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29492a7b48a00ab80202528e235d2f80a04ccff3747540b4ec6881f2f2bc42d1" +dependencies = [ + "libc", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if 1.0.4", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.12.5", + "protobuf", + "thiserror 2.0.18", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes 1.12.0", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes 1.12.0", + "cfg_aliases", + "pin-project-lite 0.2.17", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.2", + "rustls 0.23.41", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio 1.52.3", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "aws-lc-rs", + "bytes 1.12.0", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring 0.17.14", + "rustc-hash 2.1.2", + "rustls 0.23.41", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.4", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2 1.0.106", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits 0.2.19", + "rand 0.8.6", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_syscall" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +dependencies = [ + "base64 0.13.1", + "bytes 0.5.6", + "encoding_rs", + "futures-core", + "futures-util", + "http 0.2.12", + "http-body 0.3.1", + "hyper 0.13.10", + "hyper-rustls 0.21.0", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.17", + "rustls 0.18.1", + "serde", + "serde_urlencoded", + "tokio 0.2.25", + "tokio-rustls 0.14.1", + "tokio-socks 0.3.0", + "tokio-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.20.0", + "winreg 0.7.0", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes 1.12.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite 0.2.17", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio 1.52.3", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes 1.12.0", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "hyper 1.10.1", + "hyper-rustls 0.27.9", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite 0.2.17", + "quinn", + "rustls 0.23.41", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper 1.0.2", + "tokio 1.52.3", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle 2.6.1", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.4", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "ripemd160" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.1", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rs_merkle" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb09b49230ba22e8c676e7b75dfe2887dea8121f18b530ae0ba519ce442d2b21" +dependencies = [ + "sha2 0.10.9", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits 0.2.19", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature 2.2.0", + "spki 0.7.3", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "rust-embed-utils", + "syn 2.0.118", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "sha2 0.10.9", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +dependencies = [ + "base64 0.12.3", + "log", + "ring 0.16.20", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct 0.7.1", + "webpki 0.22.4", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct 0.7.1", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.41", + "rustls-native-certs 0.8.4", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "salsa20" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0fbb5f676da676c260ba276a8f43a8dc67cf02d1438423aeb1c677a7212686" +dependencies = [ + "cipher 0.3.0", +] + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73d6d7c6311ebdbd9184ad6c4447b2f36337e327bda107d3ba9e3c374f9d325" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.10.1", + "salsa20 0.9.0", + "sha2 0.10.9", +] + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash 0.5.0", + "pbkdf2 0.12.2", + "salsa20 0.10.2", + "sha2 0.10.9", +] + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array 0.14.7", + "pkcs8 0.10.2", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys 0.8.2", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand 0.8.6", + "secp256k1-sys 0.10.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.2", + "secp256k1-sys 0.11.0", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9182278ed645df3477a9c27bfee0621c621aa16f6972635f7f795dae3d81070f" +dependencies = [ + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05da0d153dd4595bdffd5099dc0e9ce425b205ee648eb93437ff7302af8c9a5" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa 1.0.18", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.18", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa 1.0.18", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug 0.3.1", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" +dependencies = [ + "block-buffer 0.7.3", + "byte-tools", + "digest 0.8.1", + "keccak 0.1.6", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "si-scale" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72e7cd0744e007e382ba320435f1ed1ecd709409b4ebd5cfbc843d77b25a8aa" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smolmix" +version = "1.21.1" +dependencies = [ + "futures 0.3.32", + "nym-ip-packet-requests", + "nym-sdk", + "smoltcp", + "thiserror 2.0.18", + "tokio 1.52.3", + "tokio-smoltcp", + "tracing", +] + +[[package]] +name = "smoltcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if 1.0.4", + "defmt 0.3.100", + "heapless", + "libc", + "log", + "managed", +] + +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2 0.10.6", + "chacha20poly1305 0.10.1", + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "rustc_version", + "sha2 0.10.9", + "subtle 2.6.1", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sphinx-packet" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e" +dependencies = [ + "aes 0.8.4", + "arrayref", + "blake2 0.8.1", + "bs58 0.5.1", + "byteorder", + "chacha", + "ctr", + "curve25519-dalek 4.1.3", + "digest 0.10.7", + "hkdf 0.12.4", + "hmac 0.12.1", + "lioness", + "rand 0.8.6", + "rand_distr", + "sha2 0.10.9", + "subtle 2.6.1", + "x25519-dalek 2.0.1", + "zeroize", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api 0.4.14", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes 1.12.0", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls 0.23.41", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "time", + "tokio 1.52.3", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.118", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2 1.0.106", + "quote 1.0.46", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.118", + "tokio 1.52.3", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "bytes 1.12.0", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf 0.12.4", + "hmac 0.12.1", + "itoa 1.0.18", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf 0.12.4", + "hmac 0.12.1", + "home", + "itoa 1.0.18", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "time", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", +] + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck 0.3.3", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "superboring" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad752f6ecf1cde1cd92cff4400137c5d92d376f78bb901d2807a35dba5872a7" +dependencies = [ + "aes-gcm", + "aes-keywrap", + "getrandom 0.2.17", + "hmac-sha256", + "hmac-sha512", + "ml-dsa", + "rand 0.8.6", + "rsa", +] + +[[package]] +name = "supercow" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171758edb47aa306a78dfa4ab9aeb5167405bd4e3dc2b64e88f6a84bbe98bd63" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "sysinfo" +version = "0.29.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +dependencies = [ + "cfg-if 1.0.4", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi 0.3.9", +] + +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.62.2", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendermint" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" +dependencies = [ + "bytes 1.12.0", + "digest 0.10.7", + "ed25519 2.2.3", + "ed25519-consensus", + "flex-error", + "futures 0.3.32", + "k256", + "num-traits 0.2.19", + "once_cell", + "prost", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.9", + "signature 2.2.0", + "subtle 2.6.1", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-config" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069d1791f9b02a596abcd26eb72003b2e9906c6169a60fa82ffc080dd3a43fda" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint", + "toml 0.8.23", + "url", +] + +[[package]] +name = "tendermint-proto" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" +dependencies = [ + "bytes 1.12.0", + "flex-error", + "prost", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-rpc" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e0569a4b4cc42ff00df5a665be2858a39ff79df4790b176f1cd0e169bc0fc2" +dependencies = [ + "async-trait", + "bytes 1.12.0", + "flex-error", + "futures 0.3.32", + "getrandom 0.2.17", + "peg", + "pin-project", + "rand 0.8.6", + "reqwest 0.11.27", + "semver", + "serde", + "serde_bytes", + "serde_json", + "subtle 2.6.1", + "subtle-encoding", + "tendermint", + "tendermint-config", + "tendermint-proto", + "thiserror 1.0.69", + "time", + "tokio 1.52.3", + "tracing", + "url", + "uuid 1.23.4", + "walkdir", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "thread-id" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2010d27add3f3240c1fef7959f46c814487b216baee662af53be645ba7831c07" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "timer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" +dependencies = [ + "chrono", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio 0.6.23", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite 0.1.12", + "signal-hook-registry", + "slab", + "tokio-macros 0.2.6", + "winapi 0.3.9", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes 1.12.0", + "libc", + "mio 1.2.1", + "parking_lot 0.12.5", + "pin-project-lite 0.2.17", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros 2.7.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" +dependencies = [ + "pin-project-lite 0.2.17", + "tokio 1.52.3", +] + +[[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "tokio-rustls" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" +dependencies = [ + "futures-core", + "rustls 0.18.1", + "tokio 0.2.25", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio 1.52.3", + "webpki 0.22.4", +] + +[[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 1.52.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.41", + "tokio 1.52.3", +] + +[[package]] +name = "tokio-smoltcp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f5d53da1c3095663a8900d86c2abb0ffe02d3f6aa86527b066148fcb33e65e" +dependencies = [ + "futures 0.3.32", + "parking_lot 0.12.5", + "pin-project-lite 0.2.17", + "smoltcp", + "tokio 1.52.3", + "tokio-util 0.7.18", +] + +[[package]] +name = "tokio-socks" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d611fd5d241872372d52a0a3d309c52d0b95a6a67671a6c8f7ab2c4a37fb2539" +dependencies = [ + "bytes 0.4.12", + "either", + "futures 0.3.32", + "thiserror 1.0.69", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio 1.52.3", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.17", + "tokio 1.52.3", + "tokio-util 0.7.18", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.12", + "tokio 1.52.3", + "tokio-rustls 0.24.1", + "tungstenite 0.20.1", + "webpki-roots 0.25.4", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.41", + "rustls-pki-types", + "tokio 1.52.3", + "tokio-rustls 0.26.4", + "tungstenite 0.26.2", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.12", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes 1.12.0", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite 0.2.17", + "slab", + "tokio 1.52.3", +] + +[[package]] +name = "tokio_with_wasm" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e40fbbbd95441133fe9483f522db15dbfd26dc636164ebd8f2dd28759a6aa6" +dependencies = [ + "js-sys", + "tokio 1.52.3", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01145a2c788d6aae4cd653afec1e8332534d7d783d01897cefcafe4428de992" +dependencies = [ + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite 0.2.17", + "sync_wrapper 1.0.2", + "tokio 1.52.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags 2.13.0", + "bytes 1.12.0", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite 0.2.17", + "tokio 1.52.3", + "tokio-util 0.7.18", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite 0.2.17", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" +dependencies = [ + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes 1.12.0", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.6", + "rustls 0.21.12", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", + "webpki-roots 0.24.0", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes 1.12.0", + "data-encoding", + "http 1.4.2", + "httparse", + "log", + "rand 0.9.2", + "rustls 0.23.41", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.2", +] + +[[package]] +name = "typemap-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +dependencies = [ + "unsafe-any-ors", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array 0.14.7", + "subtle 2.6.1", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle 2.6.1", +] + +[[package]] +name = "unsafe-any-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +dependencies = [ + "destructure_traitobject", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.17", + "serde", +] + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +dependencies = [ + "anyhow", + "cargo_metadata 0.18.1", + "cfg-if 1.0.4", + "regex", + "rustc_version", + "rustversion", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasix" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae86f02046da16a333a9129d31451423e1657737ecdafed4193838a5f54c5cfe" +dependencies = [ + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if 1.0.4", + "once_cell", + "rustversion", + "serde", + "serde_json", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote 1.0.46", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures 0.3.32", + "js-sys", + "parking_lot 0.12.5", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +dependencies = [ + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" +dependencies = [ + "rustls-webpki 0.101.7", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if 1.0.4", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "x25519-dalek" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" +dependencies = [ + "curve25519-dalek 2.1.3", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "crc32fast", + "thiserror 1.0.69", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..29fe987 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[workspace] +resolver = "2" +members = [ + "crates/gp-wallet", + "crates/gp-goblin-sender", + "crates/gp-nostr", + "crates/gp-core", + "crates/gp-server", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.8", default-features = false, features = [ + "runtime-tokio-rustls", + "sqlite", + "migrate", + "macros", +] } +tokio = { version = "1", default-features = false } + +[profile.release] +opt-level = 2 +strip = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..6045c68 --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +# GoblinPay + +A self-hostable, receive-only Grin payment server. A merchant runs it, a +customer pays from Goblin Wallet by scanning a QR code, and the payment +travels as a gift-wrapped slatepack over Nostr (optionally over the Nym +mixnet). GoblinPay auto-receives, returns the S2 reply so the payer can +finalize, confirms the transaction on chain, and signals paid. + +**Status: milestone 6 (invoices, hosted checkout, per-user endpubs, +notifications).** On top of the milestone 2-4 wallet + transport + confirmation +path, GoblinPay now carries the full merchant surface: + +- **Invoices + matching (M5):** create an invoice against an order, matched by + any of three modes (per-invoice override or the `GP_MATCH_MODE` default): + the payer's memo, a per-invoice derived Nostr identity (a stateless child of + the server nsec, recommended for stores), or an exact amount. The matcher + runs inside the ingest pipeline, so a gift-wrapped payment resolves to its + invoice automatically. +- **Hosted checkout (M5):** a zero-JS `/pay/` page (server-rendered + Askama + one CSS file + a server-generated QR SVG at ECC level H with an + optional Goblin-mark center logo), live status via ``, + and a manual slatepack fallback (paste S1 -> offline `receive_tx` -> copy the + S2 back) on every page. The same renderer serves embedded and hosted use. +- **Per-user endpubs (M5b):** an admin assigns one receiving identity per user + (a derived child keyed by `(user_id, epoch)`; only public keys and the + rotation clock are stored, never private keys), with optional rolling rotation + and an overlap window so a just-rotated endpub still lands. All funds still + land in the one Grin wallet. +- **Notifications (M6, all optional):** an HMAC-signed, idempotent, retried + HTTP webhook (the WooCommerce contract), an authenticated admin dashboard + + JSON API, and NIP-17 DMs to the merchant / payer. + +All relay traffic rides an in-process Nym mixnet tunnel (smolmix, auto-selected +exit, mix-dns; `GP_NYM=off` is a debugging escape hatch only). Encryption +negotiates NIP-44 v3 (the NIP-17 extension, via the companion `nip44` crate) per +recipient, with v2 as the mandatory baseline. Store connectors and the +conversion oracle arrive in later milestones; comprehensive documentation lands +at milestone 11. + +## Workspace + +| Crate | Purpose | +|---|---| +| `crates/gp-wallet` | Grin wallet handoff: open from mnemonic, S1 -> `receive_tx` -> S2 (offline) | +| `crates/gp-goblin-sender` | Test-only gate helper: sends and finalizes with Goblin's wallet stack | +| `crates/gp-nostr` | Nostr transport: identity, gift wrap (NIP-44 v2/v3), ingest, Nym mixnet | +| `crates/gp-core` | Domain core: config, SQLite persistence (sqlx, raw SQL) | +| `crates/gp-server` | Actix-Web binary: routes, Askama templates, rustls TLS | + +Supporting directories: `migrations/` (raw sqlx SQL), `templates/` (Askama, +zero JS), `static/` (one hand-written CSS file, no build step). + +## Configuration + +Everything is environment variables, defaults are safe for local use. + +| Variable | Default | Meaning | +|---|---|---| +| `GP_BIND` | `127.0.0.1:8080` | Listen address | +| `GP_TLS` | `off` | `off` (plain HTTP) or `rustls` (in-process TLS) | +| `GP_TLS_CERT` | unset | PEM certificate chain path, required for `rustls` | +| `GP_TLS_KEY` | unset | PEM private key path, required for `rustls` | +| `GP_DB_PATH` | `./goblinpay.db` | SQLite file, created on first start | +| `GP_DATA_DIR` | `./gp-data` | Data directory (wallet files, encrypted seed) | +| `GP_NODE_URL` | `https://main.gri.mw` | External Grin node (read only) | +| `GP_CHAIN` | `mainnet` | Grin network: `mainnet` or `testnet` | +| `GP_RELAY_MODE` | `bundled` | `bundled` or `external` | +| `GP_RELAYS` | unset | Comma-separated relay URLs | +| `GP_NYM` | `on` | Route Nostr traffic over the Nym mixnet (`on` or `off`) | +| `GP_INGEST` | `on` | Nostr ingest service (`off` = HTTP surface only, for debugging) | +| `GP_MATCH_MODE` | `memo` | Default matching mode: `memo`, `derived`, `amount` | +| `GP_MNEMONIC` | unset | Grin seed mnemonic (money secret) | +| `GP_WALLET_PASSWORD` | unset | Password encrypting the wallet seed and the Nostr identity at rest | +| `GP_NSEC` | unset | Nostr identity key (payment identity secret) | +| `GP_NCRYPTSEC` | unset | NIP-49 encrypted identity key (unlocked with the wallet password) | +| `GP_PUBLIC_URL` | `http://` | Public base URL for the hosted `/pay/` links | +| `GP_API_TOKEN` | unset | Bearer token for the connector/create-invoice API (unset = write API closed) | +| `GP_ADMIN_TOKEN` | unset | Bearer token for the admin dashboard + endpub/webhook API | +| `GP_WEBHOOK_URL` | unset | Webhook endpoint for payment events (requires `GP_WEBHOOK_SECRET`) | +| `GP_WEBHOOK_SECRET` | unset | HMAC-SHA256 secret for signing webhooks | +| `GP_QR_LOGO` | Goblin mark | Checkout QR center logo: unset = Goblin mark, `off`/`none` = plain, else a URL/path | +| `GP_MERCHANT_NPUB` | unset | Merchant npub for the NIP-17 confirmed-payment DM | +| `GP_NOTIFY_MERCHANT_DM` | `off` | Send a NIP-17 DM to the merchant on a received payment | +| `GP_NOTIFY_PAYER_RECEIPT` | `off` | Send a NIP-17 receipt DM to the payer | +| `GP_ENDPUB_ROTATE_INTERVAL` | `0` | Default per-user endpub rotation interval in seconds (0 = off) | +| `GP_ENDPUB_OVERLAP_EPOCHS` | `1` | Past epochs kept watched after a rotation | +| `GP_RATE_SOURCE` | `coingecko` | Conversion-rate oracle source for pricing fiat invoices | +| `GP_RATE_CURRENCIES` | `usd` | Comma-separated fiat currencies the oracle prices (ISO codes) | +| `GP_RATE_CACHE_TTL` | `60` | Seconds a fetched rate is reused before refetching (0 = always) | +| `GP_QUOTE_TTL` | `900` | Seconds a created fiat invoice locks its Grin quote (its expiry window) | +| `GP_RATE_STALE_MAX` | `0` | Bounded stale-rate fallback in seconds if a live fetch fails (0 = off) | + +### Conversion rates (optional) + +A store that prices in fiat (for example cryptodrip.com prices in USD) sends +`amount_fiat` + `currency` to `POST /invoice`. GoblinPay then quotes the Grin +amount through the configured oracle, locks it for `GP_QUOTE_TTL` seconds, and +fills the invoice `expected_amount` so the invoice matches by amount. A +Grin-denominated invoice (`amount_grin`) bypasses the oracle unchanged. + +The oracle default is CoinGecko (GRIN is listed under id `grin`), queried at +`api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies=`. +Rates are cached for `GP_RATE_CACHE_TTL` seconds so concurrent checkouts do not +hammer the source. If the source is unreachable or the currency is not enabled, +`create-invoice` fails fast with a clear error rather than creating an +unpriceable invoice; `GP_RATE_STALE_MAX` optionally permits serving the last +cached rate within a bounded window instead. The oracle fetch goes DIRECT over +normal HTTP, never through the Nym mixnet (the mixnet carries only the Nostr +gift-wrap layer, the same ruling as the read-only node client). + +The secrets also accept mounted-file variants, `GP_MNEMONIC_FILE`, +`GP_WALLET_PASSWORD_FILE`, `GP_NSEC_FILE`, and `GP_NCRYPTSEC_FILE` +(mode 0400 recommended). Setting both the variable and its `_FILE` variant +is an error, as is setting both `GP_NSEC` and `GP_NCRYPTSEC`. When neither +identity variable is set, a fresh random identity is generated on first +start and persisted NIP-49 encrypted at `/nostr/identity.json` +(mode 0600). The mnemonic and the nsec are deliberately independent secrets: +the mnemonic recovers the funds, the nsec recovers the payment identity, and +the Grin seed is never used for anything Nostr. + +## REST API + +Public (no auth): `/health`, and the token-as-capability routes below. Bearer +auth (`Authorization: Bearer `) where noted; the `_FILE` mounted-file +variant works for `GP_API_TOKEN`, `GP_ADMIN_TOKEN`, and `GP_WEBHOOK_SECRET` too. + +| Method | Route | Auth | Purpose | +|---|---|---|---| +| GET | `/health` | none | Liveness + version | +| POST | `/invoice` | api | Create an invoice, returns checkout info (pay_url, nprofile, QR SVG) | +| GET | `/invoice/{id}` | api | Invoice checkout info + status | +| GET | `/pay/{token}` | token | Hosted zero-JS checkout page | +| GET | `/pay/{token}/status` | token | Invoice status JSON (for polling) | +| POST | `/pay/{token}/slatepack` | token | Manual fallback: paste S1, returns the S2 page | +| GET | `/payment/{id}` | token | Payment status JSON | +| GET | `/payment/{id}/receipt` | token | Server-signed verifiable receipt (M4) | +| GET | `/admin` | admin | Dashboard (payments, balances, config) | +| GET | `/admin/payments` | admin | Recent payments JSON | +| GET/POST | `/admin/users` | admin | List users / create a user + endpub | +| GET | `/admin/users/{id}` | admin | A user's current endpub + QR | +| POST | `/admin/users/{id}/rotate` | admin | Force-rotate a user's endpub | +| POST | `/admin/users/{id}/rotate-interval` | admin | Set the per-user rotation interval | +| GET | `/admin/webhooks` | admin | Webhook delivery log | + +`POST /invoice` body: `{ order_ref?, amount_grin? | (amount_fiat + currency), memo?, match_mode?, expiry_secs? }`. + +## Webhook contract + +On a received payment, GoblinPay POSTs `application/json` to `GP_WEBHOOK_URL`: + +```json +{ + "event_id": "5f3c...", // 128-bit hex, the idempotency key + "event_type": "payment.received", + "created_at": "2026-07-01T12:00:00Z", + "payment": { + "slate_id": "...", "amount": 2000000000, "amount_grin": "2", + "status": "received", "payer": "...hex...", "confirmed_height": null + }, + "invoice_id": "...", "order_ref": "order-42", "user_id": "..." +} +``` + +Headers: `X-GoblinPay-Signature: sha256=` +and `X-GoblinPay-Delivery: `. Verify by recomputing the HMAC over the +exact received bytes (constant-time) and dedupe on the delivery id. Deliveries +are persisted and retried with exponential backoff. + +## Run + +``` +cargo run -p gp-server +curl http://127.0.0.1:8080/health +``` + +## Develop + +``` +./ci.sh # cargo fmt --check, clippy -D warnings, tests +``` + +## Credits + +GoblinPay is developed with the help of Claude (Anthropic). diff --git a/ci.sh b/ci.sh new file mode 100755 index 0000000..1dcde78 --- /dev/null +++ b/ci.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +# CI gate: formatting, lints (warnings are errors), tests. +set -eu + +cargo fmt --all -- --check +cargo clippy --workspace -- -D warnings +cargo test --workspace diff --git a/connectors/woocommerce/INSTALL.md b/connectors/woocommerce/INSTALL.md new file mode 100644 index 0000000..d5966d4 --- /dev/null +++ b/connectors/woocommerce/INSTALL.md @@ -0,0 +1,86 @@ +# Installing GoblinPay for WooCommerce + +## 1. Package the plugin + +Zip the plugin directory so the archive contains a single top-level folder named +`goblinpay-woocommerce` with the plugin files inside it: + +``` +cd connectors +zip -r goblinpay-woocommerce.zip woocommerce \ + -x '*/.git/*' +``` + +If you prefer the folder name to match the plugin, rename `woocommerce` to +`goblinpay-woocommerce` before zipping. WordPress does not require the folder +name to match; it reads the plugin header from `goblinpay-woocommerce.php`. + +## 2. Upload and activate + +In WordPress, open Plugins, then Add New Plugin, then Upload Plugin. Choose the +zip, install it, and activate. WooCommerce 8.0 or newer must already be active. + +Alternatively, copy the `woocommerce` folder into +`wp-content/plugins/goblinpay-woocommerce/` on the server and activate from the +Plugins screen. + +## 3. Configure the gateway + +Open WooCommerce, then Settings, then Payments, then GoblinPay (Grin), and set: + +- GoblinPay URL: the base URL of your GoblinPay server, for example + `http://127.0.0.1:8192` when GoblinPay runs on the same host. No trailing + slash. +- API Token: the GoblinPay create-invoice bearer token (`GP_API_TOKEN`). +- Webhook Secret: the shared HMAC secret (`GP_WEBHOOK_SECRET`). +- Matching mode: leave on Per-invoice identity (recommended) unless you have a + reason to match by order reference or amount. +- Checkout experience: Redirect (recommended) or Embed the QR on the + order-received page. +- Payment window: minutes before an unpaid order is cancelled (0 disables it). + +Enable the method and save. + +## 4. Register the webhook in GoblinPay + +Point your GoblinPay server at this site so it can report payments. Set these on +the GoblinPay side: + +- `GP_WEBHOOK_URL` = `https://YOUR-SITE/wp-json/goblinpay/v1/webhook` +- `GP_WEBHOOK_SECRET` = the same secret you entered in the gateway settings. +- `GP_API_TOKEN` = the same token you entered as the API Token. + +GoblinPay signs each delivery with `X-GoblinPay-Signature: sha256=` over +the raw body and sends an idempotency key in `X-GoblinPay-Delivery`. The plugin +verifies the signature, dedupes on the event id, and completes the matching +order. + +The exact POST target the plugin exposes (the value to use for +`GP_WEBHOOK_URL`) is: + +``` +https://YOUR-SITE/wp-json/goblinpay/v1/webhook +``` + +Make sure the WordPress REST API is reachable from the GoblinPay host. If the +webhook is ever missed, the plugin also polls +`GET {GoblinPay URL}/invoice/{invoice_id}` (with the bearer token) as a +fallback. + +## 5. Test + +Place a test order, choose Grin (GRIN), and confirm: + +- Redirect mode sends you to the GoblinPay `/pay/` page. +- Embed mode shows the Goblin QR on the order-received page. +- Paying from a Goblin Wallet moves the order to processing or completed once + GoblinPay delivers the `payment.received` webhook. + +Turn on Debug logging in the gateway settings to trace requests and webhooks in +WooCommerce, then Status, then Logs, source `goblinpay`. + +## Refund caveat + +Refunds are not automated. GoblinPay is receive-only and never sends Grin, so +any refund is a manual Grin send performed by the merchant from a wallet under +their control. diff --git a/connectors/woocommerce/README.md b/connectors/woocommerce/README.md new file mode 100644 index 0000000..59f5738 --- /dev/null +++ b/connectors/woocommerce/README.md @@ -0,0 +1,75 @@ +# GoblinPay for WooCommerce + +Accept Grin (GRIN / MimbleWimble) payments in WooCommerce through a self-hosted +GoblinPay server. The customer pays from their Goblin Wallet by scanning an +`nprofile` QR code. The payment travels as a gift-wrapped slatepack over Nostr +(optionally over the Nym mixnet). GoblinPay receives it, returns the reply +slatepack to the payer, watches the chain to confirm, and notifies WooCommerce. + +This plugin is a thin client. All of the Grin and Nostr work happens in +GoblinPay; WooCommerce only talks HTTP to your GoblinPay instance. No BTCPay, no +node exposed to the store, no wallet RPC. + +## What it does + +- Adds a "Grin (GRIN)" payment method to both the classic checkout and the + WooCommerce Blocks (Cart/Checkout block) checkout. +- On checkout, calls GoblinPay to create an invoice for the order, then either + redirects the customer to GoblinPay's hosted `/pay/` page (the default) + or shows the Goblin QR on the order-received page (the embedded option). +- Marks the order complete when GoblinPay reports the payment, via a signed + webhook. If a webhook is missed, the plugin polls GoblinPay for the invoice + status as a fallback. +- Declares HPOS (custom order tables) and Cart/Checkout Blocks compatibility. + +## Requirements + +- WordPress with WooCommerce 8.0 or newer (tested against WooCommerce 10.8). +- PHP 8.0 or newer (target host runs PHP 8.2). +- A running GoblinPay server reachable from the WordPress host. + +## Settings + +Open WooCommerce, then Settings, then Payments, then GoblinPay (Grin). + +- GoblinPay URL: base URL of your GoblinPay server, for example + `http://127.0.0.1:8192`. No trailing slash. +- API Token: the GoblinPay create-invoice bearer token (`GP_API_TOKEN` on the + server). +- Webhook Secret: the shared HMAC secret (`GP_WEBHOOK_SECRET` on the server). +- Matching mode: how GoblinPay ties an incoming payment to the order. The + default, per-invoice identity, gives each order its own QR and is the most + reliable. Order reference (memo) and amount-only are also available. +- Checkout experience: redirect to the hosted GoblinPay checkout (the default), + or embed the QR on the order-received page. +- Payment window: minutes before an unpaid order is cancelled. Set 0 to disable. + +Point your GoblinPay server's `GP_WEBHOOK_URL` at this site's webhook endpoint, +shown in the Webhook Secret field, which is: + +``` +https://YOUR-SITE/wp-json/goblinpay/v1/webhook +``` + +## Refunds + +Refunds are not automated. GoblinPay is receive-only: it never sends Grin. A +refund is therefore a manual, out-of-band Grin send by the merchant from a +wallet under their control. This plugin marks refunds as unsupported for that +reason, the same caveat the Grin BTCPay connector carries. + +## Security notes + +- The webhook is authenticated by an HMAC-SHA256 signature over the exact raw + request body, compared in constant time (`hash_equals`). A bad or missing + signature is rejected with HTTP 401. +- Webhook deliveries are deduplicated on their event id, and order completion is + idempotent, so a retried or duplicated delivery is a no-op. +- The QR SVG rendered on the order-received page is passed through a strict + `wp_kses` allowlist (svg, g, rect, path, image, title), so a compromised or + misconfigured endpoint cannot inject script. +- Secrets live in the gateway settings, never in code. + +## Credit + +Built by Claude (Anthropic) for the Goblin project. diff --git a/connectors/woocommerce/assets/js/blocks.js b/connectors/woocommerce/assets/js/blocks.js new file mode 100644 index 0000000..426226a --- /dev/null +++ b/connectors/woocommerce/assets/js/blocks.js @@ -0,0 +1,41 @@ +/* global window */ +/** + * WooCommerce Blocks (Checkout/Cart block) integration for the GoblinPay + * payment gateway. No build step: uses the globals WooCommerce Blocks exposes + * (wc-blocks-registry, wc-settings, wp-element, wp-html-entities). This is a + * redirect/on-site gateway: the block submits to the Store API, which runs the + * server-side process_payment() and follows the returned redirect (to the + * hosted GoblinPay /pay page, or the order-received page for the embedded QR). + */ +( function () { + 'use strict'; + + if ( ! window.wc || ! window.wc.wcBlocksRegistry || ! window.wp || ! window.wp.element ) { + return; + } + + var registerPaymentMethod = window.wc.wcBlocksRegistry.registerPaymentMethod; + var getSetting = window.wc.wcSettings.getSetting; + var createElement = window.wp.element.createElement; + var decodeEntities = ( window.wp.htmlEntities && window.wp.htmlEntities.decodeEntities ) || function ( s ) { return s; }; + + var data = getSetting( 'goblinpay_data', {} ); + var title = decodeEntities( data.title || 'Pay with Grin (GRIN)' ); + var description = decodeEntities( data.description || '' ); + + var Content = function () { + return createElement( 'div', { className: 'goblinpay-blocks-description' }, description ); + }; + + registerPaymentMethod( { + name: 'goblinpay', + label: createElement( 'span', null, title ), + content: createElement( Content, null ), + edit: createElement( Content, null ), + canMakePayment: function () { return true; }, + ariaLabel: title, + supports: { + features: data.supports || [ 'products' ], + }, + } ); +} )(); diff --git a/connectors/woocommerce/goblinpay-woocommerce.php b/connectors/woocommerce/goblinpay-woocommerce.php new file mode 100644 index 0000000..26846e3 --- /dev/null +++ b/connectors/woocommerce/goblinpay-woocommerce.php @@ -0,0 +1,516 @@ + + * { order_ref, amount_fiat, currency, memo, match_mode, expiry_secs } + * -> { invoice_id, token, pay_url, nprofile, npub, qr_svg, amount, status, ... } + * and receives payment events at /wp-json/goblinpay/v1/webhook + * (HMAC-SHA256 over the raw body, header "X-GoblinPay-Signature: sha256=", + * idempotency key in "X-GoblinPay-Delivery: "). + * + * Refunds are NOT automated: GoblinPay is receive-only (it never sends), so a + * refund is a manual, out-of-band Grin send by the merchant. See README.md. + * + * @package GoblinPayWooCommerce + */ + +if (!defined('ABSPATH')) { + exit; +} + +define('GOBLINPAY_WC_VERSION', '1.0.0'); +define('GOBLINPAY_WC_PLUGIN_FILE', __FILE__); +define('GOBLINPAY_WC_WH_NS', 'goblinpay/v1'); // keep stable: this is the webhook URL registered in GoblinPay +define('GOBLINPAY_WC_GATEWAY_ID', 'goblinpay'); // keep stable: ties to the saved settings option +define('GOBLINPAY_WC_EXPIRE_HOOK', 'goblinpay_wc_expire_check'); +define('GOBLINPAY_WC_POLL_HOOK', 'goblinpay_wc_poll_check'); + +/* HPOS (custom order tables) + Cart/Checkout Blocks compatibility. */ +add_action('before_woocommerce_init', function () { + if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true); + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('cart_checkout_blocks', __FILE__, true); + } +}); + +/* Block checkout payment integration (Woo Blocks, merged into WC core). */ +add_action('woocommerce_blocks_payment_method_type_registration', function ($registry) { + if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) { + require_once __DIR__ . '/includes/class-blocks.php'; + $registry->register(new GoblinPay_WC_Blocks_Support()); + } +}); + +/* Register the gateway. */ +add_filter('woocommerce_payment_gateways', function ($gateways) { + $gateways[] = 'WC_Gateway_GoblinPay'; + return $gateways; +}); + +/* Settings link on the Plugins list. */ +add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { + $url = admin_url('admin.php?page=wc-settings&tab=checkout§ion=' . GOBLINPAY_WC_GATEWAY_ID); + array_unshift($links, '' . esc_html__('Settings', 'goblinpay-woocommerce') . ''); + return $links; +}); + +add_action('plugins_loaded', function () { + if (!class_exists('WC_Payment_Gateway')) { + return; + } + + class WC_Gateway_GoblinPay extends WC_Payment_Gateway { + + public function __construct() { + $this->id = GOBLINPAY_WC_GATEWAY_ID; + $this->method_title = __('GoblinPay (Grin)', 'goblinpay-woocommerce'); + $this->method_description = __('Accept Grin (GRIN) payments through a self-hosted GoblinPay server. Customers pay from their Goblin Wallet.', 'goblinpay-woocommerce'); + $this->has_fields = false; + $this->supports = array('products'); + + $this->init_form_fields(); + $this->init_settings(); + + $this->title = $this->get_option('title', __('Grin (GRIN)', 'goblinpay-woocommerce')); + $this->description = $this->get_option('description'); + $this->enabled = $this->get_option('enabled', 'no'); + + add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options')); + add_action('woocommerce_thankyou_' . $this->id, array($this, 'thankyou_page')); + } + + public function init_form_fields() { + $webhook_url = esc_html(rest_url(GOBLINPAY_WC_WH_NS . '/webhook')); + $this->form_fields = array( + 'enabled' => array( + 'title' => __('Enable/Disable', 'goblinpay-woocommerce'), + 'type' => 'checkbox', + 'label' => __('Enable Grin payments via GoblinPay', 'goblinpay-woocommerce'), + 'default' => 'no', + ), + 'title' => array( + 'title' => __('Title', 'goblinpay-woocommerce'), + 'type' => 'text', + 'default' => __('Grin (GRIN)', 'goblinpay-woocommerce'), + 'desc_tip' => true, + 'description' => __('Payment method title shown at checkout.', 'goblinpay-woocommerce'), + ), + 'description' => array( + 'title' => __('Description', 'goblinpay-woocommerce'), + 'type' => 'textarea', + 'default' => __('Pay with Grin from your Goblin Wallet. You will be shown a QR code (or redirected to a secure checkout) to complete the payment.', 'goblinpay-woocommerce'), + ), + 'gp_url' => array( + 'title' => __('GoblinPay URL', 'goblinpay-woocommerce'), + 'type' => 'text', + 'default' => 'http://127.0.0.1:8192', + 'placeholder' => 'http://127.0.0.1:8192', + 'desc_tip' => true, + 'description' => __('Base URL of your GoblinPay server (no trailing slash).', 'goblinpay-woocommerce'), + ), + 'api_token' => array( + 'title' => __('API Token', 'goblinpay-woocommerce'), + 'type' => 'password', + 'desc_tip' => true, + 'description' => __('Bearer token for the GoblinPay create-invoice API (GP_API_TOKEN on the server).', 'goblinpay-woocommerce'), + ), + 'webhook_secret' => array( + 'title' => __('Webhook Secret', 'goblinpay-woocommerce'), + 'type' => 'password', + 'description' => sprintf( + /* translators: %s: webhook URL */ + __('Shared HMAC secret (GP_WEBHOOK_SECRET on the server). Set GoblinPay\'s GP_WEBHOOK_URL to: %s', 'goblinpay-woocommerce'), + '' . $webhook_url . '' + ), + ), + 'match_mode' => array( + 'title' => __('Matching mode', 'goblinpay-woocommerce'), + 'type' => 'select', + 'default' => 'derived', + 'options' => array( + 'derived' => __('Per-invoice identity (recommended)', 'goblinpay-woocommerce'), + 'memo' => __('Order reference (memo)', 'goblinpay-woocommerce'), + 'amount' => __('Amount only', 'goblinpay-woocommerce'), + '' => __('Server default', 'goblinpay-woocommerce'), + ), + 'desc_tip' => true, + 'description' => __('How GoblinPay matches an incoming payment to this order. Per-invoice identity gives each order its own QR and is the most reliable.', 'goblinpay-woocommerce'), + ), + 'checkout_ux' => array( + 'title' => __('Checkout experience', 'goblinpay-woocommerce'), + 'type' => 'select', + 'default' => 'redirect', + 'options' => array( + 'redirect' => __('Redirect to the hosted GoblinPay checkout (recommended)', 'goblinpay-woocommerce'), + 'embed' => __('Show the QR on the order-received page', 'goblinpay-woocommerce'), + ), + 'desc_tip' => true, + 'description' => __('Redirect sends the customer to GoblinPay\'s /pay page. Embed keeps them on your site and shows the Goblin QR on the order-received page.', 'goblinpay-woocommerce'), + ), + 'payment_window' => array( + 'title' => __('Payment window (minutes)', 'goblinpay-woocommerce'), + 'type' => 'number', + 'default' => '1440', + 'desc_tip' => true, + 'description' => __('If still unpaid after this many minutes, the order is cancelled. Set 0 to disable.', 'goblinpay-woocommerce'), + ), + 'debug' => array( + 'title' => __('Debug logging', 'goblinpay-woocommerce'), + 'type' => 'checkbox', + 'label' => __('Log requests/webhooks (WooCommerce -> Status -> Logs, source "goblinpay")', 'goblinpay-woocommerce'), + 'default' => 'no', + ), + ); + } + + private function log($msg) { + if ('yes' === $this->get_option('debug', 'no') && function_exists('wc_get_logger')) { + wc_get_logger()->info(is_string($msg) ? $msg : wp_json_encode($msg), array('source' => 'goblinpay')); + } + } + + public function process_payment($order_id) { + $order = wc_get_order($order_id); + $gp_url = rtrim((string) $this->get_option('gp_url'), '/'); + $token = trim((string) $this->get_option('api_token')); + + if (!$order || '' === $gp_url || '' === $token) { + wc_add_notice(__('Grin payments are not fully configured.', 'goblinpay-woocommerce'), 'error'); + return array('result' => 'failure'); + } + + $window = (int) $this->get_option('payment_window', 1440); + $mode = (string) $this->get_option('match_mode', 'derived'); + + $payload = array( + 'order_ref' => (string) $order->get_id(), + 'amount_fiat' => (string) $order->get_total(), + 'currency' => $order->get_currency(), + 'memo' => sprintf( + /* translators: 1: order number, 2: site name */ + __('Order %1$s at %2$s', 'goblinpay-woocommerce'), + $order->get_order_number(), + wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES) + ), + ); + if ('' !== $mode) { + $payload['match_mode'] = $mode; + } + if ($window > 0) { + $payload['expiry_secs'] = $window * 60; + } + $this->log(array('create_invoice' => $gp_url . '/invoice', 'payload' => $payload)); + + $resp = wp_remote_post($gp_url . '/invoice', array( + 'timeout' => 30, + 'headers' => array( + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $token, + ), + 'body' => wp_json_encode($payload), + )); + if (is_wp_error($resp)) { + $this->log(array('create_invoice_error' => $resp->get_error_message())); + wc_add_notice(__('Could not reach the GoblinPay server. Please try again.', 'goblinpay-woocommerce'), 'error'); + return array('result' => 'failure'); + } + + $code = wp_remote_retrieve_response_code($resp); + $body = json_decode(wp_remote_retrieve_body($resp), true); + if ($code < 200 || $code >= 300 || !is_array($body) || empty($body['invoice_id']) || empty($body['pay_url'])) { + $err = (is_array($body) && isset($body['error'])) ? $body['error'] : ('HTTP ' . $code); + $this->log(array('create_invoice_bad_response' => $code, 'body' => $body)); + wc_add_notice( + sprintf( + /* translators: %s: error message */ + __('Grin payment could not be started: %s', 'goblinpay-woocommerce'), + esc_html((string) $err) + ), + 'error' + ); + return array('result' => 'failure'); + } + + // Persist the checkout details for the order-received page and reconciliation. + $order->update_meta_data('_goblinpay_invoice_id', sanitize_text_field((string) $body['invoice_id'])); + $order->update_meta_data('_goblinpay_pay_url', esc_url_raw((string) $body['pay_url'])); + if (!empty($body['token'])) { + $order->update_meta_data('_goblinpay_token', sanitize_text_field((string) $body['token'])); + } + if (!empty($body['nprofile'])) { + $order->update_meta_data('_goblinpay_nprofile', sanitize_text_field((string) $body['nprofile'])); + } + if (!empty($body['amount'])) { + $order->update_meta_data('_goblinpay_amount', sanitize_text_field((string) $body['amount'])); + } + if (!empty($body['qr_svg'])) { + // Sanitised on output; store the raw SVG returned by our own GoblinPay. + $order->update_meta_data('_goblinpay_qr_svg', (string) $body['qr_svg']); + } + + // Awaiting payment -> on-hold (reserves stock; avoids WooCommerce's + // default unpaid-order auto-cancel that would kill slow crypto payments). + $order->update_status('on-hold', sprintf( + /* translators: %s: GoblinPay invoice id */ + __('Awaiting Grin payment (GoblinPay invoice %s).', 'goblinpay-woocommerce'), + sanitize_text_field((string) $body['invoice_id']) + )); + $order->save(); + + // Webhook-miss safety net: poll the invoice once, mid-window. + wp_schedule_single_event(time() + 5 * MINUTE_IN_SECONDS, GOBLINPAY_WC_POLL_HOOK, array($order->get_id())); + // Expiry fallback. + if ($window > 0) { + wp_schedule_single_event(time() + $window * MINUTE_IN_SECONDS, GOBLINPAY_WC_EXPIRE_HOOK, array($order->get_id())); + } + + if (function_exists('WC') && WC()->cart) { + WC()->cart->empty_cart(); + } + + $ux = (string) $this->get_option('checkout_ux', 'redirect'); + if ('embed' === $ux) { + // Stay on-site; the QR renders on the order-received page. + return array('result' => 'success', 'redirect' => $this->get_return_url($order)); + } + return array('result' => 'success', 'redirect' => esc_url_raw((string) $body['pay_url'])); + } + + /** Render the Goblin QR + nprofile panel on the order-received page (embed UX). */ + public function thankyou_page($order_id) { + if ('embed' !== (string) $this->get_option('checkout_ux', 'redirect')) { + return; + } + $order = wc_get_order($order_id); + if (!$order || $order->get_payment_method() !== GOBLINPAY_WC_GATEWAY_ID) { + return; + } + if ($order->is_paid()) { + echo '

' + . esc_html__('Grin payment received. Thank you!', 'goblinpay-woocommerce') + . '

'; + return; + } + + $qr = (string) $order->get_meta('_goblinpay_qr_svg'); + $nprofile = (string) $order->get_meta('_goblinpay_nprofile'); + $pay_url = (string) $order->get_meta('_goblinpay_pay_url'); + $amount = (string) $order->get_meta('_goblinpay_amount'); + + echo '
'; + echo '

' . esc_html__('Pay with Goblin (GRIN)', 'goblinpay-woocommerce') . '

'; + echo '

' . esc_html__('Scan this code with your Goblin Wallet to pay.', 'goblinpay-woocommerce') . '

'; + if ('' !== $amount) { + echo '

' . esc_html__('Amount:', 'goblinpay-woocommerce') . ' ' . esc_html($amount) . '

'; + } + if ('' !== $qr) { + echo '
' . goblinpay_wc_kses_svg($qr) . '
'; + } + if ('' !== $nprofile) { + echo '

' . esc_html($nprofile) . '

'; + } + if ('' !== $pay_url) { + echo '

' + . esc_html__('Open the secure GoblinPay checkout', 'goblinpay-woocommerce') . '

'; + } + echo '

' . esc_html__('Waiting for payment. This page refreshes automatically.', 'goblinpay-woocommerce') . '

'; + // Zero-JS live refresh while the order is unpaid (mirrors the hosted page). + echo ''; + echo '
'; + } + } +}); + +/* ----------------------------------------------------------------------- * + * Webhook receiver: POST /wp-json/goblinpay/v1/webhook + * ----------------------------------------------------------------------- */ +add_action('rest_api_init', function () { + register_rest_route(GOBLINPAY_WC_WH_NS, '/webhook', array( + 'methods' => 'POST', + 'permission_callback' => '__return_true', // authenticated by the HMAC signature below + 'callback' => 'goblinpay_wc_handle_webhook', + )); +}); + +function goblinpay_wc_log($m) { + $s = get_option('woocommerce_' . GOBLINPAY_WC_GATEWAY_ID . '_settings', array()); + if (is_array($s) && !empty($s['debug']) && 'yes' === $s['debug'] && function_exists('wc_get_logger')) { + wc_get_logger()->info(is_string($m) ? $m : wp_json_encode($m), array('source' => 'goblinpay')); + } +} + +/** + * Handle a GoblinPay payment webhook. Verifies the HMAC over the exact raw + * body, dedupes on the event id, maps order_ref -> WC order, and settles. + */ +function goblinpay_wc_handle_webhook(WP_REST_Request $request) { + $settings = get_option('woocommerce_' . GOBLINPAY_WC_GATEWAY_ID . '_settings', array()); + $secret = (is_array($settings) && isset($settings['webhook_secret'])) ? $settings['webhook_secret'] : ''; + + $raw = $request->get_body(); + $sig = (string) $request->get_header('x-goblinpay-signature'); + + if ('' === (string) $secret) { + return new WP_REST_Response(array('error' => 'webhook secret not configured'), 500); + } + // Verify HMAC-SHA256 over the EXACT raw body bytes, constant-time compare. + $expected = 'sha256=' . hash_hmac('sha256', $raw, (string) $secret); + if (!hash_equals($expected, $sig)) { + goblinpay_wc_log(array('webhook_bad_sig' => $sig)); + return new WP_REST_Response(array('error' => 'invalid signature'), 401); + } + + $data = json_decode($raw, true); + if (!is_array($data)) { + return new WP_REST_Response(array('error' => 'bad payload'), 400); + } + goblinpay_wc_log(array('webhook' => $data)); + + // Idempotency: dedupe on the event id (also carried in X-GoblinPay-Delivery). + $event_id = isset($data['event_id']) ? (string) $data['event_id'] : (string) $request->get_header('x-goblinpay-delivery'); + if ('' !== $event_id) { + $key = 'goblinpay_evt_' . md5($event_id); + if (false !== get_transient($key)) { + return new WP_REST_Response(array('ok' => true, 'dedupe' => true), 200); // already processed + } + set_transient($key, 1, WEEK_IN_SECONDS); + } + + $event_type = isset($data['event_type']) ? (string) $data['event_type'] : ''; + $order_ref = isset($data['order_ref']) ? (string) $data['order_ref'] : ''; + $invoice_id = isset($data['invoice_id']) ? (string) $data['invoice_id'] : ''; + $payment = (isset($data['payment']) && is_array($data['payment'])) ? $data['payment'] : array(); + $slate_id = isset($payment['slate_id']) ? (string) $payment['slate_id'] : ''; + + if ('' === $order_ref) { + return new WP_REST_Response(array('ok' => true, 'note' => 'no order_ref'), 200); // ack, nothing to do + } + $order = wc_get_order((int) $order_ref); + if (!$order || $order->get_payment_method() !== GOBLINPAY_WC_GATEWAY_ID) { + return new WP_REST_Response(array('ok' => true, 'note' => 'order not found'), 200); + } + + // Bind the webhook to the invoice we created for this order (defence in depth). + $known = (string) $order->get_meta('_goblinpay_invoice_id'); + if ('' !== $known && '' !== $invoice_id && !hash_equals($known, $invoice_id)) { + goblinpay_wc_log(array('invoice_mismatch' => array('order' => $order_ref, 'known' => $known, 'got' => $invoice_id))); + return new WP_REST_Response(array('ok' => true, 'note' => 'invoice mismatch'), 200); + } + + switch ($event_type) { + case 'payment.received': + // Funds received off-chain (S2 returned). Complete the order. + goblinpay_wc_settle_order($order, $slate_id, __('Grin payment received via GoblinPay.', 'goblinpay-woocommerce')); + break; + + case 'payment.confirmed': + // On-chain confirmation may arrive after payment.received. Idempotent: + // complete if not already paid, otherwise just note the confirmation. + if (!$order->is_paid()) { + goblinpay_wc_settle_order($order, $slate_id, __('Grin payment confirmed on chain via GoblinPay.', 'goblinpay-woocommerce')); + } else { + $height = isset($payment['confirmed_height']) ? $payment['confirmed_height'] : null; + $order->add_order_note( + null === $height + ? __('Grin payment confirmed on chain.', 'goblinpay-woocommerce') + : sprintf( + /* translators: %s: block height */ + __('Grin payment confirmed on chain at height %s.', 'goblinpay-woocommerce'), + (string) $height + ) + ); + } + break; + + default: + goblinpay_wc_log(array('unhandled_event' => $event_type)); + } + + return new WP_REST_Response(array('ok' => true), 200); +} + +/** Complete an order once, idempotently. */ +function goblinpay_wc_settle_order($order, $slate_id, $note) { + if ($order->is_paid()) { + return; + } + $order->payment_complete('' !== (string) $slate_id ? $slate_id : ''); + $order->add_order_note($note); +} + +/* Poll fallback: if a webhook was missed, ask GoblinPay for the invoice status. */ +add_action(GOBLINPAY_WC_POLL_HOOK, 'goblinpay_wc_poll_invoice'); +function goblinpay_wc_poll_invoice($order_id) { + $order = wc_get_order($order_id); + if (!$order || $order->get_payment_method() !== GOBLINPAY_WC_GATEWAY_ID || $order->is_paid()) { + return; + } + $settings = get_option('woocommerce_' . GOBLINPAY_WC_GATEWAY_ID . '_settings', array()); + $gp_url = isset($settings['gp_url']) ? rtrim((string) $settings['gp_url'], '/') : ''; + $token = isset($settings['api_token']) ? trim((string) $settings['api_token']) : ''; + $invoice_id = (string) $order->get_meta('_goblinpay_invoice_id'); + if ('' === $gp_url || '' === $token || '' === $invoice_id) { + return; + } + + $resp = wp_remote_get($gp_url . '/invoice/' . rawurlencode($invoice_id), array( + 'timeout' => 20, + 'headers' => array( + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $token, + ), + )); + if (is_wp_error($resp)) { + goblinpay_wc_log(array('poll_error' => $resp->get_error_message())); + return; + } + $body = json_decode(wp_remote_retrieve_body($resp), true); + if (is_array($body) && isset($body['status']) && 'paid' === $body['status']) { + goblinpay_wc_settle_order($order, '', __('Grin payment reconciled via GoblinPay status poll.', 'goblinpay-woocommerce')); + } +} + +/* WooCommerce-side expiry fallback (polls once more before cancelling). */ +add_action(GOBLINPAY_WC_EXPIRE_HOOK, 'goblinpay_wc_maybe_expire_order'); +function goblinpay_wc_maybe_expire_order($order_id) { + goblinpay_wc_poll_invoice($order_id); // last chance to catch a missed webhook + $order = wc_get_order($order_id); + if (!$order || $order->get_payment_method() !== GOBLINPAY_WC_GATEWAY_ID) { + return; + } + if (!$order->is_paid() && $order->has_status(array('on-hold', 'pending'))) { + $order->update_status('cancelled', __('Grin payment window elapsed without payment.', 'goblinpay-woocommerce')); + } +} + +/** + * Sanitise a GoblinPay-generated QR SVG for safe output. Allows only the small + * tag/attribute set the server emits (svg/g/rect/path/image), so a compromised + * or misconfigured endpoint cannot inject script into the order-received page. + */ +function goblinpay_wc_kses_svg($svg) { + $allowed = array( + 'svg' => array('xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'viewBox' => true, 'role' => true, 'shape-rendering' => true, 'class' => true), + 'g' => array('fill' => true, 'transform' => true), + 'rect' => array('x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, 'fill' => true), + 'path' => array('d' => true, 'fill' => true), + 'image' => array('x' => true, 'y' => true, 'width' => true, 'height' => true, 'href' => true, 'xlink:href' => true, 'preserveaspectratio' => true), + 'title' => array(), + ); + return wp_kses($svg, $allowed); +} diff --git a/connectors/woocommerce/includes/class-blocks.php b/connectors/woocommerce/includes/class-blocks.php new file mode 100644 index 0000000..4c0ea52 --- /dev/null +++ b/connectors/woocommerce/includes/class-blocks.php @@ -0,0 +1,51 @@ +gw_settings = get_option('woocommerce_goblinpay_settings', array()); + if (!is_array($this->gw_settings)) { + $this->gw_settings = array(); + } + } + + public function is_active() { + return !empty($this->gw_settings['enabled']) && 'yes' === $this->gw_settings['enabled']; + } + + public function get_payment_method_script_handles() { + $handle = 'goblinpay-blocks'; + wp_register_script( + $handle, + plugins_url('assets/js/blocks.js', GOBLINPAY_WC_PLUGIN_FILE), + array('wc-blocks-registry', 'wc-settings', 'wp-element', 'wp-html-entities'), + GOBLINPAY_WC_VERSION, + true + ); + return array($handle); + } + + public function get_payment_method_data() { + return array( + 'title' => !empty($this->gw_settings['title']) ? $this->gw_settings['title'] : 'Grin (GRIN)', + 'description' => isset($this->gw_settings['description']) ? $this->gw_settings['description'] : '', + 'supports' => array('products'), + ); + } +} diff --git a/crates/gp-core/Cargo.toml b/crates/gp-core/Cargo.toml new file mode 100644 index 0000000..9f253b5 --- /dev/null +++ b/crates/gp-core/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "gp-core" +description = "GoblinPay domain core: configuration, persistence, invoices, payments" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true + +# Child-identity derivation (matching mode 2 + per-user endpubs): SHA-256 over +# the master nsec, reduced to a valid secp256k1 scalar. secp256k1 is pinned to +# the same 0.31 line gp-nostr already uses (shared in the lock). +sha2 = "0.10" +secp256k1 = "0.31" +hex = "0.4" +# Webhook HMAC signing (milestone 6) with a constant-time verify. +hmac = "0.12" +subtle = "2" +# Server-rendered QR SVG at ECC-H, matrix only (no image/raster feature). +qrcode = { version = "0.14", default-features = false } +# Random invoice ids and unguessable checkout tokens (same line as gp-nostr). +rand = "0.9" +# Host-only log line when a stale rate is served (matches the crate's +# host-only logging posture). +log = "0.4" +# The conversion-rate oracle fetch (milestone 7). DIRECT HTTP, never Nym: +# gp-core has no mixnet linkage, so the direct path is structural. Reuses the +# process-installed rustls `ring` provider (`rustls-no-provider`, no aws-lc-rs), +# exactly as gp-server's webhook client does; no JSON feature (we parse the +# small body with serde_json). +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } diff --git a/crates/gp-core/src/config.rs b/crates/gp-core/src/config.rs new file mode 100644 index 0000000..ee228b4 --- /dev/null +++ b/crates/gp-core/src/config.rs @@ -0,0 +1,801 @@ +//! Runtime configuration. Everything that identifies a particular operator's +//! GoblinPay instance is read from the environment at startup (env-first, +//! same shape as goblin-nip05d), so a second operator can run their own +//! instance without touching the source. +//! +//! Secrets (`GP_MNEMONIC`, `GP_NSEC`) can come from the environment directly +//! or from a mounted file via the `*_FILE` variants, never from the repo. + +use std::fmt; + +use serde::{Deserialize, Serialize}; + +/// Default listen address (loopback; put a proxy or `GP_TLS=rustls` in front +/// for public exposure). +pub const DEFAULT_BIND: &str = "127.0.0.1:8080"; +/// Default SQLite database file, relative to the working directory. +pub const DEFAULT_DB_PATH: &str = "./goblinpay.db"; +/// Default data directory (wallet files, encrypted seed at rest). +pub const DEFAULT_DATA_DIR: &str = "./gp-data"; +/// Default external Grin node (read-only: confirmations and balance). +/// +/// `main.gri.mw`, not `api.grin.money`: the milestone-2/dev round found +/// `api.grin.money`'s bulk UTXO scan (`get_unspent_outputs`) returns errors, +/// while `main.gri.mw` serves the foreign API (`get_tip`, `get_kernel`) +/// cleanly. GoblinPay only ever reads (kernel confirmation + a cached balance), +/// and this traffic goes DIRECT over normal HTTP, never through the Nym tunnel +/// (owner ruling: node reads are a server concern, like Goblin's own +/// wallet->node reads which never ride the mixnet; the mixnet carries only the +/// Nostr gift-wrap layer in gp-nostr). +pub const DEFAULT_NODE_URL: &str = "https://main.gri.mw"; + +/// TLS mode for the HTTP server. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Tls { + /// Plain HTTP (default). Run behind a reverse proxy, or local only. + Off, + /// In-process rustls with a PEM certificate chain and private key. + Rustls { cert: String, key: String }, +} + +/// Grin network to operate on. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Chain { + /// Grin mainnet (default). + Mainnet, + /// Grin testnet. + Testnet, +} + +/// Where the Nostr relay lives. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RelayMode { + /// GoblinPay supervises its own relay (default; see module design 3). + Bundled, + /// Only external relays from `GP_RELAYS` are used. + External, +} + +/// Where the conversion oracle fetches the GRIN price (module `rates`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RateSource { + /// CoinGecko simple-price API (default; GRIN is listed under id `grin`). + CoinGecko, +} + +impl RateSource { + /// Stable string id, used on the quote/receipt and in logs. + pub fn as_str(self) -> &'static str { + match self { + RateSource::CoinGecko => "coingecko", + } + } +} + +/// Global default payment-matching mode (per-invoice override comes later). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum MatchMode { + /// Match by the payer's memo/reference tag. + Memo, + /// Match by a per-invoice derived Nostr identity. + Derived, + /// Match by expected amount within tolerance and expiry. + Amount, +} + +/// A sensitive value. Debug and serde output never reveal it, so a config +/// dump or a startup log line cannot leak a seed or key. +#[derive(Clone, PartialEq, Eq)] +pub struct Secret(String); + +impl Secret { + pub fn new(value: String) -> Self { + Secret(value) + } + + /// Access the underlying value. Call sites should be deliberate. + pub fn reveal(&self) -> &str { + &self.0 + } +} + +impl fmt::Debug for Secret { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Secret(redacted)") + } +} + +/// Resolved, validated runtime configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + /// Address the HTTP server binds (`GP_BIND`). + pub bind: String, + /// TLS mode (`GP_TLS`: `off` or `rustls`, plus `GP_TLS_CERT`/`GP_TLS_KEY`). + pub tls: Tls, + /// SQLite database path (`GP_DB_PATH`); created on first start. + pub db_path: String, + /// Data directory (`GP_DATA_DIR`); holds the wallet files, including the + /// encrypted seed at rest. + pub data_dir: String, + /// External Grin node URL (`GP_NODE_URL`), read-only. + pub node_url: String, + /// Grin network (`GP_CHAIN`: `mainnet` or `testnet`). + pub chain: Chain, + /// Relay mode (`GP_RELAY_MODE`: `bundled` or `external`). + pub relay_mode: RelayMode, + /// External relays (`GP_RELAYS`, comma separated). + pub relays: Vec, + /// Route Nostr traffic over the Nym mixnet (`GP_NYM`: `on` or `off`, + /// default on; clearnet is a debugging escape hatch only). + pub nym: bool, + /// Run the Nostr ingest service (`GP_INGEST`: `on` or `off`, default on). + /// When on, the wallet and identity secrets are required at boot. + pub ingest: bool, + /// Global default matching mode (`GP_MATCH_MODE`). + pub match_mode: MatchMode, + /// Grin seed mnemonic (`GP_MNEMONIC` or `GP_MNEMONIC_FILE`). Money secret. + #[serde(skip)] + pub mnemonic: Option, + /// Password encrypting the wallet seed file at rest (`GP_WALLET_PASSWORD` + /// or `GP_WALLET_PASSWORD_FILE`). Required to open the wallet. Also + /// encrypts the auto-generated Nostr identity file at rest. + #[serde(skip)] + pub wallet_password: Option, + /// Nostr identity key (`GP_NSEC` or `GP_NSEC_FILE`). Payment identity + /// secret, deliberately independent of the Grin seed. + #[serde(skip)] + pub nsec: Option, + /// NIP-49 encrypted Nostr identity key (`GP_NCRYPTSEC` or + /// `GP_NCRYPTSEC_FILE`), unlocked with the wallet password. Mutually + /// exclusive with `GP_NSEC`. + #[serde(skip)] + pub ncryptsec: Option, + /// Public base URL of this instance (`GP_PUBLIC_URL`), used to build the + /// hosted `/pay/` links. Defaults to `http://`. + pub public_url: String, + /// Bearer token for the connector/create-invoice API (`GP_API_TOKEN`). + /// When unset, the write API is closed (503) rather than open. + #[serde(skip)] + pub api_token: Option, + /// Bearer token for the admin dashboard/API (`GP_ADMIN_TOKEN`). + #[serde(skip)] + pub admin_token: Option, + /// Webhook endpoint (`GP_WEBHOOK_URL`) payment events are delivered to. + pub webhook_url: Option, + /// HMAC secret for signing webhooks (`GP_WEBHOOK_SECRET`). + #[serde(skip)] + pub webhook_secret: Option, + /// Center-logo source for checkout QR codes (`GP_QR_LOGO`): unset = the + /// bundled Goblin mark, `off`/`none` = no logo, else a URL or static path. + pub qr_logo: Option, + /// Merchant npub for confirmed-payment DMs (`GP_MERCHANT_NPUB`). + pub merchant_npub: Option, + /// Send a NIP-17 DM to the merchant on a received payment + /// (`GP_NOTIFY_MERCHANT_DM`, default off). + pub notify_merchant_dm: bool, + /// Send a NIP-17 receipt DM to the payer (`GP_NOTIFY_PAYER_RECEIPT`, + /// default off). + pub notify_payer_receipt: bool, + /// Default per-user endpub rotation interval in seconds + /// (`GP_ENDPUB_ROTATE_INTERVAL`, 0 = off). + pub endpub_rotate_interval: i64, + /// How many past epochs to keep watching after a rotation + /// (`GP_ENDPUB_OVERLAP_EPOCHS`, default 1). + pub endpub_overlap_epochs: i64, + /// Conversion-rate source (`GP_RATE_SOURCE`, default `coingecko`). + pub rate_source: RateSource, + /// Supported fiat currencies (`GP_RATE_CURRENCIES`, comma separated, + /// lowercased ISO codes; default `usd`). A fiat invoice in any other + /// currency is rejected up front. + pub rate_currencies: Vec, + /// Seconds a fetched rate is reused before refetching + /// (`GP_RATE_CACHE_TTL`, default 60). + pub rate_cache_ttl: i64, + /// Seconds a created fiat invoice locks its Grin quote (`GP_QUOTE_TTL`, + /// default 900); this becomes the invoice expiry window. + pub quote_ttl: i64, + /// Bounded stale-rate fallback in seconds (`GP_RATE_STALE_MAX`, default 0 + /// = off): if a live fetch fails, a cached rate this recent is served + /// rather than failing the checkout. + pub rate_stale_max: i64, +} + +/// Default supported fiat currency when `GP_RATE_CURRENCIES` is unset. +pub const DEFAULT_RATE_CURRENCY: &str = "usd"; +/// Default rate cache freshness (seconds). +pub const DEFAULT_RATE_CACHE_TTL: i64 = 60; +/// Default quote lock window (seconds). +pub const DEFAULT_QUOTE_TTL: i64 = 900; + +/// Default center-logo path served by gp-server when `GP_QR_LOGO` is unset. +pub const DEFAULT_QR_LOGO: &str = "/static/goblin-mark.svg"; + +impl Default for Config { + fn default() -> Self { + Config { + bind: DEFAULT_BIND.into(), + tls: Tls::Off, + db_path: DEFAULT_DB_PATH.into(), + data_dir: DEFAULT_DATA_DIR.into(), + node_url: DEFAULT_NODE_URL.into(), + chain: Chain::Mainnet, + relay_mode: RelayMode::Bundled, + relays: Vec::new(), + nym: true, + ingest: true, + match_mode: MatchMode::Memo, + mnemonic: None, + wallet_password: None, + nsec: None, + ncryptsec: None, + public_url: format!("http://{DEFAULT_BIND}"), + api_token: None, + admin_token: None, + webhook_url: None, + webhook_secret: None, + qr_logo: Some(DEFAULT_QR_LOGO.into()), + merchant_npub: None, + notify_merchant_dm: false, + notify_payer_receipt: false, + endpub_rotate_interval: 0, + endpub_overlap_epochs: 1, + rate_source: RateSource::CoinGecko, + rate_currencies: vec![DEFAULT_RATE_CURRENCY.to_string()], + rate_cache_ttl: DEFAULT_RATE_CACHE_TTL, + quote_ttl: DEFAULT_QUOTE_TTL, + rate_stale_max: 0, + } + } +} + +impl Config { + /// Load from the process environment, applying defaults, then validate. + /// Returns an error string on misconfiguration (caller should fail fast). + pub fn from_env() -> Result { + Self::from_lookup(&|key| std::env::var(key).ok()) + } + + /// Load from an arbitrary key lookup (the environment in production, a + /// map in tests, so tests never mutate global process state). + pub fn from_lookup(get: &dyn Fn(&str) -> Option) -> Result { + let defaults = Config::default(); + + let bind = get("GP_BIND").unwrap_or(defaults.bind); + + let tls = match get("GP_TLS").as_deref().unwrap_or("off") { + "off" => Tls::Off, + "rustls" => { + let cert = get("GP_TLS_CERT") + .ok_or("GP_TLS=rustls requires GP_TLS_CERT (PEM certificate chain path)")?; + let key = get("GP_TLS_KEY") + .ok_or("GP_TLS=rustls requires GP_TLS_KEY (PEM private key path)")?; + Tls::Rustls { cert, key } + } + other => return Err(format!("GP_TLS must be `off` or `rustls` (got `{other}`)")), + }; + + let db_path = get("GP_DB_PATH").unwrap_or(defaults.db_path); + let data_dir = get("GP_DATA_DIR").unwrap_or(defaults.data_dir); + let node_url = get("GP_NODE_URL").unwrap_or(defaults.node_url); + + let chain = match get("GP_CHAIN").as_deref().unwrap_or("mainnet") { + "mainnet" => Chain::Mainnet, + "testnet" => Chain::Testnet, + other => { + return Err(format!( + "GP_CHAIN must be `mainnet` or `testnet` (got `{other}`)" + )) + } + }; + + let relay_mode = match get("GP_RELAY_MODE").as_deref().unwrap_or("bundled") { + "bundled" => RelayMode::Bundled, + "external" => RelayMode::External, + other => { + return Err(format!( + "GP_RELAY_MODE must be `bundled` or `external` (got `{other}`)" + )) + } + }; + + let relays = get("GP_RELAYS") + .unwrap_or_default() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect::>(); + + let nym = match get("GP_NYM").as_deref().unwrap_or("on") { + "on" => true, + "off" => false, + other => return Err(format!("GP_NYM must be `on` or `off` (got `{other}`)")), + }; + + let ingest = match get("GP_INGEST").as_deref().unwrap_or("on") { + "on" => true, + "off" => false, + other => return Err(format!("GP_INGEST must be `on` or `off` (got `{other}`)")), + }; + + let match_mode = match get("GP_MATCH_MODE").as_deref().unwrap_or("memo") { + "memo" => MatchMode::Memo, + "derived" => MatchMode::Derived, + "amount" => MatchMode::Amount, + other => { + return Err(format!( + "GP_MATCH_MODE must be `memo`, `derived`, or `amount` (got `{other}`)" + )) + } + }; + + let mnemonic = secret(get, "GP_MNEMONIC")?; + let wallet_password = secret(get, "GP_WALLET_PASSWORD")?; + let nsec = secret(get, "GP_NSEC")?; + let ncryptsec = secret(get, "GP_NCRYPTSEC")?; + + let public_url = get("GP_PUBLIC_URL") + .map(|u| u.trim_end_matches('/').to_string()) + .filter(|u| !u.is_empty()) + .unwrap_or_else(|| format!("http://{bind}")); + let api_token = secret(get, "GP_API_TOKEN")?; + let admin_token = secret(get, "GP_ADMIN_TOKEN")?; + let webhook_url = get("GP_WEBHOOK_URL").filter(|s| !s.trim().is_empty()); + let webhook_secret = secret(get, "GP_WEBHOOK_SECRET")?; + let qr_logo = match get("GP_QR_LOGO").as_deref() { + None => Some(DEFAULT_QR_LOGO.to_string()), + Some("off") | Some("none") | Some("") => None, + Some(other) => Some(other.to_string()), + }; + let merchant_npub = get("GP_MERCHANT_NPUB").filter(|s| !s.trim().is_empty()); + let notify_merchant_dm = parse_bool(get, "GP_NOTIFY_MERCHANT_DM", false)?; + let notify_payer_receipt = parse_bool(get, "GP_NOTIFY_PAYER_RECEIPT", false)?; + let endpub_rotate_interval = parse_i64(get, "GP_ENDPUB_ROTATE_INTERVAL", 0)?; + let endpub_overlap_epochs = parse_i64(get, "GP_ENDPUB_OVERLAP_EPOCHS", 1)?; + + let rate_source = match get("GP_RATE_SOURCE").as_deref().unwrap_or("coingecko") { + "coingecko" => RateSource::CoinGecko, + other => { + return Err(format!( + "GP_RATE_SOURCE must be `coingecko` (got `{other}`)" + )) + } + }; + let rate_currencies = match get("GP_RATE_CURRENCIES") { + None => vec![DEFAULT_RATE_CURRENCY.to_string()], + Some(raw) => { + let list = raw + .split(',') + .map(|s| s.trim().to_lowercase()) + .filter(|s| !s.is_empty()) + .collect::>(); + if list.is_empty() { + return Err("GP_RATE_CURRENCIES must list at least one currency".into()); + } + list + } + }; + let rate_cache_ttl = parse_i64(get, "GP_RATE_CACHE_TTL", DEFAULT_RATE_CACHE_TTL)?; + let quote_ttl = parse_i64(get, "GP_QUOTE_TTL", DEFAULT_QUOTE_TTL)?; + let rate_stale_max = parse_i64(get, "GP_RATE_STALE_MAX", 0)?; + + let cfg = Config { + bind, + tls, + db_path, + data_dir, + node_url, + chain, + relay_mode, + relays, + nym, + ingest, + match_mode, + mnemonic, + wallet_password, + nsec, + ncryptsec, + public_url, + api_token, + admin_token, + webhook_url, + webhook_secret, + qr_logo, + merchant_npub, + notify_merchant_dm, + notify_payer_receipt, + endpub_rotate_interval, + endpub_overlap_epochs, + rate_source, + rate_currencies, + rate_cache_ttl, + quote_ttl, + rate_stale_max, + }; + cfg.validate()?; + Ok(cfg) + } + + /// The QR center-logo href to render, or `None` when disabled. + pub fn qr_logo_href(&self) -> Option<&str> { + self.qr_logo.as_deref() + } + + /// Fail-fast consistency checks. + fn validate(&self) -> Result<(), String> { + if self.bind.is_empty() { + return Err("GP_BIND must not be empty".into()); + } + if self.db_path.is_empty() { + return Err("GP_DB_PATH must not be empty".into()); + } + if self.data_dir.is_empty() { + return Err("GP_DATA_DIR must not be empty".into()); + } + if !self.node_url.starts_with("http://") && !self.node_url.starts_with("https://") { + return Err(format!( + "GP_NODE_URL must start with http:// or https:// (got `{}`)", + self.node_url + )); + } + if self.relay_mode == RelayMode::External && self.relays.is_empty() { + return Err("GP_RELAY_MODE=external requires GP_RELAYS".into()); + } + if self.nsec.is_some() && self.ncryptsec.is_some() { + return Err("set only one of GP_NSEC and GP_NCRYPTSEC".into()); + } + if self.webhook_url.is_some() && self.webhook_secret.is_none() { + return Err( + "GP_WEBHOOK_URL requires GP_WEBHOOK_SECRET (webhooks are HMAC-signed)".into(), + ); + } + if self.endpub_overlap_epochs < 0 { + return Err("GP_ENDPUB_OVERLAP_EPOCHS must be >= 0".into()); + } + if self.endpub_rotate_interval < 0 { + return Err("GP_ENDPUB_ROTATE_INTERVAL must be >= 0 (0 = off)".into()); + } + if self.rate_currencies.is_empty() { + return Err("GP_RATE_CURRENCIES must list at least one currency".into()); + } + if self.quote_ttl <= 0 { + return Err("GP_QUOTE_TTL must be > 0 (seconds)".into()); + } + if self.rate_cache_ttl < 0 { + return Err("GP_RATE_CACHE_TTL must be >= 0 (0 = always refetch)".into()); + } + if self.rate_stale_max < 0 { + return Err("GP_RATE_STALE_MAX must be >= 0 (0 = off)".into()); + } + Ok(()) + } + + /// One-line summary for the startup log. Secrets show only as set/unset. + pub fn summary(&self) -> String { + let set = |o: bool| if o { "set" } else { "unset" }; + format!( + "bind={} tls={} db={} data_dir={} node={} chain={:?} relay_mode={:?} \ + relays={:?} nym={} ingest={} match_mode={:?} mnemonic={} wallet_password={} \ + nsec={} ncryptsec={} public_url={} api_token={} admin_token={} webhook_url={} \ + webhook_secret={} qr_logo={} merchant_npub={} notify_merchant_dm={} \ + notify_payer_receipt={} endpub_rotate_interval={} endpub_overlap_epochs={} \ + rate_source={} rate_currencies={:?} rate_cache_ttl={} quote_ttl={} \ + rate_stale_max={}", + self.bind, + match &self.tls { + Tls::Off => "off".to_string(), + Tls::Rustls { cert, key } => format!("rustls(cert={cert},key={key})"), + }, + self.db_path, + self.data_dir, + self.node_url, + self.chain, + self.relay_mode, + self.relays, + if self.nym { "on" } else { "off" }, + if self.ingest { "on" } else { "off" }, + self.match_mode, + set(self.mnemonic.is_some()), + set(self.wallet_password.is_some()), + set(self.nsec.is_some()), + set(self.ncryptsec.is_some()), + self.public_url, + set(self.api_token.is_some()), + set(self.admin_token.is_some()), + self.webhook_url.as_deref().unwrap_or("unset"), + set(self.webhook_secret.is_some()), + self.qr_logo.as_deref().unwrap_or("off"), + self.merchant_npub.as_deref().unwrap_or("unset"), + if self.notify_merchant_dm { "on" } else { "off" }, + if self.notify_payer_receipt { + "on" + } else { + "off" + }, + self.endpub_rotate_interval, + self.endpub_overlap_epochs, + self.rate_source.as_str(), + self.rate_currencies, + self.rate_cache_ttl, + self.quote_ttl, + self.rate_stale_max, + ) + } +} + +/// Parse an `on`/`off` flag with a default. +fn parse_bool( + get: &dyn Fn(&str) -> Option, + key: &str, + default: bool, +) -> Result { + match get(key).as_deref() { + None => Ok(default), + Some("on") => Ok(true), + Some("off") => Ok(false), + Some(other) => Err(format!("{key} must be `on` or `off` (got `{other}`)")), + } +} + +/// Parse an integer with a default. +fn parse_i64(get: &dyn Fn(&str) -> Option, key: &str, default: i64) -> Result { + match get(key) { + None => Ok(default), + Some(v) => v + .trim() + .parse::() + .map_err(|_| format!("{key} must be an integer (got `{v}`)")), + } +} + +/// Read a secret from `KEY` or `KEY_FILE` (mounted file, trailing newline +/// trimmed). Setting both is a hard error, so a stray env var can never +/// silently shadow the mounted file or vice versa. +fn secret(get: &dyn Fn(&str) -> Option, key: &str) -> Result, String> { + let file_key = format!("{key}_FILE"); + match (get(key), get(&file_key)) { + (Some(_), Some(_)) => Err(format!("set only one of {key} and {file_key}")), + (Some(value), None) => Ok(Some(Secret::new(value))), + (None, Some(path)) => { + let text = std::fs::read_to_string(&path) + .map_err(|e| format!("{file_key} `{path}` unreadable: {e}"))?; + let value = text.trim_end_matches(['\n', '\r']).to_string(); + if value.is_empty() { + return Err(format!("{file_key} `{path}` is empty")); + } + Ok(Some(Secret::new(value))) + } + (None, None) => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + fn load(vars: &[(&str, &str)]) -> Result { + let map: HashMap = vars + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + Config::from_lookup(&|key| map.get(key).cloned()) + } + + #[test] + fn empty_env_yields_defaults() { + let cfg = load(&[]).unwrap(); + assert_eq!(cfg.bind, DEFAULT_BIND); + assert_eq!(cfg.tls, Tls::Off); + assert_eq!(cfg.db_path, DEFAULT_DB_PATH); + assert_eq!(cfg.data_dir, DEFAULT_DATA_DIR); + assert_eq!(cfg.node_url, DEFAULT_NODE_URL); + assert_eq!(cfg.chain, Chain::Mainnet); + assert_eq!(cfg.relay_mode, RelayMode::Bundled); + assert!(cfg.relays.is_empty()); + assert!(cfg.nym); + assert!(cfg.ingest); + assert_eq!(cfg.match_mode, MatchMode::Memo); + assert!(cfg.mnemonic.is_none()); + assert!(cfg.wallet_password.is_none()); + assert!(cfg.nsec.is_none()); + assert!(cfg.ncryptsec.is_none()); + } + + #[test] + fn overrides_are_applied() { + let cfg = load(&[ + ("GP_BIND", "0.0.0.0:9000"), + ("GP_DB_PATH", "/var/lib/goblinpay/gp.db"), + ("GP_DATA_DIR", "/var/lib/goblinpay/data"), + ("GP_NODE_URL", "http://127.0.0.1:3413"), + ("GP_CHAIN", "testnet"), + ("GP_RELAY_MODE", "external"), + ("GP_RELAYS", "wss://relay.example, wss://relay2.example ,"), + ("GP_NYM", "off"), + ("GP_INGEST", "off"), + ("GP_MATCH_MODE", "derived"), + ]) + .unwrap(); + assert_eq!(cfg.bind, "0.0.0.0:9000"); + assert_eq!(cfg.db_path, "/var/lib/goblinpay/gp.db"); + assert_eq!(cfg.data_dir, "/var/lib/goblinpay/data"); + assert_eq!(cfg.node_url, "http://127.0.0.1:3413"); + assert_eq!(cfg.chain, Chain::Testnet); + assert_eq!(cfg.relay_mode, RelayMode::External); + assert_eq!( + cfg.relays, + vec!["wss://relay.example", "wss://relay2.example"] + ); + assert!(!cfg.nym); + assert!(!cfg.ingest); + assert_eq!(cfg.match_mode, MatchMode::Derived); + } + + #[test] + fn tls_rustls_requires_cert_and_key() { + assert!(load(&[("GP_TLS", "rustls")]).is_err()); + assert!(load(&[("GP_TLS", "rustls"), ("GP_TLS_CERT", "/c.pem")]).is_err()); + let cfg = load(&[ + ("GP_TLS", "rustls"), + ("GP_TLS_CERT", "/c.pem"), + ("GP_TLS_KEY", "/k.pem"), + ]) + .unwrap(); + assert_eq!( + cfg.tls, + Tls::Rustls { + cert: "/c.pem".into(), + key: "/k.pem".into() + } + ); + } + + #[test] + fn rejects_unknown_enum_values() { + assert!(load(&[("GP_TLS", "acme")]).is_err()); + assert!(load(&[("GP_CHAIN", "floonet")]).is_err()); + assert!(load(&[("GP_RELAY_MODE", "both")]).is_err()); + assert!(load(&[("GP_NYM", "true")]).is_err()); + assert!(load(&[("GP_INGEST", "yes")]).is_err()); + assert!(load(&[("GP_MATCH_MODE", "exact")]).is_err()); + } + + #[test] + fn nsec_and_ncryptsec_together_is_an_error() { + assert!(load(&[("GP_NSEC", "nsec1a"), ("GP_NCRYPTSEC", "ncryptsec1b")]).is_err()); + assert!(load(&[("GP_NCRYPTSEC", "ncryptsec1b")]).is_ok()); + } + + #[test] + fn rejects_bad_node_url_and_external_without_relays() { + assert!(load(&[("GP_NODE_URL", "grin.money")]).is_err()); + assert!(load(&[("GP_RELAY_MODE", "external")]).is_err()); + assert!(load(&[("GP_DATA_DIR", "")]).is_err()); + } + + #[test] + fn secret_from_env_var() { + let cfg = load(&[("GP_MNEMONIC", "abandon ability able")]).unwrap(); + assert_eq!(cfg.mnemonic.unwrap().reveal(), "abandon ability able"); + } + + #[test] + fn secret_from_mounted_file_trims_trailing_newline() { + let path = std::env::temp_dir().join(format!("gp-nsec-{}", std::process::id())); + std::fs::write(&path, "nsec1testvalue\n").unwrap(); + let cfg = load(&[("GP_NSEC_FILE", path.to_str().unwrap())]).unwrap(); + assert_eq!(cfg.nsec.unwrap().reveal(), "nsec1testvalue"); + std::fs::remove_file(&path).unwrap(); + } + + #[test] + fn secret_env_and_file_together_is_an_error() { + assert!(load(&[("GP_NSEC", "a"), ("GP_NSEC_FILE", "/tmp/x")]).is_err()); + } + + #[test] + fn secret_missing_file_is_an_error() { + assert!(load(&[("GP_MNEMONIC_FILE", "/nonexistent/gp-seed")]).is_err()); + } + + #[test] + fn m5_m6_defaults_and_overrides() { + let cfg = load(&[]).unwrap(); + assert_eq!(cfg.public_url, format!("http://{DEFAULT_BIND}")); + assert_eq!(cfg.qr_logo.as_deref(), Some(DEFAULT_QR_LOGO)); + assert!(!cfg.notify_merchant_dm); + assert!(!cfg.notify_payer_receipt); + assert_eq!(cfg.endpub_rotate_interval, 0); + assert_eq!(cfg.endpub_overlap_epochs, 1); + assert!(cfg.api_token.is_none()); + + let cfg = load(&[ + ("GP_PUBLIC_URL", "https://pay.example/"), + ("GP_API_TOKEN", "apitok"), + ("GP_ADMIN_TOKEN", "admintok"), + ("GP_QR_LOGO", "off"), + ("GP_NOTIFY_MERCHANT_DM", "on"), + ("GP_ENDPUB_ROTATE_INTERVAL", "3600"), + ("GP_ENDPUB_OVERLAP_EPOCHS", "2"), + ]) + .unwrap(); + assert_eq!(cfg.public_url, "https://pay.example"); // trailing slash trimmed + assert_eq!(cfg.api_token.unwrap().reveal(), "apitok"); + assert!(cfg.qr_logo.is_none(), "off disables the logo"); + assert!(cfg.notify_merchant_dm); + assert_eq!(cfg.endpub_rotate_interval, 3600); + assert_eq!(cfg.endpub_overlap_epochs, 2); + } + + #[test] + fn webhook_url_requires_secret_and_flags_validate() { + assert!(load(&[("GP_WEBHOOK_URL", "https://store/hook")]).is_err()); + assert!(load(&[ + ("GP_WEBHOOK_URL", "https://store/hook"), + ("GP_WEBHOOK_SECRET", "shh"), + ]) + .is_ok()); + assert!(load(&[("GP_NOTIFY_MERCHANT_DM", "yes")]).is_err()); + assert!(load(&[("GP_ENDPUB_ROTATE_INTERVAL", "-5")]).is_err()); + assert!(load(&[("GP_ENDPUB_ROTATE_INTERVAL", "notanumber")]).is_err()); + } + + #[test] + fn m7_rate_defaults_and_overrides() { + let cfg = load(&[]).unwrap(); + assert_eq!(cfg.rate_source, RateSource::CoinGecko); + assert_eq!(cfg.rate_currencies, vec!["usd".to_string()]); + assert_eq!(cfg.rate_cache_ttl, DEFAULT_RATE_CACHE_TTL); + assert_eq!(cfg.quote_ttl, DEFAULT_QUOTE_TTL); + assert_eq!(cfg.rate_stale_max, 0); + + let cfg = load(&[ + ("GP_RATE_SOURCE", "coingecko"), + ("GP_RATE_CURRENCIES", "USD, eur , GBP,"), + ("GP_RATE_CACHE_TTL", "30"), + ("GP_QUOTE_TTL", "600"), + ("GP_RATE_STALE_MAX", "1800"), + ]) + .unwrap(); + // Currencies are lowercased and trimmed, blanks dropped. + assert_eq!(cfg.rate_currencies, vec!["usd", "eur", "gbp"]); + assert_eq!(cfg.rate_cache_ttl, 30); + assert_eq!(cfg.quote_ttl, 600); + assert_eq!(cfg.rate_stale_max, 1800); + } + + #[test] + fn m7_rate_validation_rejects_bad_values() { + assert!(load(&[("GP_RATE_SOURCE", "messari")]).is_err()); + assert!(load(&[("GP_RATE_CURRENCIES", " , ")]).is_err()); + assert!(load(&[("GP_QUOTE_TTL", "0")]).is_err()); + assert!(load(&[("GP_QUOTE_TTL", "-1")]).is_err()); + assert!(load(&[("GP_RATE_CACHE_TTL", "-1")]).is_err()); + assert!(load(&[("GP_RATE_STALE_MAX", "-5")]).is_err()); + } + + #[test] + fn debug_and_summary_never_leak_secrets() { + let cfg = load(&[ + ("GP_MNEMONIC", "topsecret words"), + ("GP_WALLET_PASSWORD", "hushhush"), + ]) + .unwrap(); + let debug = format!("{cfg:?}"); + assert!(!debug.contains("topsecret")); + assert!(!debug.contains("hushhush")); + assert!(debug.contains("Secret(redacted)")); + let summary = cfg.summary(); + assert!(!summary.contains("topsecret")); + assert!(!summary.contains("hushhush")); + assert!(summary.contains("mnemonic=set")); + assert!(summary.contains("wallet_password=set")); + assert!(summary.contains("nsec=unset")); + } +} diff --git a/crates/gp-core/src/db.rs b/crates/gp-core/src/db.rs new file mode 100644 index 0000000..746d710 --- /dev/null +++ b/crates/gp-core/src/db.rs @@ -0,0 +1,73 @@ +//! SQLite persistence via raw `sqlx` (no ORM). One database file, zero-ops, +//! trivial backup. DB access stays behind this thin module so a later +//! Postgres swap is contained. + +use sqlx::migrate::Migrator; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; + +/// Embedded migrations from the workspace-level `migrations/` directory. +pub static MIGRATOR: Migrator = sqlx::migrate!("../../migrations"); + +/// Open (creating if missing) the SQLite database at `db_path` and bring the +/// schema up to date. Called once at startup. +/// +/// A single pooled connection: SQLite serializes writers anyway, and one +/// connection keeps the migrator (and every write) free of "database is +/// locked" contention. Fine for a low-traffic receive-only till; a later +/// Postgres swap for the multi-store backend lifts the ceiling. +pub async fn init(db_path: &str) -> Result { + let options = SqliteConnectOptions::new() + .filename(db_path) + .create_if_missing(true) + .busy_timeout(std::time::Duration::from_secs(10)) + .foreign_keys(true); + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await?; + MIGRATOR.run(&pool).await?; + Ok(pool) +} + +/// A migrated in-memory database on a single shared connection, for tests. +#[cfg(test)] +pub(crate) async fn test_pool() -> SqlitePool { + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect("sqlite::memory:") + .await + .expect("open in-memory sqlite"); + MIGRATOR.run(&pool).await.expect("run migrations"); + pool +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn table_names(pool: &SqlitePool) -> Vec { + sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name") + .fetch_all(pool) + .await + .unwrap() + } + + #[tokio::test] + async fn init_creates_db_and_applies_migrations() { + let path = std::env::temp_dir().join(format!("gp-db-test-{}.db", std::process::id())); + let _ = std::fs::remove_file(&path); + + let pool = init(path.to_str().unwrap()).await.unwrap(); + let tables = table_names(&pool).await; + assert!(tables.contains(&"payment".to_string()), "{tables:?}"); + assert!(tables.contains(&"invoice".to_string()), "{tables:?}"); + pool.close().await; + + // Re-opening an existing database re-runs the migrator harmlessly. + let pool = init(path.to_str().unwrap()).await.unwrap(); + assert!(table_names(&pool).await.contains(&"payment".to_string())); + pool.close().await; + + let _ = std::fs::remove_file(&path); + } +} diff --git a/crates/gp-core/src/derive.rs b/crates/gp-core/src/derive.rs new file mode 100644 index 0000000..92411c8 --- /dev/null +++ b/crates/gp-core/src/derive.rs @@ -0,0 +1,166 @@ +//! Deterministic, stateless child-identity derivation from the server's Nostr +//! secret key. +//! +//! Both matching mode 2 (per-invoice derived identity) and the per-user +//! endpubs of milestone 5b derive a fresh Nostr identity as a child of the +//! server nsec, keyed by a context (the invoice id, or `user_id || epoch`): +//! +//! ```text +//! child_sk = SHA256(master_sk || context) (retry with a counter if the +//! digest is not a valid scalar) +//! ``` +//! +//! This is deliberately derived from the **Nostr** master secret, never the +//! Grin seed (the two-secrets rule, G6): a child identity can decrypt a gift +//! wrap, it can never touch the money. Nothing is stored: any child key +//! recomputes from its context on demand, so the database holds only public +//! keys, assignments, and the rotation clock. +//! +//! The happy path is exactly `SHA256(master_sk || context)`; a one-byte +//! big-endian counter is appended only on the (cryptographically negligible) +//! chance the digest is zero or exceeds the curve order, so derivation stays a +//! pure function of `(master_sk, context)`. + +use secp256k1::{Secp256k1, SecretKey}; +use sha2::{Digest, Sha256}; + +/// The derived child secret key (32 bytes), guaranteed a valid secp256k1 +/// scalar. `context` is the domain material appended after the master key +/// (e.g. the invoice id bytes, or `user_id` bytes followed by the epoch). +pub fn child_secret(master_sk: &[u8; 32], context: &[&[u8]]) -> [u8; 32] { + // Rejection sampling: hash, and if the digest is not a valid scalar + // (probability ~2^-128), append an incrementing counter and rehash. The + // counter is absent on the first, near-certain attempt, so the derivation + // matches the documented `SHA256(master_sk || context)` exactly. + for counter in 0u32.. { + let mut hasher = Sha256::new(); + hasher.update(master_sk); + for part in context { + hasher.update(part); + } + if counter > 0 { + hasher.update(counter.to_be_bytes()); + } + let digest: [u8; 32] = hasher.finalize().into(); + if SecretKey::from_byte_array(digest).is_ok() { + return digest; + } + } + unreachable!("a valid secp256k1 scalar is found within the first few counters") +} + +/// The x-only (BIP-340) public key of a derived child, lowercase hex. This is +/// the same 32-byte key a Nostr `npub` encodes, so it compares directly +/// against the `p` tag of an incoming gift wrap. +pub fn child_pubkey_hex(master_sk: &[u8; 32], context: &[&[u8]]) -> String { + let secret = child_secret(master_sk, context); + let secp = Secp256k1::new(); + let sk = SecretKey::from_byte_array(secret).expect("child_secret returns a valid scalar"); + let (xonly, _parity) = sk.x_only_public_key(&secp); + hex::encode(xonly.serialize()) +} + +/// Context for a per-invoice derived identity (matching mode 2): +/// `SHA256(master_sk || invoice_id)`. +pub fn invoice_context(invoice_id: &str) -> Vec> { + vec![invoice_id.as_bytes().to_vec()] +} + +/// Context for a per-user endpub (milestone 5b): +/// `SHA256(master_sk || user_id || epoch)` with the epoch big-endian. +pub fn endpub_context(user_id: &str, epoch: i64) -> Vec> { + vec![user_id.as_bytes().to_vec(), epoch.to_be_bytes().to_vec()] +} + +/// Derive the child secret for an invoice. +pub fn invoice_secret(master_sk: &[u8; 32], invoice_id: &str) -> [u8; 32] { + let ctx = invoice_context(invoice_id); + let parts: Vec<&[u8]> = ctx.iter().map(|p| p.as_slice()).collect(); + child_secret(master_sk, &parts) +} + +/// Derive the child x-only pubkey hex for an invoice. +pub fn invoice_pubkey_hex(master_sk: &[u8; 32], invoice_id: &str) -> String { + let ctx = invoice_context(invoice_id); + let parts: Vec<&[u8]> = ctx.iter().map(|p| p.as_slice()).collect(); + child_pubkey_hex(master_sk, &parts) +} + +/// Derive the child secret for a user's endpub at a given epoch. +pub fn endpub_secret(master_sk: &[u8; 32], user_id: &str, epoch: i64) -> [u8; 32] { + let ctx = endpub_context(user_id, epoch); + let parts: Vec<&[u8]> = ctx.iter().map(|p| p.as_slice()).collect(); + child_secret(master_sk, &parts) +} + +/// Derive the child x-only pubkey hex for a user's endpub at a given epoch. +pub fn endpub_pubkey_hex(master_sk: &[u8; 32], user_id: &str, epoch: i64) -> String { + let ctx = endpub_context(user_id, epoch); + let parts: Vec<&[u8]> = ctx.iter().map(|p| p.as_slice()).collect(); + child_pubkey_hex(master_sk, &parts) +} + +#[cfg(test)] +mod tests { + use super::*; + + const MASTER: [u8; 32] = [7u8; 32]; + + #[test] + fn derivation_is_deterministic_and_stateless() { + // Same inputs, same key — every time, with no stored state. + let a = invoice_secret(&MASTER, "inv-abc"); + let b = invoice_secret(&MASTER, "inv-abc"); + assert_eq!(a, b); + assert_eq!( + invoice_pubkey_hex(&MASTER, "inv-abc"), + invoice_pubkey_hex(&MASTER, "inv-abc") + ); + } + + #[test] + fn distinct_contexts_yield_distinct_keys() { + assert_ne!( + invoice_secret(&MASTER, "inv-1"), + invoice_secret(&MASTER, "inv-2") + ); + // Per-user, per-epoch keys are all distinct. + assert_ne!( + endpub_secret(&MASTER, "alice", 0), + endpub_secret(&MASTER, "alice", 1) + ); + assert_ne!( + endpub_secret(&MASTER, "alice", 0), + endpub_secret(&MASTER, "bob", 0) + ); + // And an invoice context never collides with an endpub context of the + // same textual prefix (the epoch bytes keep them apart). + assert_ne!( + invoice_pubkey_hex(&MASTER, "alice"), + endpub_pubkey_hex(&MASTER, "alice", 0) + ); + } + + #[test] + fn a_different_master_gives_a_different_child() { + let other = [9u8; 32]; + assert_ne!( + invoice_secret(&MASTER, "inv-abc"), + invoice_secret(&other, "inv-abc") + ); + } + + #[test] + fn derived_secret_is_a_valid_scalar_and_pubkey_is_32_hex_bytes() { + let secret = endpub_secret(&MASTER, "carol", 3); + assert!(SecretKey::from_byte_array(secret).is_ok()); + let pk = endpub_pubkey_hex(&MASTER, "carol", 3); + assert_eq!(pk.len(), 64, "x-only pubkey is 32 bytes = 64 hex chars"); + assert!(hex::decode(&pk).is_ok()); + // The pubkey matches an independent recomputation of the secret. + let secp = Secp256k1::new(); + let sk = SecretKey::from_byte_array(secret).unwrap(); + let (xonly, _) = sk.x_only_public_key(&secp); + assert_eq!(pk, hex::encode(xonly.serialize())); + } +} diff --git a/crates/gp-core/src/endpub.rs b/crates/gp-core/src/endpub.rs new file mode 100644 index 0000000..08bccf2 --- /dev/null +++ b/crates/gp-core/src/endpub.rs @@ -0,0 +1,356 @@ +//! Per-user endpubs with optional rolling rotation (milestone 5b, +//! multi-tenant receiving). +//! +//! An admin assigns one receiving identity ("endpub") per end-user. The +//! endpub is a stateless child of the server nsec keyed by `(user_id, epoch)` +//! (see [`crate::derive`]), so the database stores only the assignment and the +//! rotation clock, never a private key. All funds still land in the one Grin +//! wallet; the endpub only decides which user an incoming payment credits. +//! +//! Optional rotation advances a user's epoch on a per-user (or global default) +//! interval, rolling their advertised endpub. An overlap window keeps the last +//! N epochs watched, so a payment sent to a just-rotated endpub still lands and +//! still maps to that user. + +use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; + +use crate::{derive, ids}; + +/// A tenant user. `rotate_interval` is a per-user override in seconds +/// (`NULL` = global default, `0` = rotation off). `epoch` is the current +/// (highest) endpub epoch. +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct User { + pub id: String, + pub rotate_interval: Option, + pub epoch: i64, + pub last_rotated_at: String, + pub created_at: String, +} + +/// One endpub assignment: a user's receiving pubkey at a given epoch. The +/// pubkey is the derived x-only hex (public, never a secret). +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct Endpub { + pub user_id: String, + pub epoch: i64, + pub pubkey: String, + pub created_at: String, +} + +/// A user with their current endpub and running balance (admin listing). +#[derive(Debug, Clone, Serialize)] +pub struct UserBalance { + pub user_id: String, + pub epoch: i64, + pub endpub: String, + /// Total received nanogrin credited to this user. + pub balance: i64, +} + +/// Create a user (id auto-generated when `id` is `None`) and assign their +/// epoch-0 endpub. Returns the user and their first endpub. +pub async fn create_user( + pool: &SqlitePool, + master_sk: &[u8; 32], + id: Option, + rotate_interval: Option, +) -> Result<(User, Endpub), sqlx::Error> { + let id = id.unwrap_or_else(ids::random_id); + sqlx::query( + "INSERT INTO user (id, rotate_interval, epoch, last_rotated_at, created_at) \ + VALUES (?1, ?2, 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), \ + strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))", + ) + .bind(&id) + .bind(rotate_interval) + .execute(pool) + .await?; + let endpub = assign(pool, master_sk, &id, 0).await?; + let user = get_user(pool, &id).await?.ok_or(sqlx::Error::RowNotFound)?; + Ok((user, endpub)) +} + +/// Assign (idempotently) the endpub for `(user_id, epoch)`, deriving its +/// pubkey. Returns the assignment row. +async fn assign( + pool: &SqlitePool, + master_sk: &[u8; 32], + user_id: &str, + epoch: i64, +) -> Result { + let pubkey = derive::endpub_pubkey_hex(master_sk, user_id, epoch); + sqlx::query( + "INSERT OR IGNORE INTO endpub_assignment (user_id, epoch, pubkey, created_at) \ + VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))", + ) + .bind(user_id) + .bind(epoch) + .bind(&pubkey) + .execute(pool) + .await?; + sqlx::query_as::<_, Endpub>( + "SELECT user_id, epoch, pubkey, created_at FROM endpub_assignment \ + WHERE user_id = ?1 AND epoch = ?2", + ) + .bind(user_id) + .bind(epoch) + .fetch_one(pool) + .await +} + +/// Fetch a user by id. +pub async fn get_user(pool: &SqlitePool, id: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, User>( + "SELECT id, rotate_interval, epoch, last_rotated_at, created_at FROM user WHERE id = ?1", + ) + .bind(id) + .fetch_optional(pool) + .await +} + +/// The user's current (highest-epoch) endpub. +pub async fn current_endpub( + pool: &SqlitePool, + user_id: &str, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, Endpub>( + "SELECT user_id, epoch, pubkey, created_at FROM endpub_assignment \ + WHERE user_id = ?1 ORDER BY epoch DESC LIMIT 1", + ) + .bind(user_id) + .fetch_optional(pool) + .await +} + +/// Set (or clear, with `None`) a user's per-user rotation interval in seconds. +pub async fn set_rotate_interval( + pool: &SqlitePool, + user_id: &str, + interval: Option, +) -> Result { + let result = sqlx::query("UPDATE user SET rotate_interval = ?2 WHERE id = ?1") + .bind(user_id) + .bind(interval) + .execute(pool) + .await?; + Ok(result.rows_affected() > 0) +} + +/// Force-rotate a user now: advance their epoch and assign the new endpub. +/// Returns the new endpub. +pub async fn rotate( + pool: &SqlitePool, + master_sk: &[u8; 32], + user_id: &str, +) -> Result { + let user = get_user(pool, user_id) + .await? + .ok_or(sqlx::Error::RowNotFound)?; + let next = user.epoch + 1; + sqlx::query( + "UPDATE user SET epoch = ?2, last_rotated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') \ + WHERE id = ?1", + ) + .bind(user_id) + .bind(next) + .execute(pool) + .await?; + assign(pool, master_sk, user_id, next).await +} + +/// Rotate every user whose rotation clock has elapsed (per-user interval, else +/// `global_interval`; `0`/`None` = off). Called by a periodic tick. Returns +/// the number of users rotated. Rotation is staggered by each user's own +/// clock, never a flag-day. +pub async fn rotate_due( + pool: &SqlitePool, + master_sk: &[u8; 32], + global_interval: i64, +) -> Result { + let users = sqlx::query_as::<_, User>( + "SELECT id, rotate_interval, epoch, last_rotated_at, created_at FROM user", + ) + .fetch_all(pool) + .await?; + let mut rotated = 0; + for user in users { + let interval = user.rotate_interval.unwrap_or(global_interval); + if interval <= 0 { + continue; + } + // Elapsed since last rotation, in whole seconds, computed in SQL to + // avoid a Rust-side clock dependency. + let elapsed: i64 = sqlx::query_scalar( + "SELECT CAST(strftime('%s', 'now') AS INTEGER) \ + - CAST(strftime('%s', ?1) AS INTEGER)", + ) + .bind(&user.last_rotated_at) + .fetch_one(pool) + .await?; + if elapsed >= interval { + rotate(pool, master_sk, &user.id).await?; + rotated += 1; + } + } + Ok(rotated) +} + +/// The union of pubkeys to subscribe to: for every user, the current epoch and +/// the previous `overlap` epochs. This is what gp-nostr watches so a payment +/// to a just-rotated endpub still lands within the window. +pub async fn watched_pubkeys(pool: &SqlitePool, overlap: i64) -> Result, sqlx::Error> { + let overlap = overlap.max(0); + sqlx::query_as::<_, Endpub>( + "SELECT a.user_id, a.epoch, a.pubkey, a.created_at \ + FROM endpub_assignment a JOIN user u ON u.id = a.user_id \ + WHERE a.epoch >= u.epoch - ?1 \ + ORDER BY a.user_id, a.epoch", + ) + .bind(overlap) + .fetch_all(pool) + .await +} + +/// Resolve a received pubkey to its `(user_id, epoch)`, if it is any assigned +/// endpub (crediting works for any stored assignment, even one just rotated +/// past the watch window). +pub async fn user_for_pubkey( + pool: &SqlitePool, + pubkey: &str, +) -> Result, sqlx::Error> { + let row: Option<(String, i64)> = + sqlx::query_as("SELECT user_id, epoch FROM endpub_assignment WHERE pubkey = ?1 LIMIT 1") + .bind(pubkey) + .fetch_optional(pool) + .await?; + Ok(row) +} + +/// Every user with their current endpub and running received balance. +pub async fn list_with_balances(pool: &SqlitePool) -> Result, sqlx::Error> { + let users = sqlx::query_as::<_, User>( + "SELECT id, rotate_interval, epoch, last_rotated_at, created_at FROM user \ + ORDER BY created_at DESC", + ) + .fetch_all(pool) + .await?; + let mut out = Vec::with_capacity(users.len()); + for user in users { + let endpub = current_endpub(pool, &user.id) + .await? + .map(|e| e.pubkey) + .unwrap_or_default(); + let balance: i64 = + sqlx::query_scalar("SELECT COALESCE(SUM(amount), 0) FROM payment WHERE user_id = ?1") + .bind(&user.id) + .fetch_one(pool) + .await?; + out.push(UserBalance { + user_id: user.id, + epoch: user.epoch, + endpub, + balance, + }); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db; + + async fn pool() -> SqlitePool { + db::test_pool().await + } + + const MASTER: [u8; 32] = [5u8; 32]; + + #[tokio::test] + async fn create_user_assigns_a_deterministic_endpub() { + let pool = pool().await; + let (user, endpub) = create_user(&pool, &MASTER, Some("alice".into()), None) + .await + .unwrap(); + assert_eq!(user.epoch, 0); + assert_eq!(endpub.epoch, 0); + // Stateless: the stored pubkey equals a fresh derivation. + assert_eq!( + endpub.pubkey, + derive::endpub_pubkey_hex(&MASTER, "alice", 0) + ); + // And it resolves back to the user. + assert_eq!( + user_for_pubkey(&pool, &endpub.pubkey).await.unwrap(), + Some(("alice".into(), 0)) + ); + } + + #[tokio::test] + async fn rotation_keeps_old_epochs_payable_within_the_window() { + let pool = pool().await; + let (_u, first) = create_user(&pool, &MASTER, Some("bob".into()), None) + .await + .unwrap(); + let second = rotate(&pool, &MASTER, "bob").await.unwrap(); + assert_eq!(second.epoch, 1); + assert_ne!(first.pubkey, second.pubkey); + + // Both epochs still credit the same user (crediting is not gated on + // the watch window)... + assert_eq!( + user_for_pubkey(&pool, &first.pubkey).await.unwrap(), + Some(("bob".into(), 0)) + ); + assert_eq!( + user_for_pubkey(&pool, &second.pubkey).await.unwrap(), + Some(("bob".into(), 1)) + ); + + // ...and with overlap >= 1 the just-rotated old endpub is still watched. + let watched: Vec = watched_pubkeys(&pool, 1) + .await + .unwrap() + .into_iter() + .map(|e| e.pubkey) + .collect(); + assert!( + watched.contains(&first.pubkey), + "overlap keeps epoch 0 watched" + ); + assert!(watched.contains(&second.pubkey)); + + // With no overlap only the current epoch is watched. + let watched0: Vec = watched_pubkeys(&pool, 0) + .await + .unwrap() + .into_iter() + .map(|e| e.pubkey) + .collect(); + assert_eq!(watched0, vec![second.pubkey.clone()]); + } + + #[tokio::test] + async fn rotate_due_respects_intervals() { + let pool = pool().await; + // interval 0 (off): never rotates even though last_rotated is old. + create_user(&pool, &MASTER, Some("off".into()), Some(0)) + .await + .unwrap(); + // A short interval with a backdated clock rotates. + create_user(&pool, &MASTER, Some("due".into()), Some(10)) + .await + .unwrap(); + sqlx::query("UPDATE user SET last_rotated_at = '2000-01-01T00:00:00Z' WHERE id = 'due'") + .execute(&pool) + .await + .unwrap(); + + let rotated = rotate_due(&pool, &MASTER, 0).await.unwrap(); + assert_eq!(rotated, 1); + assert_eq!(get_user(&pool, "due").await.unwrap().unwrap().epoch, 1); + assert_eq!(get_user(&pool, "off").await.unwrap().unwrap().epoch, 0); + } +} diff --git a/crates/gp-core/src/ids.rs b/crates/gp-core/src/ids.rs new file mode 100644 index 0000000..ff09253 --- /dev/null +++ b/crates/gp-core/src/ids.rs @@ -0,0 +1,90 @@ +//! Random identifiers and timestamps. +//! +//! Invoice ids and webhook event ids are random 128-bit values, hex encoded. +//! Checkout tokens are 256-bit and treated as bearer capabilities (the +//! unguessable secret that authorizes `/pay/`), so they get twice the +//! entropy and a URL-safe base64 encoding. + +use rand::RngCore; + +/// A random 128-bit id, lowercase hex (32 chars). Used for invoice ids, +/// webhook event ids, and user ids. +pub fn random_id() -> String { + let mut bytes = [0u8; 16]; + rand::rng().fill_bytes(&mut bytes); + hex::encode(bytes) +} + +/// A random 256-bit checkout token, URL-safe base64 without padding (43 +/// chars). This is the bearer capability for the hosted `/pay/` page: +/// unguessable and not enumerable, never a database row number. +pub fn checkout_token() -> String { + let mut bytes = [0u8; 32]; + rand::rng().fill_bytes(&mut bytes); + base64_url_nopad(&bytes) +} + +/// Minimal URL-safe base64 (no padding), so the token needs no percent +/// encoding in a path and pulls in no base64 dependency of its own. +fn base64_url_nopad(input: &[u8]) -> String { + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + let mut out = String::with_capacity(input.len().div_ceil(3) * 4); + for chunk in input.chunks(3) { + let b0 = chunk[0] as u32; + let b1 = *chunk.get(1).unwrap_or(&0) as u32; + let b2 = *chunk.get(2).unwrap_or(&0) as u32; + let n = (b0 << 16) | (b1 << 8) | b2; + out.push(ALPHABET[(n >> 18 & 0x3f) as usize] as char); + out.push(ALPHABET[(n >> 12 & 0x3f) as usize] as char); + if chunk.len() > 1 { + out.push(ALPHABET[(n >> 6 & 0x3f) as usize] as char); + } + if chunk.len() > 2 { + out.push(ALPHABET[(n & 0x3f) as usize] as char); + } + } + out +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + + #[test] + fn ids_are_unique_and_hex() { + let mut seen = HashSet::new(); + for _ in 0..1000 { + let id = random_id(); + assert_eq!(id.len(), 32); + assert!(hex::decode(&id).is_ok()); + assert!(seen.insert(id), "ids must not collide"); + } + } + + #[test] + fn tokens_are_unguessable_length_and_url_safe() { + let mut seen = HashSet::new(); + for _ in 0..1000 { + let token = checkout_token(); + assert_eq!(token.len(), 43, "256 bits, url-safe base64 no pad"); + assert!(token + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')); + assert!(seen.insert(token), "tokens must not collide"); + } + } + + #[test] + fn base64_matches_known_vectors() { + // Cross-checked against RFC 4648 URL-safe base64 (no padding). + assert_eq!(base64_url_nopad(b""), ""); + assert_eq!(base64_url_nopad(b"f"), "Zg"); + assert_eq!(base64_url_nopad(b"fo"), "Zm8"); + assert_eq!(base64_url_nopad(b"foo"), "Zm9v"); + assert_eq!(base64_url_nopad(b"foob"), "Zm9vYg"); + assert_eq!(base64_url_nopad(&[0xff, 0xff, 0xff]), "____"); + assert_eq!(base64_url_nopad(&[0xfb, 0xff, 0xbf]), "-_-_"); + } +} diff --git a/crates/gp-core/src/invoice.rs b/crates/gp-core/src/invoice.rs new file mode 100644 index 0000000..4215802 --- /dev/null +++ b/crates/gp-core/src/invoice.rs @@ -0,0 +1,430 @@ +//! Invoices: the optional order-matching layer over received payments. +//! +//! An invoice pins an expected payment (an amount, or a fiat quote to be +//! filled by the conversion milestone) to an order reference and mints an +//! unguessable checkout token for the hosted `/pay/` page. Its +//! recipient identity is either the server's master Nostr key (for memo and +//! amount matching) or a per-invoice derived child (matching mode 2); only the +//! public key is stored, the child secret is recomputed on demand. +//! +//! Lifecycle: `open` -> `paid` (a received payment matched it) or `expired` +//! (its expiry passed while still open). Expiry is evaluated lazily on read +//! and by a periodic sweep, never by a background per-invoice timer. + +use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; + +use crate::config::MatchMode; +use crate::{derive, ids}; + +/// Invoice lifecycle status. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum InvoiceStatus { + /// Awaiting a matching payment. + Open, + /// A received payment matched this invoice. + Paid, + /// Expiry passed before a payment matched. + Expired, +} + +impl InvoiceStatus { + pub fn as_str(self) -> &'static str { + match self { + InvoiceStatus::Open => "open", + InvoiceStatus::Paid => "paid", + InvoiceStatus::Expired => "expired", + } + } + + pub fn parse(s: &str) -> InvoiceStatus { + match s { + "paid" => InvoiceStatus::Paid, + "expired" => InvoiceStatus::Expired, + _ => InvoiceStatus::Open, + } + } +} + +/// How to state an invoice amount at creation: an exact Grin amount, a raw +/// fiat amount plus currency (unpriced), or a fiat amount already priced into +/// Grin by the conversion oracle (milestone 7). +/// +/// The connector/API sends `Grin` or `Fiat`; the server resolves a `Fiat` +/// through the oracle into a `FiatQuoted` (with the locked nanogrin) before +/// persisting, so a fiat invoice's `expected_amount` is filled and it matches +/// by amount. A bare `Fiat` that reaches persistence stays unpriced +/// (`expected_amount` NULL), matchable only by memo or a derived identity. +#[derive(Debug, Clone)] +pub enum AmountSpec { + /// Exact amount in nanogrin. + Grin(u64), + /// Fiat amount (decimal string) in the given ISO currency code, not yet + /// priced (the pre-oracle state; expected_amount stays NULL). + Fiat { amount: String, currency: String }, + /// A fiat amount priced into Grin by the oracle: the locked quote. + FiatQuoted { + /// The original fiat amount (decimal string), echoed for display. + amount: String, + /// The ISO currency code. + currency: String, + /// The locked Grin amount in nanogrin (becomes `expected_amount`). + nanogrin: u64, + /// The rate used, fiat per GRIN (decimal string, for the receipt). + rate: String, + /// The oracle source the rate came from (e.g. `coingecko`). + source: String, + }, +} + +/// Parameters for [`create`]. +#[derive(Debug, Clone)] +pub struct NewInvoice { + /// The store's order reference (also the memo/subject match key). + pub order_ref: Option, + /// The amount, exact Grin or a fiat quote. + pub amount: AmountSpec, + /// A human memo shown on the checkout page. + pub memo: Option, + /// Per-invoice matching-mode override; `None` uses the global default. + pub match_mode: Option, + /// Expiry, seconds from now; `None` means no expiry. + pub expiry_secs: Option, +} + +/// A persisted invoice row. +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct Invoice { + pub id: String, + #[sqlx(rename = "ref")] + pub order_ref: Option, + pub expected_amount: Option, + pub expiry: Option, + pub status: String, + pub created_at: String, + pub token: Option, + pub memo: Option, + pub recipient_pubkey: Option, + pub fiat_amount: Option, + pub fiat_currency: Option, + pub match_mode: Option, + pub paid_payment_id: Option, + pub paid_at: Option, + /// The locked rate (fiat per GRIN) a fiat quote was priced at, else NULL. + pub quote_rate: Option, + /// The oracle source the quote came from (e.g. `coingecko`), else NULL. + pub quote_source: Option, +} + +impl Invoice { + /// The effective matching mode: the per-invoice override, else the global + /// default supplied by the caller. + pub fn effective_mode(&self, default: MatchMode) -> MatchMode { + match self.match_mode.as_deref() { + Some("memo") => MatchMode::Memo, + Some("derived") => MatchMode::Derived, + Some("amount") => MatchMode::Amount, + _ => default, + } + } + + /// The status as a typed enum. + pub fn status(&self) -> InvoiceStatus { + InvoiceStatus::parse(&self.status) + } +} + +fn mode_str(mode: MatchMode) -> &'static str { + match mode { + MatchMode::Memo => "memo", + MatchMode::Derived => "derived", + MatchMode::Amount => "amount", + } +} + +/// Create an invoice: mint an id + checkout token, resolve the recipient +/// identity (a per-invoice derived child in `derived` mode, else the server +/// master key), persist it `open`, and return the row. +/// +/// `master_sk` is the server Nostr secret (used only to derive the child +/// public key; the secret is never stored). `master_pubkey_hex` is the +/// server's own x-only key, used as the recipient for memo/amount invoices. +pub async fn create( + pool: &SqlitePool, + params: NewInvoice, + master_sk: &[u8; 32], + master_pubkey_hex: &str, + default_mode: MatchMode, +) -> Result { + let id = ids::random_id(); + let token = ids::checkout_token(); + let effective = params.match_mode.unwrap_or(default_mode); + + // Derived mode gets a unique per-invoice child key; everything else + // receives on the server's own identity and matches by memo or amount. + let recipient_pubkey = if effective == MatchMode::Derived { + derive::invoice_pubkey_hex(master_sk, &id) + } else { + master_pubkey_hex.to_string() + }; + + let (expected_amount, fiat_amount, fiat_currency, quote_rate, quote_source) = + match ¶ms.amount { + AmountSpec::Grin(nano) => (Some(*nano as i64), None, None, None, None), + AmountSpec::Fiat { amount, currency } => ( + None, + Some(amount.clone()), + Some(currency.clone()), + None, + None, + ), + AmountSpec::FiatQuoted { + amount, + currency, + nanogrin, + rate, + source, + } => ( + Some(*nanogrin as i64), + Some(amount.clone()), + Some(currency.clone()), + Some(rate.clone()), + Some(source.clone()), + ), + }; + + // Store the per-invoice override only when it differs from a bare default, + // so an invoice created under one global default keeps behaving as created + // even if the operator later changes GP_MATCH_MODE. + let stored_mode = params.match_mode.map(mode_str); + + sqlx::query( + "INSERT INTO invoice \ + (id, ref, expected_amount, expiry, status, created_at, token, memo, \ + recipient_pubkey, fiat_amount, fiat_currency, match_mode, \ + quote_rate, quote_source) \ + VALUES (?1, ?2, ?3, \ + CASE WHEN ?4 IS NULL THEN NULL \ + ELSE strftime('%Y-%m-%dT%H:%M:%SZ', 'now', ?4) END, \ + 'open', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), \ + ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + ) + .bind(&id) + .bind(¶ms.order_ref) + .bind(expected_amount) + .bind(params.expiry_secs.map(|s| format!("{s:+} seconds"))) + .bind(&token) + .bind(¶ms.memo) + .bind(&recipient_pubkey) + .bind(&fiat_amount) + .bind(&fiat_currency) + .bind(stored_mode) + .bind("e_rate) + .bind("e_source) + .execute(pool) + .await?; + + get(pool, &id) + .await? + .ok_or_else(|| sqlx::Error::RowNotFound) +} + +const COLUMNS: &str = "id, ref, expected_amount, expiry, status, created_at, token, memo, \ + recipient_pubkey, fiat_amount, fiat_currency, match_mode, paid_payment_id, paid_at, \ + quote_rate, quote_source"; + +/// Fetch an invoice by id, marking it expired first if its expiry has passed. +pub async fn get(pool: &SqlitePool, id: &str) -> Result, sqlx::Error> { + expire_if_due_id(pool, id).await?; + let sql = format!("SELECT {COLUMNS} FROM invoice WHERE id = ?1"); + sqlx::query_as::<_, Invoice>(&sql) + .bind(id) + .fetch_optional(pool) + .await +} + +/// Fetch an invoice by its checkout token (the `/pay/` bearer), +/// marking it expired first if due. +pub async fn get_by_token(pool: &SqlitePool, token: &str) -> Result, sqlx::Error> { + // Expire lazily so the hosted page reflects the true status on load. + sqlx::query( + "UPDATE invoice SET status = 'expired' \ + WHERE token = ?1 AND status = 'open' \ + AND expiry IS NOT NULL AND expiry <= strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", + ) + .bind(token) + .execute(pool) + .await?; + let sql = format!("SELECT {COLUMNS} FROM invoice WHERE token = ?1"); + sqlx::query_as::<_, Invoice>(&sql) + .bind(token) + .fetch_optional(pool) + .await +} + +/// The most recent invoices, newest first (admin listing). +pub async fn list(pool: &SqlitePool, limit: i64) -> Result, sqlx::Error> { + expire_due(pool).await?; + let sql = format!("SELECT {COLUMNS} FROM invoice ORDER BY created_at DESC LIMIT ?1"); + sqlx::query_as::<_, Invoice>(&sql) + .bind(limit) + .fetch_all(pool) + .await +} + +/// Mark an invoice paid, linking the payment that satisfied it. Idempotent: +/// only an `open` invoice transitions, so a replayed match is a no-op. +pub async fn mark_paid( + pool: &SqlitePool, + invoice_id: &str, + payment_id: &str, +) -> Result { + let result = sqlx::query( + "UPDATE invoice SET status = 'paid', paid_payment_id = ?2, \ + paid_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') \ + WHERE id = ?1 AND status = 'open'", + ) + .bind(invoice_id) + .bind(payment_id) + .execute(pool) + .await?; + Ok(result.rows_affected() > 0) +} + +/// Sweep: mark every open invoice whose expiry has passed as expired. +pub async fn expire_due(pool: &SqlitePool) -> Result { + let result = sqlx::query( + "UPDATE invoice SET status = 'expired' \ + WHERE status = 'open' AND expiry IS NOT NULL \ + AND expiry <= strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", + ) + .execute(pool) + .await?; + Ok(result.rows_affected()) +} + +async fn expire_if_due_id(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> { + sqlx::query( + "UPDATE invoice SET status = 'expired' \ + WHERE id = ?1 AND status = 'open' \ + AND expiry IS NOT NULL AND expiry <= strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", + ) + .bind(id) + .execute(pool) + .await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db; + + async fn pool() -> SqlitePool { + // In-memory database, migrated: fast and isolated per test. + db::test_pool().await + } + + const MASTER: [u8; 32] = [3u8; 32]; + const MASTER_PUB: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn grin(nano: u64) -> NewInvoice { + NewInvoice { + order_ref: Some("order-7".into()), + amount: AmountSpec::Grin(nano), + memo: Some("Coffee".into()), + match_mode: None, + expiry_secs: None, + } + } + + #[tokio::test] + async fn create_get_and_token_roundtrip() { + let pool = pool().await; + let inv = create( + &pool, + grin(1_500_000_000), + &MASTER, + MASTER_PUB, + MatchMode::Memo, + ) + .await + .unwrap(); + assert_eq!(inv.status(), InvoiceStatus::Open); + assert_eq!(inv.expected_amount, Some(1_500_000_000)); + assert_eq!(inv.order_ref.as_deref(), Some("order-7")); + let token = inv.token.clone().unwrap(); + assert_eq!(token.len(), 43); + + let by_id = get(&pool, &inv.id).await.unwrap().unwrap(); + assert_eq!(by_id.id, inv.id); + let by_token = get_by_token(&pool, &token).await.unwrap().unwrap(); + assert_eq!(by_token.id, inv.id); + // Memo-mode invoices receive on the master identity. + assert_eq!(by_token.recipient_pubkey.as_deref(), Some(MASTER_PUB)); + } + + #[tokio::test] + async fn derived_mode_gets_a_unique_child_recipient() { + let pool = pool().await; + let mut p = grin(1); + p.match_mode = Some(MatchMode::Derived); + let inv = create(&pool, p, &MASTER, MASTER_PUB, MatchMode::Memo) + .await + .unwrap(); + let recipient = inv.recipient_pubkey.clone().unwrap(); + assert_ne!(recipient, MASTER_PUB, "derived mode must not reuse master"); + // Stateless: recomputing from the invoice id yields the same key. + assert_eq!(recipient, derive::invoice_pubkey_hex(&MASTER, &inv.id)); + assert_eq!(inv.effective_mode(MatchMode::Memo), MatchMode::Derived); + } + + #[tokio::test] + async fn fiat_invoice_has_no_expected_grin_amount_yet() { + let pool = pool().await; + let p = NewInvoice { + order_ref: None, + amount: AmountSpec::Fiat { + amount: "19.99".into(), + currency: "USD".into(), + }, + memo: None, + match_mode: None, + expiry_secs: None, + }; + let inv = create(&pool, p, &MASTER, MASTER_PUB, MatchMode::Amount) + .await + .unwrap(); + assert_eq!(inv.expected_amount, None); + assert_eq!(inv.fiat_amount.as_deref(), Some("19.99")); + assert_eq!(inv.fiat_currency.as_deref(), Some("USD")); + } + + #[tokio::test] + async fn expiry_is_evaluated_lazily() { + let pool = pool().await; + let mut p = grin(1); + p.expiry_secs = Some(-1); // already in the past + let inv = create(&pool, p, &MASTER, MASTER_PUB, MatchMode::Memo) + .await + .unwrap(); + // Fetching it flips open -> expired. + let fetched = get(&pool, &inv.id).await.unwrap().unwrap(); + assert_eq!(fetched.status(), InvoiceStatus::Expired); + } + + #[tokio::test] + async fn mark_paid_is_idempotent() { + let pool = pool().await; + let inv = create(&pool, grin(10), &MASTER, MASTER_PUB, MatchMode::Memo) + .await + .unwrap(); + assert!(mark_paid(&pool, &inv.id, "pay-1").await.unwrap()); + // Second call does not transition again (already paid). + assert!(!mark_paid(&pool, &inv.id, "pay-2").await.unwrap()); + let fetched = get(&pool, &inv.id).await.unwrap().unwrap(); + assert_eq!(fetched.status(), InvoiceStatus::Paid); + assert_eq!(fetched.paid_payment_id.as_deref(), Some("pay-1")); + } +} diff --git a/crates/gp-core/src/lib.rs b/crates/gp-core/src/lib.rs new file mode 100644 index 0000000..b108b77 --- /dev/null +++ b/crates/gp-core/src/lib.rs @@ -0,0 +1,26 @@ +//! GoblinPay domain core. +//! +//! Holds everything that is not transport or wallet crypto: the runtime +//! configuration (env-first, like goblin-nip05d), the SQLite persistence +//! layer, and (in later milestones) invoices, payments, matching, conversion, +//! and notification traits. + +pub mod config; +pub mod db; +pub mod derive; +pub mod endpub; +pub mod ids; +pub mod invoice; +pub mod matching; +pub mod qr; +pub mod rates; +pub mod store; +pub mod webhook; + +use subtle::ConstantTimeEq; + +/// Constant-time byte-string equality, for comparing bearer tokens and other +/// secrets without leaking a timing side channel. +pub fn ct_eq(a: &[u8], b: &[u8]) -> bool { + a.len() == b.len() && bool::from(a.ct_eq(b)) +} diff --git a/crates/gp-core/src/matching.rs b/crates/gp-core/src/matching.rs new file mode 100644 index 0000000..de87dee --- /dev/null +++ b/crates/gp-core/src/matching.rs @@ -0,0 +1,513 @@ +//! The shared matching layer: map one received payment to an open invoice +//! (advancing its status) and to a tenant user (for crediting), composing all +//! three matching modes. +//! +//! An incoming payment carries the identity it was received on (the master key +//! or a per-invoice / per-user derived child), the amount, and an optional +//! memo (the payer's `subject` tag). Resolution tries, in order: +//! +//! 1. **Derived identity** (mode 2) — the recipient pubkey uniquely names a +//! per-invoice child, an O(1) indexed lookup. Recommended for stores. +//! 2. **Memo / reference** (mode 1) — the memo equals the invoice's order ref. +//! 3. **Amount** (mode 3) — the exact expected amount, among unexpired open +//! invoices, oldest first. +//! +//! Each candidate is scoped to invoices whose *effective* mode is that mode +//! (the per-invoice override, else the global default), so an amount-mode +//! invoice is never matched by a same-amount derived-mode invoice and vice +//! versa. User crediting (5b) is resolved independently from the endpub the +//! payment landed on and composes with any invoice match. +//! +//! This runs after the wallet has recorded the payment; it is a pure database +//! operation over synthetic inputs, so every mode is unit-testable without a +//! relay or a wallet. + +use sqlx::SqlitePool; + +use crate::config::MatchMode; +use crate::{endpub, invoice}; + +/// One received payment presented to the matcher. `slate_id` is also the +/// payment row id. +#[derive(Debug, Clone)] +pub struct IncomingPayment<'a> { + pub slate_id: &'a str, + pub amount: u64, + /// The server identity that received it (master or a derived child), + /// x-only hex. + pub recipient_hex: &'a str, + /// The payer's sanitized memo (subject tag), if any. + pub memo: Option<&'a str>, +} + +/// What the payment resolved to. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct MatchResult { + /// The invoice it satisfied, if any. + pub invoice_id: Option, + /// The tenant user it credits, if the endpub belongs to one. + pub user_id: Option, +} + +/// Resolve `incoming` against the open invoices and endpubs, mark a matched +/// invoice paid, and link the payment row to the invoice + user. Returns what +/// it matched. +pub async fn match_payment( + pool: &SqlitePool, + default_mode: MatchMode, + incoming: &IncomingPayment<'_>, +) -> Result { + let default = mode_str(default_mode); + + // 5b: which tenant user does this endpub credit? Independent of invoices. + let user_id = endpub::user_for_pubkey(pool, incoming.recipient_hex) + .await? + .map(|(user, _epoch)| user); + + // Invoice resolution, first hit wins across the three scoped modes. + let invoice_id = resolve_derived(pool, default, incoming.recipient_hex).await?; + let invoice_id = match invoice_id { + Some(id) => Some(id), + None => match incoming.memo { + Some(memo) => resolve_memo(pool, default, memo).await?, + None => None, + }, + }; + let invoice_id = match invoice_id { + Some(id) => Some(id), + None => resolve_amount(pool, default, incoming.amount).await?, + }; + + if let Some(id) = &invoice_id { + invoice::mark_paid(pool, id, incoming.slate_id).await?; + } + + // Link the payment row to whatever it resolved to (both optional). + sqlx::query("UPDATE payment SET invoice_id = ?2, user_id = ?3 WHERE id = ?1") + .bind(incoming.slate_id) + .bind(&invoice_id) + .bind(&user_id) + .execute(pool) + .await?; + + Ok(MatchResult { + invoice_id, + user_id, + }) +} + +fn mode_str(mode: MatchMode) -> &'static str { + match mode { + MatchMode::Memo => "memo", + MatchMode::Derived => "derived", + MatchMode::Amount => "amount", + } +} + +async fn resolve_derived( + pool: &SqlitePool, + default: &str, + recipient_hex: &str, +) -> Result, sqlx::Error> { + sqlx::query_scalar( + "SELECT id FROM invoice \ + WHERE recipient_pubkey = ?1 AND status = 'open' \ + AND COALESCE(match_mode, ?2) = 'derived' \ + ORDER BY created_at LIMIT 1", + ) + .bind(recipient_hex) + .bind(default) + .fetch_optional(pool) + .await +} + +async fn resolve_memo( + pool: &SqlitePool, + default: &str, + memo: &str, +) -> Result, sqlx::Error> { + sqlx::query_scalar( + "SELECT id FROM invoice \ + WHERE ref = ?1 AND status = 'open' \ + AND COALESCE(match_mode, ?2) = 'memo' \ + ORDER BY created_at LIMIT 1", + ) + .bind(memo) + .bind(default) + .fetch_optional(pool) + .await +} + +async fn resolve_amount( + pool: &SqlitePool, + default: &str, + amount: u64, +) -> Result, sqlx::Error> { + sqlx::query_scalar( + "SELECT id FROM invoice \ + WHERE expected_amount = ?1 AND status = 'open' \ + AND COALESCE(match_mode, ?2) = 'amount' \ + AND (expiry IS NULL OR expiry > strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) \ + ORDER BY created_at LIMIT 1", + ) + .bind(amount as i64) + .bind(default) + .fetch_optional(pool) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db; + use crate::invoice::{AmountSpec, NewInvoice}; + + async fn pool() -> SqlitePool { + db::test_pool().await + } + + const MASTER: [u8; 32] = [11u8; 32]; + const MASTER_PUB: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + /// Insert a payment row the way the ingest adapter does, so matching has a + /// row to link. + async fn insert_payment(pool: &SqlitePool, slate_id: &str, amount: u64, recipient: &str) { + sqlx::query( + "INSERT INTO payment (id, amount, payer, slate_id, recipient, status, created_at) \ + VALUES (?1, ?2, 'payerhex', ?1, ?3, 'received', \ + strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))", + ) + .bind(slate_id) + .bind(amount as i64) + .bind(recipient) + .execute(pool) + .await + .unwrap(); + } + + fn new(amount: AmountSpec, order_ref: Option<&str>, mode: Option) -> NewInvoice { + NewInvoice { + order_ref: order_ref.map(|s| s.to_string()), + amount, + memo: None, + match_mode: mode, + expiry_secs: None, + } + } + + #[tokio::test] + async fn memo_mode_matches_by_order_ref() { + let pool = pool().await; + let inv = invoice::create( + &pool, + new( + AmountSpec::Grin(100), + Some("order-42"), + Some(MatchMode::Memo), + ), + &MASTER, + MASTER_PUB, + MatchMode::Memo, + ) + .await + .unwrap(); + insert_payment(&pool, "slate-a", 100, MASTER_PUB).await; + + let result = match_payment( + &pool, + MatchMode::Memo, + &IncomingPayment { + slate_id: "slate-a", + amount: 100, + recipient_hex: MASTER_PUB, + memo: Some("order-42"), + }, + ) + .await + .unwrap(); + + assert_eq!(result.invoice_id.as_deref(), Some(inv.id.as_str())); + assert_eq!( + invoice::get(&pool, &inv.id) + .await + .unwrap() + .unwrap() + .status(), + invoice::InvoiceStatus::Paid + ); + // The payment row is linked back. + let linked: Option = + sqlx::query_scalar("SELECT invoice_id FROM payment WHERE id = 'slate-a'") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(linked.as_deref(), Some(inv.id.as_str())); + } + + #[tokio::test] + async fn derived_mode_matches_by_recipient_identity() { + let pool = pool().await; + let inv = invoice::create( + &pool, + new(AmountSpec::Grin(100), None, Some(MatchMode::Derived)), + &MASTER, + MASTER_PUB, + MatchMode::Memo, + ) + .await + .unwrap(); + let recipient = inv.recipient_pubkey.clone().unwrap(); + insert_payment(&pool, "slate-b", 999, &recipient).await; + + // Even with a mismatched amount and no memo, the derived identity is + // unambiguous. + let result = match_payment( + &pool, + MatchMode::Memo, + &IncomingPayment { + slate_id: "slate-b", + amount: 999, + recipient_hex: &recipient, + memo: None, + }, + ) + .await + .unwrap(); + assert_eq!(result.invoice_id.as_deref(), Some(inv.id.as_str())); + } + + #[tokio::test] + async fn amount_mode_matches_exact_amount_oldest_first() { + let pool = pool().await; + let first = invoice::create( + &pool, + new( + AmountSpec::Grin(2_000_000_000), + None, + Some(MatchMode::Amount), + ), + &MASTER, + MASTER_PUB, + MatchMode::Amount, + ) + .await + .unwrap(); + // A second same-amount invoice; the oldest open one wins. + let _second = invoice::create( + &pool, + new( + AmountSpec::Grin(2_000_000_000), + None, + Some(MatchMode::Amount), + ), + &MASTER, + MASTER_PUB, + MatchMode::Amount, + ) + .await + .unwrap(); + insert_payment(&pool, "slate-c", 2_000_000_000, MASTER_PUB).await; + + let result = match_payment( + &pool, + MatchMode::Amount, + &IncomingPayment { + slate_id: "slate-c", + amount: 2_000_000_000, + recipient_hex: MASTER_PUB, + memo: None, + }, + ) + .await + .unwrap(); + assert_eq!(result.invoice_id.as_deref(), Some(first.id.as_str())); + } + + #[tokio::test] + async fn mode_scoping_prevents_cross_mode_amount_collision() { + let pool = pool().await; + // A derived-mode invoice with the same amount must NOT be matched by an + // amount-only payment on the master identity. + let _derived = invoice::create( + &pool, + new(AmountSpec::Grin(500), None, Some(MatchMode::Derived)), + &MASTER, + MASTER_PUB, + MatchMode::Amount, + ) + .await + .unwrap(); + insert_payment(&pool, "slate-d", 500, MASTER_PUB).await; + + let result = match_payment( + &pool, + MatchMode::Amount, + &IncomingPayment { + slate_id: "slate-d", + amount: 500, + recipient_hex: MASTER_PUB, + memo: None, + }, + ) + .await + .unwrap(); + // No amount-mode invoice exists, so nothing matches. + assert_eq!(result.invoice_id, None); + } + + #[tokio::test] + async fn credits_a_user_via_the_endpub_and_composes_with_invoices() { + let pool = pool().await; + let (_user, ep) = endpub::create_user(&pool, &MASTER, Some("alice".into()), None) + .await + .unwrap(); + insert_payment(&pool, "slate-e", 7, &ep.pubkey).await; + + let result = match_payment( + &pool, + MatchMode::Memo, + &IncomingPayment { + slate_id: "slate-e", + amount: 7, + recipient_hex: &ep.pubkey, + memo: None, + }, + ) + .await + .unwrap(); + assert_eq!(result.user_id.as_deref(), Some("alice")); + assert_eq!(result.invoice_id, None); + + // The payment is credited to the user (balance reflects it). + let balances = endpub::list_with_balances(&pool).await.unwrap(); + assert_eq!(balances[0].user_id, "alice"); + assert_eq!(balances[0].balance, 7); + } + + #[tokio::test] + async fn fiat_quoted_invoice_matches_a_synthetic_payment_of_the_quoted_amount() { + use crate::rates::Oracle; + + let pool = pool().await; + // Inject a fixed rate (no network): 0.02 USD/GRIN, so 10.00 USD is + // 500 GRIN = 500_000_000_000 nanogrin. + let oracle = Oracle::fixed(&["usd"], 0.02, 900); + let quote = oracle.quote("10.00", "USD").await.unwrap(); + assert_eq!(quote.nanogrin, 500_000_000_000); + + // Create the fiat invoice priced by the oracle, amount-matched. + let inv = invoice::create( + &pool, + NewInvoice { + order_ref: None, + amount: AmountSpec::FiatQuoted { + amount: "10.00".into(), + currency: "USD".into(), + nanogrin: quote.nanogrin, + rate: crate::rates::format_rate(quote.fiat_per_grin), + source: quote.source.to_string(), + }, + memo: None, + match_mode: Some(MatchMode::Amount), + expiry_secs: Some(900), + }, + &MASTER, + MASTER_PUB, + MatchMode::Amount, + ) + .await + .unwrap(); + // The gap M5 left is filled: expected_amount is the locked nanogrin, and + // the quote (rate + source) is stored. + assert_eq!(inv.expected_amount, Some(500_000_000_000)); + assert_eq!(inv.fiat_amount.as_deref(), Some("10.00")); + assert_eq!(inv.fiat_currency.as_deref(), Some("USD")); + assert_eq!(inv.quote_rate.as_deref(), Some("0.02")); + assert_eq!(inv.quote_source.as_deref(), Some("coingecko")); + + // A payment of exactly the quoted amount matches by amount. + insert_payment(&pool, "slate-fiat", 500_000_000_000, MASTER_PUB).await; + let result = match_payment( + &pool, + MatchMode::Amount, + &IncomingPayment { + slate_id: "slate-fiat", + amount: 500_000_000_000, + recipient_hex: MASTER_PUB, + memo: None, + }, + ) + .await + .unwrap(); + assert_eq!(result.invoice_id.as_deref(), Some(inv.id.as_str())); + } + + #[tokio::test] + async fn expired_fiat_quote_is_not_matched_and_forces_a_requote() { + let pool = pool().await; + // A fiat quote whose lock window already elapsed (expiry in the past). + let inv = invoice::create( + &pool, + NewInvoice { + order_ref: None, + amount: AmountSpec::FiatQuoted { + amount: "10.00".into(), + currency: "usd".into(), + nanogrin: 500_000_000_000, + rate: "0.02".into(), + source: "coingecko".into(), + }, + memo: None, + match_mode: Some(MatchMode::Amount), + expiry_secs: Some(-1), + }, + &MASTER, + MASTER_PUB, + MatchMode::Amount, + ) + .await + .unwrap(); + insert_payment(&pool, "slate-late", 500_000_000_000, MASTER_PUB).await; + let result = match_payment( + &pool, + MatchMode::Amount, + &IncomingPayment { + slate_id: "slate-late", + amount: 500_000_000_000, + recipient_hex: MASTER_PUB, + memo: None, + }, + ) + .await + .unwrap(); + // The stale-locked quote does not match; the checkout must re-quote. + assert_eq!(result.invoice_id, None); + assert_eq!( + invoice::get(&pool, &inv.id) + .await + .unwrap() + .unwrap() + .status(), + invoice::InvoiceStatus::Expired + ); + } + + #[tokio::test] + async fn unmatched_payment_returns_empty() { + let pool = pool().await; + insert_payment(&pool, "slate-f", 1, MASTER_PUB).await; + let result = match_payment( + &pool, + MatchMode::Memo, + &IncomingPayment { + slate_id: "slate-f", + amount: 1, + recipient_hex: MASTER_PUB, + memo: Some("no-such-order"), + }, + ) + .await + .unwrap(); + assert_eq!(result, MatchResult::default()); + } +} diff --git a/crates/gp-core/src/qr.rs b/crates/gp-core/src/qr.rs new file mode 100644 index 0000000..dced115 --- /dev/null +++ b/crates/gp-core/src/qr.rs @@ -0,0 +1,130 @@ +//! Server-rendered QR codes as SVG, zero JavaScript. +//! +//! The QR is always generated at error-correction level **H** (tolerates +//! ~30% occlusion) so an optional centered logo, sized to ~22% of the code, +//! never breaks scannability. The logo is a white rounded backing rectangle +//! plus one `` element referencing a statically served asset (the +//! Goblin mark by default, or the operator's own via `GP_QR_LOGO`). With no +//! logo it is a plain black-on-white QR. +//! +//! Rendering is hand-rolled (one `` of the dark modules) so the crate +//! needs only the `qrcode` matrix, not its image/SVG feature or any raster +//! dependency, and we keep full control of the logo overlay. + +use qrcode::{Color, EcLevel, QrCode}; + +/// Logo size as a fraction of the QR width (safe under ECC level H). +pub const LOGO_FRACTION: f64 = 0.22; +/// Quiet zone in modules on every side (the QR spec's required margin). +const QUIET: u32 = 4; + +/// Failed to build a QR (e.g. the payload exceeds the largest QR version). +#[derive(Debug)] +pub struct QrError(pub String); + +impl std::fmt::Display for QrError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "qr error: {}", self.0) + } +} + +impl std::error::Error for QrError {} + +/// Render `data` as an SVG string at ECC level H. When `logo_href` is set, a +/// white rounded rectangle plus a centered `` are overlaid. The SVG +/// scales to its container; a `viewBox` in module units keeps it crisp. +pub fn svg(data: &str, logo_href: Option<&str>) -> Result { + let code = QrCode::with_error_correction_level(data.as_bytes(), EcLevel::H) + .map_err(|e| QrError(e.to_string()))?; + let width = code.width() as u32; + let colors = code.to_colors(); + let dim = width + 2 * QUIET; + + // One path for every dark module (each a 1x1 unit square), offset by the + // quiet zone. + let mut path = String::new(); + for y in 0..width { + for x in 0..width { + if colors[(y * width + x) as usize] == Color::Dark { + let px = x + QUIET; + let py = y + QUIET; + path.push_str(&format!("M{px} {py}h1v1h-1z")); + } + } + } + + let mut svg = format!( + "\ + \ + " + ); + + if let Some(href) = logo_href { + // Center a logo sized to LOGO_FRACTION of the code (module units), + // on a slightly larger white rounded backing so it reads cleanly. + let logo = (dim as f64 * LOGO_FRACTION).round(); + let pad = 1.0_f64; + let back = logo + 2.0 * pad; + let center = dim as f64 / 2.0; + let back_x = center - back / 2.0; + let back_y = center - back / 2.0; + let logo_x = center - logo / 2.0; + let logo_y = center - logo / 2.0; + let radius = back * 0.18; + svg.push_str(&format!( + "\ + " + )); + } + + svg.push_str(""); + Ok(svg) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_valid_svg_at_ecc_h() { + let out = svg("grin1qtestaddressdata", None).unwrap(); + assert!(out.starts_with("")); + assert!(out.contains("viewBox")); + // No script, no external CSS: zero JS by construction. + assert!(!out.contains("node reads. This crate +//! has no Nym linkage at all, so the direct path is structural, not configured. +//! +//! Design: +//! - **Source** (`GP_RATE_SOURCE`, default `coingecko`): where the GRIN price +//! comes from. CoinGecko lists GRIN under id `grin` and prices many fiats in +//! one call (`/simple/price?ids=grin&vs_currencies=usd,eur,...`). +//! - **Rate cache** (`GP_RATE_CACHE_TTL`, default 60s): a fetched rate is +//! reused for the TTL so concurrent checkouts do not hammer the source. +//! - **Quote lock** (`GP_QUOTE_TTL`, default 900s): a created invoice locks its +//! Grin amount for this window (its `expiry`); an amount-match past the lock +//! re-quotes rather than honouring a stale rate. +//! - **Stale fallback** (`GP_RATE_STALE_MAX`, default 0 = off): if a live fetch +//! fails but the last cached rate is within this bound, serve it (flagged +//! `stale`) instead of failing the checkout. 0 keeps the strict fail-fast. +//! +//! Testing: the conversion math, the CoinGecko parser (against a recorded +//! response fixture), the quote-lock predicate, and the cache-freshness logic +//! are all pure and unit-tested here. No test touches the network; the live +//! fetch path is exercised in the supervised integration round, the same +//! precedent as the M4 confirmation "found" path. + +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +use crate::config::{Config, RateSource}; + +/// The CoinGecko coin id for GRIN. +const COINGECKO_GRIN_ID: &str = "grin"; +/// CoinGecko simple-price endpoint base (host-only kept for the log line). +const COINGECKO_BASE: &str = "https://api.coingecko.com/api/v3/simple/price"; +/// Per-request timeout for the oracle fetch (a single small JSON GET). +const FETCH_TIMEOUT: Duration = Duration::from_secs(10); + +/// Why a quote could not be produced. Mapped to a clear HTTP error by the +/// create-invoice handler so an unpriceable invoice is never silently created. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RateError { + /// The requested currency is not in `GP_RATE_CURRENCIES` (a 400: the caller + /// must send a supported currency). Checked before any network call. + UnsupportedCurrency(String), + /// The fiat amount could not be parsed as a non-negative decimal (a 400). + BadAmount(String), + /// No fresh rate and no usable stale fallback (source unreachable or the + /// response had no price for the currency): a 502, fail fast. + SourceUnavailable(String), + /// Misconfiguration (an unknown source reached the oracle): a 500. + Config(String), +} + +impl std::fmt::Display for RateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RateError::UnsupportedCurrency(c) => { + write!(f, "currency `{c}` is not enabled (see GP_RATE_CURRENCIES)") + } + RateError::BadAmount(a) => write!(f, "fiat amount `{a}` is not a valid decimal"), + RateError::SourceUnavailable(m) => write!(f, "price oracle unavailable: {m}"), + RateError::Config(m) => write!(f, "rate oracle misconfigured: {m}"), + } + } +} + +impl std::error::Error for RateError {} + +/// A locked quote: the priced Grin amount plus the rate and source it was +/// derived from, echoed onto the invoice for the receipt/audit trail. +#[derive(Debug, Clone, PartialEq)] +pub struct Quote { + /// The locked Grin amount in nanogrin (the invoice `expected_amount`). + pub nanogrin: u64, + /// The currency the quote is in (lowercased ISO code). + pub currency: String, + /// The rate used: fiat units per one GRIN (the price of 1 GRIN). + pub fiat_per_grin: f64, + /// The source the rate came from (e.g. `coingecko`). + pub source: &'static str, + /// True when served from a stale cache entry (a fallback, not a fresh fetch). + pub stale: bool, +} + +/// Parse a fiat amount decimal string into an `f64`, rejecting anything that is +/// not a finite, non-negative number. +pub fn parse_fiat_amount(amount: &str) -> Result { + let trimmed = amount.trim(); + let value: f64 = trimmed + .parse() + .map_err(|_| RateError::BadAmount(amount.to_string()))?; + if !value.is_finite() || value < 0.0 { + return Err(RateError::BadAmount(amount.to_string())); + } + Ok(value) +} + +/// Convert a fiat amount to nanogrin at a given rate (fiat units per one GRIN), +/// rounding to the nearest nanogrin (1 GRIN = 1e9 nanogrin). +/// +/// `grin = fiat / fiat_per_grin`, then `nanogrin = round(grin * 1e9)`. Pure and +/// deterministic for a fixed `(fiat, rate)`, so the rounding is unit-tested. +pub fn fiat_to_nanogrin(fiat_amount: f64, fiat_per_grin: f64) -> Result { + if !fiat_per_grin.is_finite() || fiat_per_grin <= 0.0 { + return Err(RateError::SourceUnavailable(format!( + "non-positive rate {fiat_per_grin}" + ))); + } + if !fiat_amount.is_finite() || fiat_amount < 0.0 { + return Err(RateError::BadAmount(fiat_amount.to_string())); + } + let nano = (fiat_amount / fiat_per_grin * 1e9).round(); + if !nano.is_finite() || nano < 0.0 || nano > u64::MAX as f64 { + return Err(RateError::BadAmount(format!( + "amount {fiat_amount} at rate {fiat_per_grin} overflows nanogrin" + ))); + } + Ok(nano as u64) +} + +/// Format a rate for storage/display: fiat-per-GRIN to a fixed precision, +/// trailing zeros trimmed. Used for the invoice `quote_rate` column. +pub fn format_rate(fiat_per_grin: f64) -> String { + let s = format!("{fiat_per_grin:.10}"); + let trimmed = s.trim_end_matches('0').trim_end_matches('.'); + if trimmed.is_empty() { + "0".to_string() + } else { + trimmed.to_string() + } +} + +/// Parse a CoinGecko `/simple/price` response, returning the fiat-per-GRIN +/// price for `currency` (case-insensitive). The response shape is +/// `{"grin":{"usd":0.021,"eur":0.018}}`. +pub fn parse_coingecko(json: &str, currency: &str) -> Result { + let value: serde_json::Value = serde_json::from_str(json) + .map_err(|e| RateError::SourceUnavailable(format!("bad JSON from coingecko: {e}")))?; + let cur = currency.to_lowercase(); + value + .get(COINGECKO_GRIN_ID) + .and_then(|m| m.get(&cur)) + .and_then(|v| v.as_f64()) + .filter(|p| p.is_finite() && *p > 0.0) + .ok_or_else(|| { + RateError::SourceUnavailable(format!("coingecko returned no `{cur}` price for grin")) + }) +} + +/// Whether a quote locked at `quoted_at_unix` for `ttl_secs` is still valid at +/// `now_unix`. The pure predicate behind the invoice `expiry` column: a quote +/// is honoured only inside its lock window; past it, the amount-match fails and +/// the checkout re-quotes. +pub fn quote_valid(quoted_at_unix: i64, ttl_secs: i64, now_unix: i64) -> bool { + now_unix >= quoted_at_unix && now_unix < quoted_at_unix.saturating_add(ttl_secs) +} + +/// One cached rate for a currency: the price and when it was fetched. +#[derive(Debug, Clone, Copy)] +struct CachedRate { + fiat_per_grin: f64, + fetched: Instant, +} + +/// The configurable price oracle. Holds the supported currency set, the cache, +/// and the lock/TTL knobs; the live fetch reuses one reqwest client. +pub struct Oracle { + source: RateSource, + /// Supported fiat currencies (lowercased ISO codes). + currencies: Vec, + cache_ttl: Duration, + stale_max: Duration, + /// The invoice quote-lock window in seconds (`GP_QUOTE_TTL`). + quote_ttl_secs: i64, + cache: Mutex>, + /// A shared HTTP client (DIRECT, never Nym). `None` for a fixed/test oracle + /// whose cache is pre-seeded so it never fetches. + client: Option, +} + +impl Oracle { + /// Build the oracle from the resolved config: the live CoinGecko client with + /// the configured currency set and lock/TTL windows. + pub fn from_config(cfg: &Config) -> Oracle { + let client = reqwest::Client::builder() + .timeout(FETCH_TIMEOUT) + // CoinGecko 403s the default reqwest agent from datacenter IPs; a + // browser-style UA is accepted (verified from the us-east host). + .user_agent("Mozilla/5.0 (compatible; GoblinPay/0.1)") + .build() + .ok(); + Oracle { + source: cfg.rate_source, + currencies: cfg.rate_currencies.clone(), + cache_ttl: Duration::from_secs(cfg.rate_cache_ttl.max(0) as u64), + stale_max: Duration::from_secs(cfg.rate_stale_max.max(0) as u64), + quote_ttl_secs: cfg.quote_ttl, + cache: Mutex::new(HashMap::new()), + client, + } + } + + /// A network-free oracle with a fixed rate for every supported currency, for + /// tests and air-gapped/offline operation: the cache is pre-seeded fresh so + /// `quote` never fetches. `quote_ttl_secs` sets the lock window. + pub fn fixed(currencies: &[&str], fiat_per_grin: f64, quote_ttl_secs: i64) -> Oracle { + let mut cache = HashMap::new(); + let now = Instant::now(); + for c in currencies { + cache.insert( + c.to_lowercase(), + CachedRate { + fiat_per_grin, + fetched: now, + }, + ); + } + Oracle { + source: RateSource::CoinGecko, + currencies: currencies.iter().map(|c| c.to_lowercase()).collect(), + // A very long freshness so the seeded entry is always used. + cache_ttl: Duration::from_secs(u32::MAX as u64), + stale_max: Duration::ZERO, + quote_ttl_secs, + cache: Mutex::new(cache), + client: None, + } + } + + /// The quote-lock window in seconds (the fiat invoice's expiry). + pub fn quote_ttl_secs(&self) -> i64 { + self.quote_ttl_secs + } + + /// Whether a currency is enabled (case-insensitive). + pub fn supports(&self, currency: &str) -> bool { + let cur = currency.to_lowercase(); + self.currencies.contains(&cur) + } + + /// Quote a `{fiat amount, currency}` into a locked Grin amount. + /// + /// Fails fast when the currency is not enabled (no network call), the amount + /// is malformed, or no fresh/stale rate can be sourced. On success the + /// returned [`Quote`] carries the nanogrin the invoice `expected_amount` is + /// set to plus the rate/source for the audit trail. + pub async fn quote(&self, fiat_amount: &str, currency: &str) -> Result { + if !self.supports(currency) { + return Err(RateError::UnsupportedCurrency(currency.to_string())); + } + let amount = parse_fiat_amount(fiat_amount)?; + let cur = currency.to_lowercase(); + + let (fiat_per_grin, stale) = self.rate_for(&cur).await?; + let nanogrin = fiat_to_nanogrin(amount, fiat_per_grin)?; + Ok(Quote { + nanogrin, + currency: cur, + fiat_per_grin, + source: self.source.as_str(), + stale, + }) + } + + /// Resolve a currency's fiat-per-GRIN rate: a fresh cache hit, else a live + /// fetch, else a stale-cache fallback within `GP_RATE_STALE_MAX`. Returns + /// `(rate, stale)`. + async fn rate_for(&self, cur: &str) -> Result<(f64, bool), RateError> { + let now = Instant::now(); + // Fresh cache hit. + if let Some(entry) = self.cache_get(cur) { + if is_fresh(now.saturating_duration_since(entry.fetched), self.cache_ttl) { + return Ok((entry.fiat_per_grin, false)); + } + } + // Live fetch (DIRECT). + match self.fetch(cur).await { + Ok(rate) => { + self.cache_put(cur, rate, now); + Ok((rate, false)) + } + Err(fetch_err) => { + // Stale fallback within the bounded window, if any. + if let Some(entry) = self.cache_get(cur) { + if !self.stale_max.is_zero() + && is_fresh(now.saturating_duration_since(entry.fetched), self.stale_max) + { + log::warn!( + "rates: {} fetch failed, serving stale {cur} rate: {fetch_err}", + self.source.as_str() + ); + return Ok((entry.fiat_per_grin, true)); + } + } + Err(fetch_err) + } + } + } + + /// The live, DIRECT HTTP fetch for one currency's GRIN price. Never called + /// by the fixed/test oracle (its cache is always fresh). + async fn fetch(&self, cur: &str) -> Result { + let client = self + .client + .as_ref() + .ok_or_else(|| RateError::SourceUnavailable("HTTP client unavailable".into()))?; + match self.source { + RateSource::CoinGecko => { + let url = format!("{COINGECKO_BASE}?ids={COINGECKO_GRIN_ID}&vs_currencies={cur}"); + let resp = client.get(&url).send().await.map_err(|e| { + RateError::SourceUnavailable(format!("coingecko request failed: {e}")) + })?; + if !resp.status().is_success() { + return Err(RateError::SourceUnavailable(format!( + "coingecko HTTP {}", + resp.status().as_u16() + ))); + } + let body = resp.text().await.map_err(|e| { + RateError::SourceUnavailable(format!("coingecko body read failed: {e}")) + })?; + parse_coingecko(&body, cur) + } + } + } + + fn cache_get(&self, cur: &str) -> Option { + self.cache.lock().ok().and_then(|m| m.get(cur).copied()) + } + + fn cache_put(&self, cur: &str, fiat_per_grin: f64, fetched: Instant) { + if let Ok(mut m) = self.cache.lock() { + m.insert( + cur.to_string(), + CachedRate { + fiat_per_grin, + fetched, + }, + ); + } + } +} + +/// Whether an entry aged `age` is still fresh under `ttl`. A zero TTL means +/// "always refetch" (never fresh). +fn is_fresh(age: Duration, ttl: Duration) -> bool { + !ttl.is_zero() && age <= ttl +} + +#[cfg(test)] +mod tests { + use super::*; + + // A REAL CoinGecko `/simple/price?ids=grin&vs_currencies=usd,eur,gbp` + // response, captured read-only 2026-07-01. GRIN is listed under id `grin`. + // The parser is asserted against this exact wire shape so a production + // response and this test agree; no test hits the live oracle. + const COINGECKO_FIXTURE: &str = + r#"{"grin":{"usd":0.02097549,"eur":0.01841713,"gbp":0.01577731}}"#; + + #[test] + fn parses_recorded_coingecko_fixture() { + assert_eq!( + parse_coingecko(COINGECKO_FIXTURE, "usd").unwrap(), + 0.02097549 + ); + assert_eq!( + parse_coingecko(COINGECKO_FIXTURE, "eur").unwrap(), + 0.01841713 + ); + assert_eq!( + parse_coingecko(COINGECKO_FIXTURE, "gbp").unwrap(), + 0.01577731 + ); + // Case-insensitive currency selection. + assert_eq!( + parse_coingecko(COINGECKO_FIXTURE, "USD").unwrap(), + 0.02097549 + ); + } + + #[test] + fn coingecko_missing_currency_is_source_error() { + // A currency not present in the response is a source error, not a panic. + assert!(matches!( + parse_coingecko(COINGECKO_FIXTURE, "jpy"), + Err(RateError::SourceUnavailable(_)) + )); + assert!(matches!( + parse_coingecko("not json", "usd"), + Err(RateError::SourceUnavailable(_)) + )); + } + + #[test] + fn conversion_rounds_to_nearest_nanogrin() { + // Clean case: 10.00 USD at 0.02 USD/GRIN = 500 GRIN exactly. + assert_eq!(fiat_to_nanogrin(10.0, 0.02).unwrap(), 500_000_000_000); + // Rounding case: 1.00 at 0.03 = 33.3333... GRIN -> 33_333_333_333 nano. + assert_eq!(fiat_to_nanogrin(1.0, 0.03).unwrap(), 33_333_333_333); + // A tiny amount still rounds to the nearest nanogrin. + assert_eq!(fiat_to_nanogrin(0.00000000002, 0.02).unwrap(), 1); + // Zero fiat is a zero-nanogrin quote, not an error. + assert_eq!(fiat_to_nanogrin(0.0, 0.02).unwrap(), 0); + } + + #[test] + fn conversion_rejects_bad_inputs() { + assert!(matches!( + fiat_to_nanogrin(10.0, 0.0), + Err(RateError::SourceUnavailable(_)) + )); + assert!(matches!( + fiat_to_nanogrin(10.0, -1.0), + Err(RateError::SourceUnavailable(_)) + )); + assert!(matches!( + fiat_to_nanogrin(-1.0, 0.02), + Err(RateError::BadAmount(_)) + )); + assert!(matches!( + fiat_to_nanogrin(f64::NAN, 0.02), + Err(RateError::BadAmount(_)) + )); + } + + #[test] + fn parses_fiat_amount_strings() { + assert_eq!(parse_fiat_amount("19.99").unwrap(), 19.99); + assert_eq!(parse_fiat_amount(" 5 ").unwrap(), 5.0); + assert_eq!(parse_fiat_amount("0").unwrap(), 0.0); + assert!(matches!( + parse_fiat_amount("abc"), + Err(RateError::BadAmount(_)) + )); + assert!(matches!( + parse_fiat_amount("-3.00"), + Err(RateError::BadAmount(_)) + )); + } + + #[test] + fn rate_formatting_trims_zeros() { + assert_eq!(format_rate(0.02097549), "0.02097549"); + assert_eq!(format_rate(0.02), "0.02"); + assert_eq!(format_rate(1.0), "1"); + } + + #[test] + fn quote_lock_expires_after_ttl() { + // Locked at t=1000 for 900s: valid inside the window, rejected past it. + assert!(quote_valid(1000, 900, 1000)); // at lock time + assert!(quote_valid(1000, 900, 1899)); // last valid second + assert!(!quote_valid(1000, 900, 1900)); // TTL elapsed -> re-quote + assert!(!quote_valid(1000, 900, 2500)); // long past + assert!(!quote_valid(1000, 900, 999)); // before the lock (clock skew) + } + + #[test] + fn cache_freshness_respects_ttl() { + assert!(is_fresh(Duration::from_secs(30), Duration::from_secs(60))); + assert!(is_fresh(Duration::from_secs(60), Duration::from_secs(60))); + assert!(!is_fresh(Duration::from_secs(61), Duration::from_secs(60))); + // A zero TTL is never fresh (always refetch). + assert!(!is_fresh(Duration::ZERO, Duration::ZERO)); + } + + #[tokio::test] + async fn fixed_oracle_quotes_without_network() { + // 0.02 USD per GRIN, so 10.00 USD = 500 GRIN. + let oracle = Oracle::fixed(&["usd", "eur"], 0.02, 900); + let q = oracle.quote("10.00", "usd").await.unwrap(); + assert_eq!(q.nanogrin, 500_000_000_000); + assert_eq!(q.currency, "usd"); + assert_eq!(q.fiat_per_grin, 0.02); + assert!(!q.stale); + assert_eq!(oracle.quote_ttl_secs(), 900); + // Case-insensitive on the way in, lowercased out. + let q2 = oracle.quote("10.00", "USD").await.unwrap(); + assert_eq!(q2.nanogrin, 500_000_000_000); + } + + #[tokio::test] + async fn fixed_oracle_rejects_unsupported_currency_before_any_fetch() { + let oracle = Oracle::fixed(&["usd"], 0.02, 900); + assert_eq!( + oracle.quote("10.00", "jpy").await, + Err(RateError::UnsupportedCurrency("jpy".into())) + ); + assert!(!oracle.supports("jpy")); + assert!(oracle.supports("USD")); + } + + #[tokio::test] + async fn fixed_oracle_rejects_bad_amount() { + let oracle = Oracle::fixed(&["usd"], 0.02, 900); + assert!(matches!( + oracle.quote("not-a-number", "usd").await, + Err(RateError::BadAmount(_)) + )); + } +} diff --git a/crates/gp-core/src/store.rs b/crates/gp-core/src/store.rs new file mode 100644 index 0000000..dc83999 --- /dev/null +++ b/crates/gp-core/src/store.rs @@ -0,0 +1,99 @@ +//! The store-connector seam. +//! +//! Every store integration (the built-in generic REST connector, the +//! WooCommerce and Medusa plugins that arrive in a later milestone, and the +//! future pop-up Nostr store) drives GoblinPay through one uniform contract: +//! a create-invoice request in, a hosted checkout + signed webhook out. This +//! trait keeps that mapping in one place so the core never grows per-store +//! branches: a connector only decides how a store's order becomes invoice +//! parameters and where its payment webhooks go. + +use crate::config::MatchMode; +use crate::invoice::{AmountSpec, NewInvoice}; + +/// A store's request to create an invoice, uniform across connectors. +#[derive(Debug, Clone)] +pub struct CreateInvoiceRequest { + /// The store's order reference (also the memo/subject match key). + pub order_ref: Option, + /// The amount, exact Grin or a fiat quote. + pub amount: AmountSpec, + /// A human memo for the checkout page. + pub memo: Option, + /// Per-invoice matching-mode override; `None` uses the global default. + pub match_mode: Option, + /// Expiry in seconds from now; `None` for no expiry. + pub expiry_secs: Option, +} + +/// The uniform connector contract. Implementors translate a store request into +/// invoice parameters and advertise where payment webhooks should be sent. +pub trait StoreConnector: Send + Sync { + /// Stable connector id (e.g. `rest`, `woocommerce`, `medusa`). + fn id(&self) -> &str; + + /// Map a store request into invoice-creation parameters. The default is + /// the identity mapping; a connector overrides only to impose its own + /// policy (a forced matching mode, a default expiry, and so on). + fn new_invoice(&self, req: CreateInvoiceRequest) -> NewInvoice { + NewInvoice { + order_ref: req.order_ref, + amount: req.amount, + memo: req.memo, + match_mode: req.match_mode, + expiry_secs: req.expiry_secs, + } + } + + /// The webhook endpoint payment events for this store are delivered to, if + /// it consumes webhooks. + fn webhook_url(&self) -> Option<&str> { + None + } +} + +/// The built-in generic REST connector: the identity request mapping plus the +/// operator's configured webhook endpoint. WooCommerce and Medusa speak this +/// same REST + webhook contract, so server-side they reuse it unchanged. +pub struct RestConnector { + webhook_url: Option, +} + +impl RestConnector { + pub fn new(webhook_url: Option) -> RestConnector { + RestConnector { webhook_url } + } +} + +impl StoreConnector for RestConnector { + fn id(&self) -> &str { + "rest" + } + + fn webhook_url(&self) -> Option<&str> { + self.webhook_url.as_deref() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rest_connector_maps_request_identically() { + let conn = RestConnector::new(Some("https://store.example/hook".into())); + assert_eq!(conn.id(), "rest"); + assert_eq!(conn.webhook_url(), Some("https://store.example/hook")); + let req = CreateInvoiceRequest { + order_ref: Some("order-9".into()), + amount: AmountSpec::Grin(42), + memo: Some("m".into()), + match_mode: Some(MatchMode::Derived), + expiry_secs: Some(600), + }; + let inv = conn.new_invoice(req); + assert_eq!(inv.order_ref.as_deref(), Some("order-9")); + assert_eq!(inv.match_mode, Some(MatchMode::Derived)); + assert_eq!(inv.expiry_secs, Some(600)); + } +} diff --git a/crates/gp-core/src/webhook.rs b/crates/gp-core/src/webhook.rs new file mode 100644 index 0000000..ed4f604 --- /dev/null +++ b/crates/gp-core/src/webhook.rs @@ -0,0 +1,383 @@ +//! HTTP webhook notifications (milestone 6): the signed, idempotent, retried +//! payload a store backend (WooCommerce, or any REST consumer) receives on a +//! payment event. This is the contract the connector plugins depend on, so +//! the field names, the signature scheme, and the headers are fixed here. +//! +//! ## Body (`application/json`) +//! +//! ```json +//! { +//! "event_id": "5f3c…", // 128-bit hex, the idempotency key +//! "event_type": "payment.received", // (payment.confirmed once node-confirmed) +//! "created_at": "2026-07-01T12:00:00Z", +//! "payment": { +//! "slate_id": "…", +//! "amount": 2000000000, // nanogrin (integer) +//! "amount_grin": "2", // human decimal string +//! "status": "received", +//! "payer": "…hex…", // sender pubkey, or null +//! "confirmed_height": null // set once confirmed on chain +//! }, +//! "invoice_id": "…", // or null +//! "order_ref": "order-42", // or null +//! "user_id": "…" // multi-tenant crediting (5b), or null +//! } +//! ``` +//! +//! ## Signature +//! +//! `X-GoblinPay-Signature: sha256=` where `` is +//! `HMAC-SHA256(secret, raw_body_bytes)`. The receiver recomputes the HMAC +//! over the exact bytes it received and compares in constant time. +//! `X-GoblinPay-Delivery: ` lets the receiver dedupe retries. +//! +//! Sending, retries, and backoff are persisted in `webhook_delivery`, so a +//! crash mid-retry resumes; the HTTP transport itself lives in gp-server. + +use hmac::{Hmac, Mac}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use sqlx::SqlitePool; +use subtle::ConstantTimeEq; + +use crate::ids; + +/// HTTP header carrying the HMAC signature. +pub const SIGNATURE_HEADER: &str = "X-GoblinPay-Signature"; +/// HTTP header carrying the idempotency key (the event id). +pub const DELIVERY_HEADER: &str = "X-GoblinPay-Delivery"; + +/// Base retry backoff (seconds); doubles each attempt up to [`BACKOFF_CAP`]. +const BACKOFF_BASE: i64 = 30; +/// Maximum retry backoff (seconds). +const BACKOFF_CAP: i64 = 3600; +/// Give up after this many attempts. +pub const MAX_ATTEMPTS: i64 = 12; + +type HmacSha256 = Hmac; + +/// The payment slice of the payload. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentPayload { + pub slate_id: String, + pub amount: u64, + pub amount_grin: String, + pub status: String, + pub payer: Option, + pub confirmed_height: Option, +} + +/// The full webhook payload (the JSON body). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebhookPayload { + pub event_id: String, + pub event_type: String, + pub created_at: String, + pub payment: PaymentPayload, + pub invoice_id: Option, + pub order_ref: Option, + pub user_id: Option, +} + +/// Format nanogrin as a trimmed decimal Grin string (1 grin = 1e9 nanogrin). +pub fn nanogrin_to_grin(nano: u64) -> String { + let whole = nano / 1_000_000_000; + let frac = nano % 1_000_000_000; + if frac == 0 { + whole.to_string() + } else { + let frac = format!("{frac:09}"); + format!("{whole}.{}", frac.trim_end_matches('0')) + } +} + +impl WebhookPayload { + /// Build a `payment.received` payload with a fresh idempotency key. + #[allow(clippy::too_many_arguments)] + pub fn received( + slate_id: String, + amount: u64, + payer: Option, + invoice_id: Option, + order_ref: Option, + user_id: Option, + ) -> WebhookPayload { + WebhookPayload { + event_id: ids::random_id(), + event_type: "payment.received".into(), + created_at: now_iso8601(), + payment: PaymentPayload { + slate_id: slate_id.clone(), + amount, + amount_grin: nanogrin_to_grin(amount), + status: "received".into(), + payer, + confirmed_height: None, + }, + invoice_id, + order_ref, + user_id, + } + } + + /// Serialize to the exact JSON body that gets signed and stored. + pub fn to_body(&self) -> String { + serde_json::to_string(self).expect("payload serializes") + } +} + +/// `sha256=`, the value of the signature +/// header. +pub fn sign(secret: &str, body: &[u8]) -> String { + let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC accepts any key size"); + mac.update(body); + let digest = mac.finalize().into_bytes(); + format!("sha256={}", hex::encode(digest)) +} + +/// Verify a signature header against the body in constant time. Accepts the +/// full `sha256=` form (case-insensitive scheme, lower-hex digest). +pub fn verify(secret: &str, body: &[u8], header: &str) -> bool { + let expected = sign(secret, body); + // Compare the whole `sha256=` string in constant time. Equal length + // for a well-formed header; a length mismatch is a plain reject. + let a = expected.as_bytes(); + let b = header.trim().as_bytes(); + if a.len() != b.len() { + return false; + } + a.ct_eq(b).into() +} + +/// Retry backoff for the Nth attempt (attempt counter starts at 1 after the +/// first failure): `min(BASE * 2^(attempts-1), CAP)`. +pub fn backoff_secs(attempts: i64) -> i64 { + if attempts <= 0 { + return 0; + } + let shift = (attempts - 1).min(20) as u32; + BACKOFF_BASE.saturating_mul(1i64 << shift).min(BACKOFF_CAP) +} + +/// A persisted delivery awaiting (re)send. +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct Delivery { + pub id: String, + pub url: String, + pub body: String, + pub attempts: i64, +} + +/// Persist a payload for delivery to `url`, due immediately. Returns the +/// event id (idempotency key). No-op-safe: the event id is unique. +pub async fn enqueue( + pool: &SqlitePool, + url: &str, + payload: &WebhookPayload, +) -> Result { + let body = payload.to_body(); + sqlx::query( + "INSERT INTO webhook_delivery \ + (id, payment_id, event_type, url, body, attempts, delivered, next_attempt_at, \ + created_at, updated_at) \ + VALUES (?1, ?2, ?3, ?4, ?5, 0, 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), \ + strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))", + ) + .bind(&payload.event_id) + .bind(&payload.payment.slate_id) + .bind(&payload.event_type) + .bind(url) + .bind(&body) + .execute(pool) + .await?; + Ok(payload.event_id.clone()) +} + +/// Deliveries that are due (undelivered and past their next-attempt time and +/// under the attempt ceiling). +pub async fn due(pool: &SqlitePool, limit: i64) -> Result, sqlx::Error> { + sqlx::query_as::<_, Delivery>( + "SELECT id, url, body, attempts FROM webhook_delivery \ + WHERE delivered = 0 AND attempts < ?2 \ + AND next_attempt_at <= strftime('%Y-%m-%dT%H:%M:%SZ', 'now') \ + ORDER BY next_attempt_at LIMIT ?1", + ) + .bind(limit) + .bind(MAX_ATTEMPTS) + .fetch_all(pool) + .await +} + +/// Mark a delivery succeeded. +pub async fn mark_delivered(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> { + sqlx::query( + "UPDATE webhook_delivery SET delivered = 1, attempts = attempts + 1, last_error = NULL, \ + updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?1", + ) + .bind(id) + .execute(pool) + .await?; + Ok(()) +} + +/// Record a failed attempt and schedule the next one with backoff. +pub async fn mark_failed(pool: &SqlitePool, id: &str, error: &str) -> Result<(), sqlx::Error> { + // The new attempt count decides the backoff, computed in Rust and applied + // as a relative SQL offset. + let attempts: i64 = + sqlx::query_scalar("SELECT attempts + 1 FROM webhook_delivery WHERE id = ?1") + .bind(id) + .fetch_one(pool) + .await?; + let backoff = format!("+{} seconds", backoff_secs(attempts)); + sqlx::query( + "UPDATE webhook_delivery SET attempts = ?2, last_error = ?3, \ + next_attempt_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now', ?4), \ + updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?1", + ) + .bind(id) + .bind(attempts) + .bind(error) + .bind(backoff) + .execute(pool) + .await?; + Ok(()) +} + +/// Current UTC time as ISO-8601 seconds (`YYYY-MM-DDTHH:MM:SSZ`), computed +/// from the Unix epoch without pulling in a date library. +fn now_iso8601() -> String { + let secs = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) as i64; + let days = secs.div_euclid(86_400); + let tod = secs.rem_euclid(86_400); + let (h, m, s) = (tod / 3600, (tod % 3600) / 60, tod % 60); + let (y, mo, d) = civil_from_days(days); + format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z") +} + +/// Days since the Unix epoch to a civil (year, month, day). Howard Hinnant's +/// algorithm; avoids a chrono/time dependency for one timestamp. +fn civil_from_days(z: i64) -> (i64, i64, i64) { + let z = z + 719_468; + let era = z.div_euclid(146_097); + let doe = z.rem_euclid(146_097); + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + (if m <= 2 { y + 1 } else { y }, m, d) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db; + + async fn pool() -> SqlitePool { + db::test_pool().await + } + + #[test] + fn sign_and_verify_round_trip() { + let secret = "s3cr3t"; + let body = br#"{"event_id":"abc","amount":1}"#; + let sig = sign(secret, body); + assert!(sig.starts_with("sha256=")); + assert_eq!(sig.len(), "sha256=".len() + 64); + assert!(verify(secret, body, &sig)); + // A tampered body fails. + assert!(!verify(secret, br#"{"event_id":"abc","amount":2}"#, &sig)); + // A wrong secret fails. + assert!(!verify("other", body, &sig)); + // Garbage header fails without panicking. + assert!(!verify(secret, body, "sha256=deadbeef")); + assert!(!verify(secret, body, "")); + } + + #[test] + fn signature_matches_a_known_vector() { + // HMAC-SHA256("key", "The quick brown fox jumps over the lazy dog") + // = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 + let sig = sign("key", b"The quick brown fox jumps over the lazy dog"); + assert_eq!( + sig, + "sha256=f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" + ); + } + + #[test] + fn grin_formatting() { + assert_eq!(nanogrin_to_grin(0), "0"); + assert_eq!(nanogrin_to_grin(1_000_000_000), "1"); + assert_eq!(nanogrin_to_grin(2_500_000_000), "2.5"); + assert_eq!(nanogrin_to_grin(1_234_567_890), "1.23456789"); + assert_eq!(nanogrin_to_grin(1), "0.000000001"); + } + + #[test] + fn backoff_grows_and_caps() { + assert_eq!(backoff_secs(0), 0); + assert_eq!(backoff_secs(1), 30); + assert_eq!(backoff_secs(2), 60); + assert_eq!(backoff_secs(3), 120); + assert_eq!(backoff_secs(100), BACKOFF_CAP, "must cap"); + } + + #[test] + fn timestamp_is_iso8601() { + let ts = now_iso8601(); + assert_eq!(ts.len(), 20, "YYYY-MM-DDTHH:MM:SSZ"); + assert!(ts.ends_with('Z')); + assert!(ts.contains('T')); + // A known epoch second: 2021-01-01T00:00:00Z = 1609459200. + assert_eq!(civil_from_days(1_609_459_200 / 86_400), (2021, 1, 1)); + } + + #[tokio::test] + async fn enqueue_deliver_and_idempotency() { + let pool = pool().await; + let payload = WebhookPayload::received( + "slate-1".into(), + 2_000_000_000, + Some("payerhex".into()), + Some("inv-1".into()), + Some("order-1".into()), + None, + ); + let id = enqueue(&pool, "https://store.example/hook", &payload) + .await + .unwrap(); + assert_eq!(id, payload.event_id); + + // It is due immediately. + let due_now = due(&pool, 10).await.unwrap(); + assert_eq!(due_now.len(), 1); + assert_eq!(due_now[0].id, id); + // The stored body verifies under the same secret. + assert!(verify( + "hooksecret", + due_now[0].body.as_bytes(), + &sign("hooksecret", due_now[0].body.as_bytes()) + )); + + // A failure reschedules it into the future (no longer due now). + mark_failed(&pool, &id, "connection refused").await.unwrap(); + assert!(due(&pool, 10).await.unwrap().is_empty()); + + // Delivery marks it done and it never comes due again. + mark_delivered(&pool, &id).await.unwrap(); + assert!(due(&pool, 10).await.unwrap().is_empty()); + let delivered: i64 = + sqlx::query_scalar("SELECT delivered FROM webhook_delivery WHERE id = ?1") + .bind(&id) + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(delivered, 1); + } +} diff --git a/crates/gp-goblin-sender/Cargo.toml b/crates/gp-goblin-sender/Cargo.toml new file mode 100644 index 0000000..6aabfc9 --- /dev/null +++ b/crates/gp-goblin-sender/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "gp-goblin-sender" +description = "Milestone-2 gate helper: builds and finalizes slatepacks with Goblin's actual wallet stack" +version.workspace = true +edition.workspace = true +license.workspace = true + +# Test-only tool, never deployed. It is the SENDER half of the slatepack +# round-trip gate in gp-wallet/tests/goblin_roundtrip.rs, kept in its own +# binary because Goblin's fork (heed / lmdb-master-sys) and upstream +# grin-wallet (lmdb-zero / liblmdb-sys) bundle two incompatible LMDB C +# libraries that collide when linked into one executable. + +[dependencies] +# Goblin's actual wallet stack (grin-wallet fork + vendored grin node +# crates), as renamed path dependencies. Requires the goblin checkout as a +# sibling of the GoblinPay directory. +goblin_libwallet = { package = "grin_wallet_libwallet", path = "../../../goblin/wallet/libwallet" } +goblin_impls = { package = "grin_wallet_impls", path = "../../../goblin/wallet/impls" } +goblin_core = { package = "grin_core", path = "../../../goblin/node/core" } +goblin_keychain = { package = "grin_keychain", path = "../../../goblin/node/keychain" } +goblin_util = { package = "grin_util", path = "../../../goblin/node/util" } +rand = "0.6" +serde_json = { workspace = true } diff --git a/crates/gp-goblin-sender/src/main.rs b/crates/gp-goblin-sender/src/main.rs new file mode 100644 index 0000000..4eef268 --- /dev/null +++ b/crates/gp-goblin-sender/src/main.rs @@ -0,0 +1,464 @@ +//! Milestone-2 gate helper: the SENDER half of the slatepack round-trip, +//! running Goblin's actual wallet stack (the grin-wallet fork vendored at +//! goblin/wallet over grin_core 5.4.1). +//! +//! Two subcommands, driven by gp-wallet/tests/goblin_roundtrip.rs: +//! +//! gen [recipient_slatepack_address] +//! Creates a throwaway wallet from a fresh random mnemonic under +//! /sender-wallet, injects one spendable output (valid keys and +//! commitment, never on chain, which offline finalization never checks), +//! runs init_send_tx, and writes: +//! /s1.armor S1 slatepack (plain, or encrypted to the +//! recipient address when one is given) +//! /meta.json {"slate_id": "...", "amount": N} +//! +//! check +//! Reopens the same wallet, parses the S2 reply, finalizes the +//! transaction (full offline validation: sums, signatures, range +//! proofs), asserts slate id / kernel consistency, and writes +//! /result.json +//! Exits nonzero on any mismatch. +//! +//! Everything offline: no node, no chain, mainnet parameters. Only freshly +//! generated random test mnemonics, never any real seed. + +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; + +use rand::RngCore; + +use goblin_core::core::{Transaction, TxKernel}; +use goblin_core::global as gglobal; +use goblin_impls::{DefaultLCProvider, DefaultWalletImpl}; +use goblin_keychain::{ExtKeychain, Keychain}; +use goblin_libwallet::api_impl::owner as gowner; +use goblin_libwallet::{ + InitTxArgs, NodeVersionInfo, OutputData, OutputStatus, Slate, SlateState, SlatepackAddress, + WalletInst, +}; +use goblin_util::secp::key::SecretKey; +use goblin_util::secp::pedersen; +use goblin_util::Mutex; +use goblin_util::ZeroingString; + +const TIP_HEIGHT: u64 = 10; +const PASSWORD: &str = "gate-sender-pw"; + +type Error = Box; +type Provider = DefaultLCProvider; +type WalletBox = Box>; +type Instance = Arc>; + +/// Offline stand-in for a Grin node: the send path only ever asks for the +/// chain tip. Everything else is unreachable here. +#[derive(Clone)] +struct StubNode; + +fn offline(what: &str) -> Result { + Err(goblin_libwallet::Error::ClientCallback(format!( + "offline gate stub: {what}" + ))) +} + +impl goblin_libwallet::NodeClient for StubNode { + fn node_url(&self) -> &str { + "http://127.0.0.1:13413" + } + fn set_node_url(&mut self, _: &str) {} + fn node_api_secret(&self) -> Option { + None + } + fn set_node_api_secret(&mut self, _: Option) {} + fn post_tx(&self, _: &Transaction, _: bool) -> Result<(), goblin_libwallet::Error> { + offline("post_tx") + } + fn get_version_info(&mut self) -> Option { + None + } + fn get_chain_tip(&self) -> Result<(u64, String), goblin_libwallet::Error> { + Ok((TIP_HEIGHT, "0".repeat(64))) + } + fn get_kernel( + &mut self, + _: &pedersen::Commitment, + _: Option, + _: Option, + ) -> Result, goblin_libwallet::Error> { + offline("get_kernel") + } + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, goblin_libwallet::Error> { + // Goblin's fork refreshes outputs from the node before selecting + // inputs (updater::refresh_outputs inside add_inputs_to_slate, a + // deviation from upstream). Report every wallet output as unspent + // on chain so the injected input stays spendable. + Ok(wallet_outputs + .into_iter() + .map(|c| { + let hex: String = c.0.iter().map(|b| format!("{b:02x}")).collect(); + (c, (hex, 1, 1)) + }) + .collect()) + } + fn get_outputs_by_pmmr_index( + &self, + _: u64, + _: Option, + _: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + ), + goblin_libwallet::Error, + > { + offline("get_outputs_by_pmmr_index") + } + fn height_range_to_pmmr_indices( + &self, + _: u64, + _: Option, + ) -> Result<(u64, u64), goblin_libwallet::Error> { + offline("height_range_to_pmmr_indices") + } +} + +struct Sender { + instance: Instance, + mask: Option, +} + +impl Sender { + /// Create a fresh wallet from a random mnemonic (gen phase). + fn create(dir: &Path) -> Result { + let mut entropy = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut entropy); + let mnemonic = goblin_keychain::mnemonic::from_entropy(&entropy) + .map_err(|e| format!("mnemonic generation failed: {e:?}"))?; + Self::open(dir, Some(mnemonic)) + } + + /// Open the wallet, creating it first when a mnemonic is given. + fn open(dir: &Path, create_mnemonic: Option) -> Result { + let mut wallet = Box::new(DefaultWalletImpl::::new(StubNode)?) as WalletBox; + let mask = { + let lc = wallet.lc_provider()?; + lc.set_top_level_directory( + dir.to_str() + .ok_or_else(|| format!("non-UTF8 dir {dir:?}"))?, + )?; + if let Some(mnemonic) = create_mnemonic { + lc.create_wallet( + None, + Some(ZeroingString::from(mnemonic)), + 32, + ZeroingString::from(PASSWORD), + false, + )?; + } + lc.open_wallet(None, ZeroingString::from(PASSWORD), true, false)? + }; + Ok(Sender { + instance: Arc::new(Mutex::new(wallet)), + mask, + }) + } + + /// Give the wallet one ordinary spendable output so init_send_tx has + /// coins to select. Valid keys and commitment, never on chain. + fn inject_funds(&self, value: u64) -> Result<(), Error> { + let mut w_lock = self.instance.lock(); + let lc = w_lock.lc_provider()?; + let w = lc.wallet_inst()?; + let parent = w.parent_key_id(); + let key_id = w.next_child(self.mask.as_ref())?; + let n_child = u32::from(key_id.to_path().path[2]); + let mut batch = w.batch(self.mask.as_ref())?; + batch.save(OutputData { + root_key_id: parent.clone(), + key_id, + n_child, + commit: None, + mmr_index: None, + value, + status: OutputStatus::Unspent, + height: 1, + lock_height: 0, + is_coinbase: false, + tx_log_entry: None, + })?; + batch.save_last_confirmed_height(&parent, TIP_HEIGHT)?; + batch.commit()?; + Ok(()) + } + + fn init_send( + &self, + amount: u64, + proof_recipient: Option, + ) -> Result { + let mut w_lock = self.instance.lock(); + let lc = w_lock.lc_provider()?; + let w = lc.wallet_inst()?; + let args = InitTxArgs { + amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + // When set, init_send_tx puts a PaymentInfo on the slate (our + // sender address + this recipient address, no receiver signature + // yet), which is exactly the payment-proof request the receiver + // fills in during receive_tx. + payment_proof_recipient_address: proof_recipient, + ..Default::default() + }; + let slate = gowner::init_send_tx(w, self.mask.as_ref(), args, false)?; + // Lock before transmitting S1, exactly like Goblin does + // (goblin/src/wallet/wallet.rs calls api.tx_lock_outputs right after + // init_send_tx). Locking also records the change output in the + // wallet DB; finalize's repopulate_tx silently drops the change + // output when it is missing, which breaks the kernel sums. + gowner::tx_lock_outputs(w, self.mask.as_ref(), &slate)?; + Ok(slate) + } + + fn armor(&self, slate: &Slate, recipients: Vec) -> Result { + Ok(gowner::create_slatepack_message( + self.instance.clone(), + self.mask.as_ref(), + slate, + Some(0), + recipients, + )?) + } + + fn parse_s2(&self, armor: &str) -> Result { + Ok(gowner::slate_from_slatepack_message( + self.instance.clone(), + self.mask.as_ref(), + armor.trim().to_string(), + vec![], + )?) + } + + fn finalize(&self, slate: &Slate) -> Result { + let mut w_lock = self.instance.lock(); + let lc = w_lock.lc_provider()?; + let w = lc.wallet_inst()?; + Ok(gowner::finalize_tx(w, self.mask.as_ref(), slate)?) + } + + fn calc_excess(&self, slate: &Slate) -> Result { + let mut w_lock = self.instance.lock(); + let lc = w_lock.lc_provider()?; + let w = lc.wallet_inst()?; + let keychain = w.keychain(self.mask.as_ref())?; + Ok(slate.calc_excess(keychain.secp())?) + } +} + +fn wallet_dir(workdir: &Path) -> std::path::PathBuf { + workdir.join("sender-wallet") +} + +fn cmd_gen(workdir: &Path, amount: u64, recipient: Option<&str>) -> Result<(), Error> { + let dir = wallet_dir(workdir); + std::fs::create_dir_all(&dir)?; + + let sender = Sender::create(&dir)?; + // Amount plus generous room for the fee, in one output. + sender.inject_funds(amount + 1_000_000_000)?; + + let slate = sender.init_send(amount, None)?; + if slate.state != SlateState::Standard1 { + return Err(format!("expected S1 out of init_send_tx, got {:?}", slate.state).into()); + } + + let recipients = match recipient { + Some(addr) => vec![SlatepackAddress::try_from(addr) + .map_err(|e| format!("recipient address `{addr}` rejected: {e}"))?], + None => vec![], + }; + let armor = sender.armor(&slate, recipients)?; + + std::fs::write(workdir.join("s1.armor"), &armor)?; + let meta = serde_json::json!({ + "slate_id": slate.id.to_string(), + "amount": amount, + }); + std::fs::write(workdir.join("meta.json"), meta.to_string())?; + println!("{meta}"); + Ok(()) +} + +/// Like `gen`, but the S1 REQUESTS a payment proof to `recipient` (the +/// receiver's slatepack address). Exercises gp-wallet's receiver-side proof +/// path. Armor is plain (proof and armor encryption are orthogonal). +fn cmd_genproof(workdir: &Path, amount: u64, recipient: &str) -> Result<(), Error> { + let dir = wallet_dir(workdir); + std::fs::create_dir_all(&dir)?; + + let proof_addr = SlatepackAddress::try_from(recipient) + .map_err(|e| format!("proof recipient address `{recipient}` rejected: {e}"))?; + + let sender = Sender::create(&dir)?; + sender.inject_funds(amount + 1_000_000_000)?; + + let slate = sender.init_send(amount, Some(proof_addr))?; + if slate.state != SlateState::Standard1 { + return Err(format!("expected S1 out of init_send_tx, got {:?}", slate.state).into()); + } + if slate.payment_proof.is_none() { + return Err("init_send_tx did not attach a payment-proof request".into()); + } + let armor = sender.armor(&slate, vec![])?; + + std::fs::write(workdir.join("s1.armor"), &armor)?; + let meta = serde_json::json!({ + "slate_id": slate.id.to_string(), + "amount": amount, + "proof_recipient": recipient, + }); + std::fs::write(workdir.join("meta.json"), meta.to_string())?; + println!("{meta}"); + Ok(()) +} + +fn cmd_check(workdir: &Path, s2_file: &Path) -> Result<(), Error> { + let meta: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(workdir.join("meta.json"))?)?; + let slate_id = meta["slate_id"] + .as_str() + .ok_or("meta.json missing slate_id")? + .to_string(); + + let sender = Sender::open(&wallet_dir(workdir), None)?; + let s2 = sender.parse_s2(&std::fs::read_to_string(s2_file)?)?; + + if s2.id.to_string() != slate_id { + return Err(format!("S2 slate id {} != sent {}", s2.id, slate_id).into()); + } + if s2.state != SlateState::Standard2 { + return Err(format!("expected S2, got {:?}", s2.state).into()); + } + // Compact slates: only the receiver's participant entry travels back in + // S2; the sender's own entry is restored from the stored context during + // finalize. + if s2.participant_data.len() != 1 { + return Err(format!("S2 has {} participants, want 1", s2.participant_data.len()).into()); + } + + // The real crypto gate: finalizing validates the receiver's output, + // range proof, and partial signature against consensus rules, offline. + let final_slate = sender.finalize(&s2)?; + if final_slate.state != SlateState::Standard3 { + return Err(format!("expected S3 after finalize, got {:?}", final_slate.state).into()); + } + let tx = final_slate + .tx + .clone() + .ok_or("final slate carries no transaction")?; + if tx.kernels().len() != 1 || tx.inputs().len() != 1 || tx.outputs().len() != 2 { + return Err(format!( + "unexpected tx shape: {} kernels, {} inputs, {} outputs", + tx.kernels().len(), + tx.inputs().len(), + tx.outputs().len() + ) + .into()); + } + let kernel = &tx.kernels()[0]; + kernel + .verify() + .map_err(|e| format!("kernel signature invalid: {e}"))?; + let excess = sender.calc_excess(&final_slate)?; + if kernel.excess != excess { + return Err("kernel excess inconsistent with slate".into()); + } + + let excess_hex: String = excess.0.iter().map(|b| format!("{b:02x}")).collect(); + let result = serde_json::json!({ + "slate_id": final_slate.id.to_string(), + "state": "Standard3", + "kernel_verified": true, + "kernel_excess": excess_hex, + "kernels": 1, + "inputs": 1, + "outputs": 2, + }); + std::fs::write(workdir.join("result.json"), result.to_string())?; + println!("{result}"); + Ok(()) +} + +/// Harness self-test: the fork wallet receives its own S1 (self-spend) and +/// finalizes, without gp-wallet involved. Proves the injected-funds harness +/// is sound independently of any cross-stack question. +fn cmd_selfcheck(workdir: &Path) -> Result<(), Error> { + use goblin_libwallet::api_impl::foreign as gforeign; + + let dir = wallet_dir(workdir); + std::fs::create_dir_all(&dir)?; + let sender = Sender::create(&dir)?; + let amount = 2_000_000_000u64; + sender.inject_funds(amount + 1_000_000_000)?; + + let s1 = sender.init_send(amount, None)?; + let s1_armor = sender.armor(&s1, vec![])?; + + // Receive with the same fork stack (self-spend), through the armor. + let parsed = sender.parse_s2(&s1_armor)?; // generic slatepack parse + let s2 = { + let mut w_lock = sender.instance.lock(); + let lc = w_lock.lc_provider()?; + let w = lc.wallet_inst()?; + gforeign::receive_tx(w, sender.mask.as_ref(), &parsed, None, false)? + }; + let s2_armor = sender.armor(&s2, vec![])?; + + let s2_back = sender.parse_s2(&s2_armor)?; + let final_slate = sender.finalize(&s2_back)?; + if final_slate.state != SlateState::Standard3 { + return Err(format!("selfcheck: expected S3, got {:?}", final_slate.state).into()); + } + println!("selfcheck ok: {} {:?}", final_slate.id, final_slate.state); + Ok(()) +} + +fn run() -> Result<(), Error> { + gglobal::init_global_chain_type(gglobal::ChainTypes::Mainnet); + gglobal::set_local_chain_type(gglobal::ChainTypes::Mainnet); + + let args: Vec = std::env::args().collect(); + match args.get(1).map(String::as_str) { + Some("gen") if args.len() == 4 || args.len() == 5 => { + let amount: u64 = args[3].parse()?; + cmd_gen(Path::new(&args[2]), amount, args.get(4).map(String::as_str)) + } + Some("genproof") if args.len() == 5 => { + let amount: u64 = args[3].parse()?; + cmd_genproof(Path::new(&args[2]), amount, &args[4]) + } + Some("check") if args.len() == 4 => cmd_check(Path::new(&args[2]), Path::new(&args[3])), + Some("selfcheck") if args.len() == 3 => cmd_selfcheck(Path::new(&args[2])), + _ => Err( + "usage: gp-goblin-sender gen [recipient] \ + | genproof \ + | check | selfcheck " + .into(), + ), + } +} + +fn main() { + if let Err(e) = run() { + eprintln!("gp-goblin-sender: {e} ({e:?})"); + std::process::exit(1); + } +} diff --git a/crates/gp-nostr/Cargo.toml b/crates/gp-nostr/Cargo.toml new file mode 100644 index 0000000..eff4e7f --- /dev/null +++ b/crates/gp-nostr/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "gp-nostr" +description = "Nostr transport and secure handoff for GoblinPay (identity, gift wrap, ingest, Nym)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gp-core = { path = "../gp-core" } + +# Nostr: the same SDK line Goblin ships (relay pool, NIP-44 v2, NIP-49 +# ncryptsec, NIP-59 gift wrap). Deliberately NO `nip06` feature: the identity +# is a random standalone nsec, never derived from any mnemonic (two-secrets +# rule). +nostr-sdk = { version = "0.44", features = ["nip44", "nip49", "nip59"] } +nostr-relay-pool = "0.44" +async-wsocket = "0.13" +tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] } + +# NIP-44 v3 (the companion crate, M0). Path dep on the local checkout, PINNED: +# working tree is on branch `v3` at rev e3dfa5e ("Document the v3 API in the +# README"). It provides encrypt_v3/decrypt_v3 with the authenticated +# kind/scope context binding the NIP-17 extension rides on; v2 stays on +# nostr-sdk. Do not float the checkout without re-running the wrap tests. +nip44 = { path = "../../../nip44" } +# The nip44 crate speaks secp256k1 0.31 types (nostr-sdk is on 0.29; the two +# versions coexist, conversion goes through raw bytes). `global-context` gives +# the shared `SECP256K1` context and `hashes` the SHA-256 used to sign the +# server receipt (BIP-340 Schnorr over the receipt digest, same key as the +# Nostr identity). +secp256k1 = { version = "0.31", features = ["global-context", "hashes"] } + +# Nym mixnet, linked IN-PROCESS via smolmix (TCP/UDP tunnel over the mixnet +# with an AUTO-SELECTED IPR exit; no sidecar, no SOCKS5 loopback, no +# single-exit SPOF). Path dep into the local nym checkout, PINNED at rev +# f6ed17d949cc19fee0fb51db3cb65771fd510d5b ("http-api-client: preconfigured +# webpki roots on Android" — the Android patch is irrelevant server-side, but +# the pin is what Goblin G14 validated; do not float it silently). +smolmix = { path = "../../../nym/smolmix/core" } +# mix-dns wire codec. Already in the dependency graph via nym-http-api-client +# (smolmix -> nym-sdk), so we reuse it instead of vendoring a DNS +# encode/parse (same justification as Goblin). +hickory-proto = { version = "0.26", default-features = false, features = ["std"] } + +tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync"] } +log = "0.4" +serde = { workspace = true } +serde_json = { workspace = true } +# mix-dns transaction ids. +rand = "0.9" + +[dev-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/gp-nostr/src/identity.rs b/crates/gp-nostr/src/identity.rs new file mode 100644 index 0000000..6b73f7f --- /dev/null +++ b/crates/gp-nostr/src/identity.rs @@ -0,0 +1,309 @@ +//! The server's Nostr payment identity: a random standalone nsec or an +//! imported one, NEVER derived from the Grin mnemonic (the two-secrets rule: +//! the mnemonic is the money secret, the nsec is the payment identity; losing +//! one must never compromise or resurrect the other). Mirrors Goblin's +//! `nostr/identity.rs`, trimmed to what a headless daemon needs. +//! +//! Resolution order (see [`load_or_create`]): +//! 1. `GP_NSEC` — plaintext key from the environment (mounted-file variant +//! supported by gp-core). Used as-is, never persisted. +//! 2. `GP_NCRYPTSEC` — NIP-49 encrypted key, unlocked with the wallet +//! password. Never persisted. +//! 3. Neither set — load `/nostr/identity.json`, or generate a +//! fresh RANDOM key and persist it NIP-49 encrypted (wallet password), +//! file mode 0600. + +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; + +use gp_core::config::Config; +use nostr_sdk::nips::nip49::{EncryptedSecretKey, KeySecurity}; +use nostr_sdk::{FromBech32, Keys, SecretKey, ToBech32}; +use serde::{Deserialize, Serialize}; + +/// NIP-49 scrypt work factor (~64 MiB, interactive-grade; same as Goblin). +const NCRYPTSEC_LOG_N: u8 = 16; + +/// Identity file stored at `/nostr/identity.json`. Only the +/// encrypted key and the public key: a headless till has no NIP-05 name, no +/// contact metadata. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ServerIdentity { + pub ver: u8, + /// NIP-49 encrypted secret key (bech32 ncryptsec). + pub ncryptsec: String, + /// Public key, bech32 npub (plaintext for logs and the QR). + pub npub: String, +} + +#[derive(Debug)] +pub enum IdentityError { + /// Missing or inconsistent configuration (fail fast at startup). + Config(String), + /// Key parse/encrypt/decrypt failure (includes wrong password). + Key(String), + /// Filesystem failure persisting or reading the identity file. + Io(String), +} + +impl fmt::Display for IdentityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IdentityError::Config(m) => write!(f, "identity config error: {m}"), + IdentityError::Key(m) => write!(f, "identity key error: {m}"), + IdentityError::Io(m) => write!(f, "identity io error: {m}"), + } + } +} + +impl std::error::Error for IdentityError {} + +impl ServerIdentity { + pub const FILE_NAME: &'static str = "identity.json"; + + /// Identity file path for a data dir. + pub fn path(data_dir: &Path) -> PathBuf { + data_dir.join("nostr").join(Self::FILE_NAME) + } + + /// Load the identity file if it exists and parses. + pub fn load(data_dir: &Path) -> Option { + let raw = fs::read_to_string(Self::path(data_dir)).ok()?; + serde_json::from_str(&raw).ok() + } + + /// Persist with owner-only permissions (the ncryptsec blob must not be + /// world readable: a local attacker could grind the password offline). + pub fn save(&self, data_dir: &Path) -> Result<(), IdentityError> { + let dir = data_dir.join("nostr"); + fs::create_dir_all(&dir).map_err(|e| IdentityError::Io(format!("create {dir:?}: {e}")))?; + restrict(&dir, 0o700)?; + let raw = serde_json::to_string_pretty(self) + .map_err(|e| IdentityError::Io(format!("serialize identity: {e}")))?; + let path = Self::path(data_dir); + fs::write(&path, raw).map_err(|e| IdentityError::Io(format!("write {path:?}: {e}")))?; + restrict(&path, 0o600)?; + Ok(()) + } + + /// Unlock the stored key with the wallet password. + pub fn unlock(&self, password: &str) -> Result { + decrypt_ncryptsec(&self.ncryptsec, password) + } + + fn from_keys(keys: &Keys, password: &str) -> Result { + let encrypted = EncryptedSecretKey::new( + keys.secret_key(), + password, + NCRYPTSEC_LOG_N, + KeySecurity::Medium, + ) + .map_err(|e| IdentityError::Key(format!("encrypt failed: {e}")))?; + Ok(ServerIdentity { + ver: 1, + ncryptsec: encrypted + .to_bech32() + .map_err(|e| IdentityError::Key(format!("bech32 failed: {e}")))?, + npub: keys + .public_key() + .to_bech32() + .map_err(|e| IdentityError::Key(format!("bech32 failed: {e}")))?, + }) + } +} + +/// Resolve the identity keys from the configuration (see the module doc for +/// the order). Fails fast on a missing wallet password whenever the at-rest +/// encryption needs one. +pub fn load_or_create(cfg: &Config) -> Result { + // 1. Plaintext nsec from the environment: authoritative, not persisted. + if let Some(nsec) = &cfg.nsec { + let secret = SecretKey::parse(nsec.reveal().trim()) + .map_err(|e| IdentityError::Key(format!("invalid GP_NSEC: {e}")))?; + return Ok(Keys::new(secret)); + } + + let password = cfg + .wallet_password + .as_ref() + .ok_or_else(|| { + IdentityError::Config( + "GP_WALLET_PASSWORD (or _FILE) is required to unlock or persist the \ + Nostr identity (set GP_NSEC to bypass at-rest encryption)" + .into(), + ) + })? + .reveal() + .to_string(); + + // 2. NIP-49 encrypted key from the environment: unlocked, not persisted. + if let Some(ncryptsec) = &cfg.ncryptsec { + return decrypt_ncryptsec(ncryptsec.reveal().trim(), &password); + } + + // 3. Persisted identity, or a fresh RANDOM key (never seed-derived). + let data_dir = Path::new(&cfg.data_dir); + if let Some(identity) = ServerIdentity::load(data_dir) { + return identity.unlock(&password); + } + let keys = Keys::generate(); + ServerIdentity::from_keys(&keys, &password)?.save(data_dir)?; + Ok(keys) +} + +fn decrypt_ncryptsec(ncryptsec: &str, password: &str) -> Result { + let encrypted = EncryptedSecretKey::from_bech32(ncryptsec) + .map_err(|e| IdentityError::Key(format!("invalid ncryptsec: {e}")))?; + let secret = encrypted + .decrypt(password) + .map_err(|_| IdentityError::Key("wrong password for ncryptsec".into()))?; + Ok(Keys::new(secret)) +} + +/// chmod, failing fast (Unix only; the daemon targets Linux servers). +fn restrict(path: &Path, mode: u32) -> Result<(), IdentityError> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(mode)) + .map_err(|e| IdentityError::Io(format!("chmod {path:?}: {e}"))) + } + #[cfg(not(unix))] + { + let _ = (path, mode); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicU32, Ordering}; + + use gp_core::config::Secret; + + use super::*; + + /// Self-cleaning unique temp dir (no extra dev-deps). + struct TempDir(PathBuf); + + impl TempDir { + fn new(tag: &str) -> TempDir { + static N: AtomicU32 = AtomicU32::new(0); + let path = std::env::temp_dir().join(format!( + "gp-nostr-id-{tag}-{}-{}", + std::process::id(), + N.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + TempDir(path) + } + } + + impl Drop for TempDir { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + fn cfg(dir: &TempDir) -> Config { + Config { + data_dir: dir.0.to_str().unwrap().to_string(), + wallet_password: Some(Secret::new("hunter2".into())), + ..Config::default() + } + } + + #[test] + fn generates_persists_and_reloads_the_same_key() { + let dir = TempDir::new("gen"); + let cfg = cfg(&dir); + let first = load_or_create(&cfg).unwrap(); + let second = load_or_create(&cfg).unwrap(); + assert_eq!(first.public_key(), second.public_key()); + + // Encrypted at rest: no bech32 nsec in the file. + let raw = fs::read_to_string(ServerIdentity::path(&dir.0)).unwrap(); + let nsec = first.secret_key().to_bech32().unwrap(); + assert!(!raw.contains(&nsec), "identity file leaks the nsec"); + assert!(raw.contains("ncryptsec1"), "key must be NIP-49 encrypted"); + } + + #[cfg(unix)] + #[test] + fn identity_file_is_owner_only() { + use std::os::unix::fs::PermissionsExt; + let dir = TempDir::new("perm"); + load_or_create(&cfg(&dir)).unwrap(); + let meta = fs::metadata(ServerIdentity::path(&dir.0)).unwrap(); + assert_eq!( + meta.permissions().mode() & 0o077, + 0, + "identity.json must be 0600" + ); + } + + #[test] + fn wrong_password_fails_and_never_regenerates() { + let dir = TempDir::new("wrongpw"); + let mut c = cfg(&dir); + let keys = load_or_create(&c).unwrap(); + c.wallet_password = Some(Secret::new("not-it".into())); + // A wrong password must be a hard error, not a silent fresh identity + // (payers hold the old npub; regenerating would strand their sends). + assert!(load_or_create(&c).is_err()); + c.wallet_password = Some(Secret::new("hunter2".into())); + assert_eq!(load_or_create(&c).unwrap().public_key(), keys.public_key()); + } + + #[test] + fn imports_nsec_without_persisting() { + let dir = TempDir::new("nsec"); + let external = Keys::generate(); + let mut c = cfg(&dir); + c.nsec = Some(Secret::new(external.secret_key().to_bech32().unwrap())); + c.wallet_password = None; // not needed on this path + let keys = load_or_create(&c).unwrap(); + assert_eq!(keys.public_key(), external.public_key()); + assert!( + !ServerIdentity::path(&dir.0).exists(), + "env-provided keys must not be written to disk" + ); + } + + #[test] + fn imports_ncryptsec_from_env() { + let dir = TempDir::new("ncryptsec"); + let external = Keys::generate(); + let encrypted = EncryptedSecretKey::new( + external.secret_key(), + "hunter2", + NCRYPTSEC_LOG_N, + KeySecurity::Medium, + ) + .unwrap(); + let mut c = cfg(&dir); + c.ncryptsec = Some(Secret::new(encrypted.to_bech32().unwrap())); + let keys = load_or_create(&c).unwrap(); + assert_eq!(keys.public_key(), external.public_key()); + assert!(!ServerIdentity::path(&dir.0).exists()); + } + + #[test] + fn missing_password_fails_fast() { + let dir = TempDir::new("nopw"); + let mut c = cfg(&dir); + c.wallet_password = None; + let err = load_or_create(&c).unwrap_err(); + assert!(err.to_string().contains("GP_WALLET_PASSWORD"), "{err}"); + } + + #[test] + fn random_identities_are_independent() { + // Fresh entropy every time — nothing chains identities to each other + // (or to any wallet seed; there is no derivation path at all). + let a = load_or_create(&cfg(&TempDir::new("ind-a"))).unwrap(); + let b = load_or_create(&cfg(&TempDir::new("ind-b"))).unwrap(); + assert_ne!(a.public_key(), b.public_key()); + } +} diff --git a/crates/gp-nostr/src/ingest.rs b/crates/gp-nostr/src/ingest.rs new file mode 100644 index 0000000..c8d2b66 --- /dev/null +++ b/crates/gp-nostr/src/ingest.rs @@ -0,0 +1,692 @@ +//! The guarded ingest pipeline: what to do with an incoming gift wrap. +//! Mirrors the shape of `goblin/src/nostr/ingest.rs` (a pure, unit-tested +//! `decide()` plus dedupe and rate limiting around it), simplified for a +//! receive-only payment server: +//! +//! - The accept policy is fixed to **auto-receive everyone** — a public till +//! takes payments from strangers by design. +//! - Only Standard1 sends are processed, and that invariant is enforced by +//! the WALLET (`gp_wallet::receive_slatepack` rejects everything else), so +//! the policy here reasons about the message, not slate internals. +//! - There is no finalize/post path, no payment requests, no contacts: a +//! reply-to-us (S2) or an invoice would target a sender wallet we do not +//! have. They drop. + +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; + +use log::{info, warn}; +use nostr_sdk::{Event, EventBuilder, Keys, Kind, PublicKey, Tag, UnsignedEvent}; + +use crate::{ + protocol, unix_time, IncomingContext, KeyDirectory, MasterDirectory, ReceiveError, + SlatepackReceiver, +}; + +/// Rate limit for incoming wraps per sender (events/hour). A payment server +/// has no contact book, so everyone gets Goblin's unknown-sender budget. +const RATE_PER_SENDER_PER_HOUR: usize = 10; +/// Global ceiling on gift-wrap decrypt attempts per minute across ALL +/// senders (Goblin's fresh-keypair-spam bound: the per-sender limit only +/// applies after the expensive decrypt reveals the sender). +const GLOBAL_UNWRAP_PER_MIN: usize = 120; +/// Cap on remembered rate-limiter senders before pruning. +const RATE_MAP_CAP: usize = 10_000; + +/// What the pipeline should do with a validated incoming message. +/// Pure policy — unit tested, no side effects. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IngestDecision { + /// A fresh payment message: receive it and reply S2 automatically. + AutoReceive, + /// Drop silently (reason for logging only). + Drop(&'static str), +} + +/// Inputs for the policy decision. +pub struct IngestContext<'a> { + /// Seal-verified sender public key, hex. + pub sender: &'a str, + /// The sender is ourselves (wrap-to-self copy). + pub is_self: bool, + /// The rumor is a kind 14 DM within the size cap. + pub rumor_is_dm: bool, + /// The rumor content carries exactly one slatepack armor block. + pub has_slatepack: bool, + /// This wrap/rumor was already processed. + pub duplicate: bool, +} + +/// Pure policy function (auto-receive everyone, mirroring Goblin's shape). +pub fn decide(ctx: &IngestContext) -> IngestDecision { + if ctx.duplicate { + return IngestDecision::Drop("already processed"); + } + if ctx.is_self { + return IngestDecision::Drop("own message"); + } + if !ctx.rumor_is_dm { + return IngestDecision::Drop("not a kind 14 DM"); + } + if !ctx.has_slatepack { + return IngestDecision::Drop("no slatepack payload"); + } + IngestDecision::AutoReceive +} + +/// A reply ready to be encrypted and dispatched: the identity it is sent FROM +/// (the master key, or the derived child the payer addressed), the payer, and +/// the unsigned kind-14 rumor carrying the S2 armor. Version choice + gift +/// wrapping happen at the send site (they depend on the payer's advertised +/// 10050). +#[derive(Debug, Clone)] +pub struct PendingReply { + pub from: Keys, + pub payer: PublicKey, + pub rumor: UnsignedEvent, +} + +/// Outcome of handling one gift wrap event. +#[derive(Debug)] +pub enum IngestOutcome { + /// A payment was received; dispatch the reply. Boxed: the reply carries a + /// full key pair, far larger than the other (unit/string) variants. + Received { + slate_id: String, + amount: u64, + reply: Box, + }, + /// Dropped permanently (marked processed). + Dropped(&'static str), + /// Rate limited — NOT marked processed, a legitimate burst retries later. + RateLimited, + /// Transient receive failure — NOT marked processed, the next catch-up + /// retries (an incoming payment is never silently lost on a hiccup). + Failed(String), +} + +/// The ingest state machine: dedupe, rate limits, unwrap, policy, handoff. +pub struct Ingest { + keys: Keys, + receiver: R, + /// Resolves an incoming wrap's `p` tag to the identity we hold for it + /// (master, or a per-invoice / per-user derived child). + directory: Arc, + /// Processed markers: wrap ids, rumor ids, `slate:` markers. + seen: Mutex>, + /// Per-sender sliding-window rate state (unix seconds of accepted events; + /// the `"\0global"` key carries the global unwrap ceiling). + rate: Mutex>>, +} + +impl Ingest { + /// Ingest for the single master identity (the milestone-3 default). + pub fn new(keys: Keys, receiver: R) -> Ingest { + let directory = Arc::new(MasterDirectory(keys.clone())); + Ingest::with_directory(keys, receiver, directory) + } + + /// Ingest with a multi-identity directory (master + derived children), so a + /// payment to a per-invoice or per-user endpub unwraps and its reply is + /// signed by that same identity. + pub fn with_directory(keys: Keys, receiver: R, directory: Arc) -> Ingest { + Ingest { + keys, + receiver, + directory, + seen: Mutex::new(HashSet::new()), + rate: Mutex::new(HashMap::new()), + } + } + + /// The wallet-handoff seam (for reconcile + status updates). + pub fn receiver(&self) -> &R { + &self.receiver + } + + /// The identities we watch (for the relay subscription filter). + pub fn watched(&self) -> Vec { + self.directory.watched() + } + + /// Resolve a recipient pubkey (hex) to the keys we hold, for reconcile. + pub fn resolve(&self, recipient_hex: &str) -> Option { + self.directory.resolve(recipient_hex) + } + + /// Build the S2 reply rumor from `from` to `payer` (also used by + /// reconcile). The rumor author is the identity that received the payment, + /// so the payer's wallet associates the reply with what it paid. + pub fn build_reply(&self, from: Keys, payer: PublicKey, s2_armor: &str) -> PendingReply { + let mut tags = protocol::build_rumor_tags(None); + tags.push(Tag::public_key(payer)); + let rumor = EventBuilder::new( + Kind::PrivateDirectMessage, + protocol::build_payment_content(s2_armor), + ) + .tags(tags) + .build(from.public_key()); + PendingReply { from, payer, rumor } + } + + /// Full guarded pipeline for one incoming gift wrap event, mirroring + /// Goblin's `handle_wrap` step for step (minus contacts/requests). + pub async fn handle_wrap(&self, event: &Event) -> IngestOutcome { + // 0. Only gift wraps. + if event.kind != Kind::GiftWrap { + return IngestOutcome::Dropped("not a gift wrap"); + } + let wrap_id = event.id.to_hex(); + // 1. Cheap size cap before any crypto. + if event.content.len() > protocol::MAX_WRAP_CONTENT { + self.mark(&wrap_id); + return IngestOutcome::Dropped("oversized wrap"); + } + // 2. Wrap-level dedupe. + if self.is_seen(&wrap_id) { + return IngestOutcome::Dropped("already processed"); + } + // 2.5 Global decrypt ceiling (fresh-keypair spam bound). Not marked + // processed — a genuine backlog re-attempts once the window reopens. + if !self.allow_global_unwrap() { + return IngestOutcome::RateLimited; + } + // 2.7 Resolve WHICH of our identities this wrap addresses, from its + // public `p` tag (how relays route NIP-59), then unwrap with that + // key. The master identity, a per-invoice derived child, or a + // per-user endpub all resolve here; anything else is not for us. + let recipient_hex = event.tags.iter().find_map(|t| { + let parts = t.as_slice(); + if parts.first().map(|s| s.as_str()) == Some("p") { + parts.get(1).cloned() + } else { + None + } + }); + let recipient_keys = match recipient_hex + .as_deref() + .and_then(|h| self.directory.resolve(h)) + { + Some(keys) => keys, + None => { + self.mark(&wrap_id); + return IngestOutcome::Dropped("not a watched identity"); + } + }; + let recipient_hex = recipient_keys.public_key().to_hex(); + // 3. Unwrap (version-dispatching; seal signature verified, rumor + // author must equal the seal signer — enforced inside). + let unwrapped = match crate::wrap::unwrap_gift_wrap(&recipient_keys, event) { + Ok(u) => u, + Err(_) => { + self.mark(&wrap_id); + return IngestOutcome::Dropped("unwrap failed"); + } + }; + let sender_hex = unwrapped.sender.to_hex(); + let mut rumor = unwrapped.rumor; + let rumor_id = rumor.id().to_hex(); + // 4. Policy over the message shape. + let armor = protocol::extract_slatepack(&rumor.content); + let decision = decide(&IngestContext { + sender: &sender_hex, + is_self: unwrapped.sender == self.keys.public_key(), + rumor_is_dm: rumor.kind == Kind::PrivateDirectMessage + && rumor.content.len() <= protocol::MAX_RUMOR_CONTENT, + has_slatepack: armor.is_some(), + duplicate: self.is_seen(&rumor_id), + }); + let reason = match decision { + IngestDecision::AutoReceive => None, + IngestDecision::Drop(reason) => Some(reason), + }; + if let Some(reason) = reason { + self.mark(&wrap_id); + self.mark(&rumor_id); + return IngestOutcome::Dropped(reason); + } + // 5. Rate limit per sender. Deliberately NOT marked processed: + // legitimate bursts can retry later (Goblin's rule). + if !self.allow_sender(&sender_hex) { + warn!("ingest: rate limited sender {}…", &sender_hex[..8]); + return IngestOutcome::RateLimited; + } + // 6. Hand the armor to the wallet (parse, S1-only check, receive_tx, + // persist, match to an invoice/user — all enforced on the wallet + // + core side). The memo (subject tag) and the receiving identity + // are what the matching layer keys off. + let armor = armor.expect("checked by decide"); + let memo = protocol::extract_subject(&rumor.tags); + let ctx = IncomingContext { + payer_hex: &sender_hex, + recipient_hex: &recipient_hex, + memo: memo.as_deref(), + }; + match self.receiver.receive(&armor, &ctx).await { + Ok(payment) => { + // Durable: commit dedupe markers before the reply leg, so a + // crash there cannot re-trigger a second receive on catch-up + // (grin's TransactionAlreadyReceived also backstops this). + self.mark(&wrap_id); + self.mark(&rumor_id); + self.mark(&format!("slate:{}", payment.slate_id)); + info!( + "ingest: received slate {} ({} nanogrin) from {}…", + payment.slate_id, + payment.amount, + &sender_hex[..8] + ); + let reply = self.build_reply(recipient_keys, unwrapped.sender, &payment.s2_armor); + IngestOutcome::Received { + slate_id: payment.slate_id, + amount: payment.amount, + reply: Box::new(reply), + } + } + Err(ReceiveError::Duplicate) => { + self.mark(&wrap_id); + self.mark(&rumor_id); + IngestOutcome::Dropped("slate already received") + } + Err(ReceiveError::Rejected(m)) => { + self.mark(&wrap_id); + self.mark(&rumor_id); + warn!("ingest: rejected slatepack from {}…: {m}", &sender_hex[..8]); + IngestOutcome::Dropped("invalid slatepack") + } + // Transient: leave UNMARKED so the next catch-up retries. + Err(ReceiveError::Failed(m)) => IngestOutcome::Failed(m), + } + } + + fn is_seen(&self, key: &str) -> bool { + self.seen.lock().expect("seen lock").contains(key) + } + + fn mark(&self, key: &str) { + self.seen.lock().expect("seen lock").insert(key.to_string()); + } + + /// Sliding-window per-sender rate limiter (Goblin's `allow_sender`). + fn allow_sender(&self, sender: &str) -> bool { + let now = unix_time(); + let mut rate = self.rate.lock().expect("rate lock"); + let hits = rate.entry(sender.to_string()).or_default(); + hits.retain(|t| now - *t < 3600); + if hits.len() >= RATE_PER_SENDER_PER_HOUR { + return false; + } + hits.push(now); + if rate.len() > RATE_MAP_CAP { + rate.retain(|_, v| v.iter().any(|t| now - *t < 3600)); + } + true + } + + /// Global unwrap ceiling (Goblin's `allow_global_unwrap`). + fn allow_global_unwrap(&self) -> bool { + let now = unix_time(); + let mut rate = self.rate.lock().expect("rate lock"); + let hits = rate.entry("\0global".to_string()).or_default(); + hits.retain(|t| now - *t < 60); + if hits.len() >= GLOBAL_UNWRAP_PER_MIN { + return false; + } + hits.push(now); + true + } +} + +#[cfg(test)] +mod tests { + use std::sync::Mutex as StdMutex; + + use nostr_sdk::EventBuilder; + + use super::*; + use crate::{ReceivedPayment, UnrepliedPayment}; + + const ALICE: &str = "91cf9dbbea5e6511fd2bbb190b112055ee4131c5d2bbb9faedf3ee8cbeac0d05"; + + fn ctx<'a>(sender: &'a str) -> IngestContext<'a> { + IngestContext { + sender, + is_self: false, + rumor_is_dm: true, + has_slatepack: true, + duplicate: false, + } + } + + #[test] + fn fresh_payment_auto_receives_from_anyone() { + assert_eq!(decide(&ctx(ALICE)), IngestDecision::AutoReceive); + } + + #[test] + fn duplicates_own_messages_and_junk_drop() { + let mut c = ctx(ALICE); + c.duplicate = true; + assert!(matches!(decide(&c), IngestDecision::Drop(_))); + + let mut c = ctx(ALICE); + c.is_self = true; + assert!(matches!(decide(&c), IngestDecision::Drop(_))); + + let mut c = ctx(ALICE); + c.rumor_is_dm = false; + assert!(matches!(decide(&c), IngestDecision::Drop(_))); + + let mut c = ctx(ALICE); + c.has_slatepack = false; + assert!(matches!(decide(&c), IngestDecision::Drop(_))); + } + + /// A directory over an explicit set of identities, for the derived-key test. + struct MultiDirectory(Vec); + + impl KeyDirectory for MultiDirectory { + fn resolve(&self, recipient_hex: &str) -> Option { + self.0 + .iter() + .find(|k| k.public_key().to_hex() == recipient_hex) + .cloned() + } + + fn watched(&self) -> Vec { + self.0.iter().map(|k| k.public_key()).collect() + } + } + + /// Scripted stand-in for the wallet handoff. Captures the last context so + /// tests can assert the recipient identity and memo were threaded through. + struct StubReceiver { + outcomes: StdMutex>>, + calls: StdMutex, + last_recipient: StdMutex>, + last_memo: StdMutex>, + } + + impl StubReceiver { + fn new(outcomes: Vec>) -> StubReceiver { + StubReceiver { + outcomes: StdMutex::new(outcomes), + calls: StdMutex::new(0), + last_recipient: StdMutex::new(None), + last_memo: StdMutex::new(None), + } + } + + fn calls(&self) -> usize { + *self.calls.lock().unwrap() + } + } + + impl SlatepackReceiver for StubReceiver { + async fn receive( + &self, + _s1_armor: &str, + ctx: &IncomingContext<'_>, + ) -> Result { + *self.calls.lock().unwrap() += 1; + *self.last_recipient.lock().unwrap() = Some(ctx.recipient_hex.to_string()); + *self.last_memo.lock().unwrap() = ctx.memo.map(|m| m.to_string()); + self.outcomes.lock().unwrap().remove(0) + } + + async fn mark_replied(&self, _slate_id: &str) {} + + async fn unreplied(&self) -> Vec { + vec![] + } + } + + const PACK: &str = "BEGINSLATEPACK. 4H1qx1wHe668tFW yC2gfL8PPd8kSgv \ + pcXQhyRkHbyKHZg GN75o7uWoT3dkib. ENDSLATEPACK."; + + fn payment_wrap(payer: &Keys, server: &Keys, version: crate::wrap::WrapVersion) -> Event { + payment_wrap_noted(payer, server, version, "test") + } + + /// Like [`payment_wrap`] but with a distinct note, so rumors built within + /// the same second (same content, seconds-resolution `created_at`) do not + /// collide on the rumor id — distinct real payments always differ by + /// their slatepack. + fn payment_wrap_noted( + payer: &Keys, + server: &Keys, + version: crate::wrap::WrapVersion, + note: &str, + ) -> Event { + let mut tags = protocol::build_rumor_tags(Some(note)); + tags.push(Tag::public_key(server.public_key())); + let rumor = EventBuilder::new( + Kind::PrivateDirectMessage, + protocol::build_payment_content(PACK), + ) + .tags(tags) + .build(payer.public_key()); + crate::wrap::gift_wrap(payer, &server.public_key(), rumor, version).unwrap() + } + + #[tokio::test] + async fn pipeline_receives_replies_and_dedupes() { + let payer = Keys::generate(); + let server = Keys::generate(); + let ingest = Ingest::new( + server.clone(), + StubReceiver::new(vec![Ok(ReceivedPayment { + slate_id: "slate-1".into(), + amount: 42, + s2_armor: "BEGINSLATEPACK. reply. ENDSLATEPACK.".into(), + })]), + ); + let wrap = payment_wrap(&payer, &server, crate::wrap::WrapVersion::V3); + + let outcome = ingest.handle_wrap(&wrap).await; + let reply = match outcome { + IngestOutcome::Received { + slate_id, + amount, + reply, + } => { + assert_eq!(slate_id, "slate-1"); + assert_eq!(amount, 42); + reply + } + other => panic!("expected Received, got {other:?}"), + }; + assert_eq!(reply.payer, payer.public_key()); + assert_eq!(reply.rumor.kind, Kind::PrivateDirectMessage); + assert!(protocol::extract_slatepack(&reply.rumor.content).is_some()); + + // The same wrap again drops without another wallet call. + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Dropped(_) + )); + assert_eq!(ingest.receiver().calls(), 1); + } + + #[tokio::test] + async fn transient_failure_is_retryable_permanent_rejection_is_not() { + let payer = Keys::generate(); + let server = Keys::generate(); + let ingest = Ingest::new( + server.clone(), + StubReceiver::new(vec![ + Err(ReceiveError::Failed("wallet hiccup".into())), + Ok(ReceivedPayment { + slate_id: "slate-2".into(), + amount: 7, + s2_armor: "BEGINSLATEPACK. r. ENDSLATEPACK.".into(), + }), + ]), + ); + let wrap = payment_wrap(&payer, &server, crate::wrap::WrapVersion::V2); + + // Transient failure leaves the wrap unmarked... + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Failed(_) + )); + // ...so the catch-up retry succeeds. + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Received { .. } + )); + + // A rejected slatepack is a permanent drop. + let ingest = Ingest::new( + server.clone(), + StubReceiver::new(vec![Err(ReceiveError::Rejected("not S1".into()))]), + ); + let wrap = payment_wrap(&payer, &server, crate::wrap::WrapVersion::V2); + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Dropped(_) + )); + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Dropped("already processed") + )); + assert_eq!(ingest.receiver().calls(), 1); + } + + #[tokio::test] + async fn non_payment_messages_never_reach_the_wallet() { + let payer = Keys::generate(); + let server = Keys::generate(); + let ingest = Ingest::new(server.clone(), StubReceiver::new(vec![])); + + // A DM without a slatepack. + let rumor = EventBuilder::new(Kind::PrivateDirectMessage, "just chatting") + .tags([Tag::public_key(server.public_key())]) + .build(payer.public_key()); + let wrap = crate::wrap::gift_wrap( + &payer, + &server.public_key(), + rumor, + crate::wrap::WrapVersion::V2, + ) + .unwrap(); + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Dropped("no slatepack payload") + )); + + // A non-gift-wrap event. + let plain = EventBuilder::new(Kind::TextNote, "hello") + .sign_with_keys(&payer) + .unwrap(); + assert!(matches!( + ingest.handle_wrap(&plain).await, + IngestOutcome::Dropped("not a gift wrap") + )); + + // A wrap addressed to someone else: its `p` tag resolves to no + // identity we watch, so it drops before any decrypt attempt. + let other = Keys::generate(); + let wrap = payment_wrap(&payer, &other, crate::wrap::WrapVersion::V3); + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Dropped("not a watched identity") + )); + assert_eq!(ingest.receiver().calls(), 0); + } + + #[tokio::test] + async fn per_sender_rate_limit_kicks_in() { + let payer = Keys::generate(); + let server = Keys::generate(); + let outcomes = (0..RATE_PER_SENDER_PER_HOUR) + .map(|i| { + Ok(ReceivedPayment { + slate_id: format!("slate-{i}"), + amount: 1, + s2_armor: "BEGINSLATEPACK. r. ENDSLATEPACK.".into(), + }) + }) + .collect(); + let ingest = Ingest::new(server.clone(), StubReceiver::new(outcomes)); + + for i in 0..RATE_PER_SENDER_PER_HOUR { + let wrap = payment_wrap_noted( + &payer, + &server, + crate::wrap::WrapVersion::V2, + &format!("payment {i}"), + ); + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::Received { .. } + )); + } + // One more within the hour: rate limited, wallet untouched. + let wrap = payment_wrap_noted( + &payer, + &server, + crate::wrap::WrapVersion::V2, + "one too many", + ); + assert!(matches!( + ingest.handle_wrap(&wrap).await, + IngestOutcome::RateLimited + )); + assert_eq!(ingest.receiver().calls(), RATE_PER_SENDER_PER_HOUR); + } + + #[tokio::test] + async fn derived_identity_is_resolved_and_reply_signed_from_it() { + // A payment addressed to a derived child (not the master) unwraps via + // the directory, threads the recipient + memo to the wallet handoff, + // and its reply is signed FROM the derived identity the payer paid. + let payer = Keys::generate(); + let master = Keys::generate(); + let derived = Keys::generate(); // stands in for a per-invoice child + let directory = Arc::new(MultiDirectory(vec![master.clone(), derived.clone()])); + let ingest = Ingest::with_directory( + master.clone(), + StubReceiver::new(vec![Ok(ReceivedPayment { + slate_id: "slate-x".into(), + amount: 5, + s2_armor: "BEGINSLATEPACK. r. ENDSLATEPACK.".into(), + })]), + directory, + ); + + // Payer wraps to the DERIVED key with an order memo. + let mut tags = protocol::build_rumor_tags(Some("order-99")); + tags.push(Tag::public_key(derived.public_key())); + let rumor = EventBuilder::new( + Kind::PrivateDirectMessage, + protocol::build_payment_content(PACK), + ) + .tags(tags) + .build(payer.public_key()); + let wrap = crate::wrap::gift_wrap( + &payer, + &derived.public_key(), + rumor, + crate::wrap::WrapVersion::V3, + ) + .unwrap(); + + let reply = match ingest.handle_wrap(&wrap).await { + IngestOutcome::Received { reply, .. } => reply, + other => panic!("expected Received, got {other:?}"), + }; + // The reply comes FROM the derived identity, TO the payer. + assert_eq!(reply.from.public_key(), derived.public_key()); + assert_eq!(reply.payer, payer.public_key()); + // The wallet handoff saw the derived recipient and the memo. + assert_eq!( + ingest.receiver().last_recipient.lock().unwrap().as_deref(), + Some(derived.public_key().to_hex().as_str()) + ); + assert_eq!( + ingest.receiver().last_memo.lock().unwrap().as_deref(), + Some("order-99") + ); + } +} diff --git a/crates/gp-nostr/src/lib.rs b/crates/gp-nostr/src/lib.rs new file mode 100644 index 0000000..ed4e862 --- /dev/null +++ b/crates/gp-nostr/src/lib.rs @@ -0,0 +1,206 @@ +//! Nostr transport and secure handoff for GoblinPay, mirroring Goblin's +//! proven `src/nostr` + `src/nym` stack adapted to a headless daemon: +//! +//! - [`identity`]: a random standalone nsec (or an imported one), NIP-49 +//! encrypted at rest, deliberately independent of the Grin seed (the +//! two-secrets rule). +//! - [`wrap`]: NIP-59 gift wrap build/unwrap with the NIP-17 backward-compat +//! extension — NIP-44 v3 (kind/scope context binding, via the companion +//! `nip44` crate) negotiated per recipient, v2 via nostr-sdk as the +//! mandatory baseline. +//! - [`protocol`]: the Goblin payment message layout (kind-14 rumor carrying +//! one slatepack armor block). +//! - [`ingest`]: the guarded ingest pipeline (dedupe, rate limits, the pure +//! `decide()` policy) handing S1 slatepacks to the wallet and building the +//! S2 reply rumor. +//! - [`service`]: the daemon loop — relay pool over the in-process Nym +//! mixnet, kind-10050 publishing, catch-up + live subscription, reply +//! dispatch, boot-time reconcile. +//! - [`nym`]: the smolmix tunnel, mix-dns and the relay websocket transport, +//! ported from Goblin (G14). +//! +//! Privacy: log lines carry short event/key prefixes and hosts only — never +//! armor contents, full URLs, or secrets (Goblin's host-only level). + +pub mod identity; +pub mod ingest; +pub mod nym; +pub mod protocol; +pub mod receipt; +pub mod relays; +pub mod service; +pub mod wrap; + +/// Re-exported so downstream crates (gp-server) can name the identity key types +/// without depending on nostr-sdk directly. +pub use nostr_sdk::{Keys, PublicKey}; + +/// What the wallet hands back for one received S1 slatepack. Mirrors +/// `gp_wallet::Received`, redefined here so the transport crate never links +/// the Grin stack (the wallet side plugs in through [`SlatepackReceiver`]). +#[derive(Debug, Clone)] +pub struct ReceivedPayment { + /// Slate UUID, shared by S1, S2, and the final transaction. + pub slate_id: String, + /// Amount in nanogrin, as stated by the slate. + pub amount: u64, + /// The S2 reply slatepack armor for the payer to finalize. + pub s2_armor: String, +} + +/// A payment whose S2 reply has not (verifiably) reached the payer yet, +/// surfaced by the store for the boot-time reconcile pass. +#[derive(Debug, Clone)] +pub struct UnrepliedPayment { + /// Slate UUID. + pub slate_id: String, + /// Payer public key, hex. + pub payer_hex: String, + /// The stored S2 reply armor. + pub s2_armor: String, + /// Our identity that received it (master or a derived child), x-only hex, + /// so the reply is re-sent from the right key. + pub recipient_hex: String, +} + +/// Context threaded from the ingest pipeline into the wallet handoff: who paid, +/// which of our identities received the payment (the master key or a per-invoice +/// / per-user derived child), and the payer's memo. The recipient and memo are +/// what the matching layer (milestone 5) keys off. +#[derive(Debug, Clone)] +pub struct IncomingContext<'a> { + /// Seal-verified sender public key, hex. + pub payer_hex: &'a str, + /// The identity that received it (x-only hex). + pub recipient_hex: &'a str, + /// The payer's sanitized memo (subject tag), if any. + pub memo: Option<&'a str>, +} + +/// Why a receive was refused. +#[derive(Debug)] +pub enum ReceiveError { + /// This slate was already received — drop the wrap permanently. + Duplicate, + /// The slatepack is invalid (bad armor, wrong state, garbage) — drop the + /// wrap permanently. + Rejected(String), + /// Transient failure (wallet/db hiccup) — leave the wrap unmarked so the + /// next catch-up retries it (an incoming payment must never be silently + /// lost on a momentary hiccup; Goblin's rule). + Failed(String), +} + +impl std::fmt::Display for ReceiveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReceiveError::Duplicate => write!(f, "slate already received"), + ReceiveError::Rejected(m) => write!(f, "slatepack rejected: {m}"), + ReceiveError::Failed(m) => write!(f, "receive failed: {m}"), + } + } +} + +impl std::error::Error for ReceiveError {} + +/// The secure handoff seam into the wallet (gp-server implements it over +/// `gp_wallet::GpWallet` + SQLite). Only armored slatepack strings cross the +/// boundary, exactly like production Goblin. +#[allow(async_fn_in_trait)] // consumed generically, never as `dyn` +pub trait SlatepackReceiver: Send + Sync { + /// Receive an S1 slatepack (parse, `receive_tx`, persist, match to an + /// invoice/user) and return the S2 reply. `ctx` carries the payer, the + /// receiving identity, and the memo the matching layer keys off. + async fn receive( + &self, + s1_armor: &str, + ctx: &IncomingContext<'_>, + ) -> Result; + + /// Mark a payment's S2 reply as dispatched (a relay accepted it). + async fn mark_replied(&self, slate_id: &str); + + /// Payments still awaiting their S2 dispatch (for boot-time reconcile). + async fn unreplied(&self) -> Vec; +} + +/// Resolves an incoming gift wrap's `p` tag (the recipient x-only hex) to the +/// secret keys we hold for it, and lists the identities we currently watch. +/// +/// The default is the master identity alone; gp-server supplies a DB-backed +/// directory that also resolves per-invoice (matching mode 2) and per-user +/// (5b) derived children, so a payment sent to any of those unwraps and its +/// reply is signed by the same identity the payer addressed. +pub trait KeyDirectory: Send + Sync { + /// The keys for a recipient pubkey (hex), or `None` if we do not hold it. + fn resolve(&self, recipient_hex: &str) -> Option; + /// Every pubkey we currently watch (always includes the master), for the + /// relay subscription filter. + fn watched(&self) -> Vec; +} + +/// The default single-identity directory: the server master key only. +pub struct MasterDirectory(pub nostr_sdk::Keys); + +impl KeyDirectory for MasterDirectory { + fn resolve(&self, recipient_hex: &str) -> Option { + if self.0.public_key().to_hex() == recipient_hex { + Some(self.0.clone()) + } else { + None + } + } + + fn watched(&self) -> Vec { + vec![self.0.public_key()] + } +} + +/// Build `Keys` from a raw 32-byte secret (used by DB-backed directories to +/// reconstruct a derived child from its recomputed secret). +pub fn keys_from_secret(secret: &[u8; 32]) -> Result { + let sk = nostr_sdk::SecretKey::from_slice(secret).map_err(|e| e.to_string())?; + Ok(nostr_sdk::Keys::new(sk)) +} + +/// Bech32 npub for a key pair (for logs and the merchant QR). +pub fn npub(keys: &nostr_sdk::Keys) -> String { + use nostr_sdk::ToBech32; + keys.public_key().to_bech32().unwrap_or_default() +} + +/// Bech32 npub for a public key (checkout page display). +pub fn npub_of(pk: nostr_sdk::PublicKey) -> String { + use nostr_sdk::ToBech32; + pk.to_bech32().unwrap_or_default() +} + +/// Parse a public key from a bech32 `npub` or a raw hex string (for the +/// configured merchant identity). +pub fn pubkey_from_str(s: &str) -> Option { + use nostr_sdk::FromBech32; + let s = s.trim(); + nostr_sdk::PublicKey::from_bech32(s) + .ok() + .or_else(|| nostr_sdk::PublicKey::from_hex(s).ok()) +} + +/// Bech32 `nprofile` for a public key plus its relay hints (the checkout QR +/// payload; a Goblin wallet scans it to know where to send). +pub fn nprofile(pk: nostr_sdk::PublicKey, relays: &[String]) -> String { + use nostr_sdk::nips::nip19::Nip19Profile; + use nostr_sdk::{RelayUrl, ToBech32}; + let urls: Vec = relays + .iter() + .filter_map(|r| RelayUrl::parse(r).ok()) + .collect(); + Nip19Profile::new(pk, urls).to_bech32().unwrap_or_default() +} + +/// Unix time in seconds (mirrors Goblin's helper). +pub(crate) fn unix_time() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} diff --git a/crates/gp-nostr/src/nym/dns.rs b/crates/gp-nostr/src/nym/dns.rs new file mode 100644 index 0000000..4adfcf6 --- /dev/null +++ b/crates/gp-nostr/src/nym/dns.rs @@ -0,0 +1,235 @@ +//! mix-dns: hostname resolution THROUGH the mixnet (ported from +//! `goblin/src/nym/dns.rs`). `Tunnel::tcp_connect` takes a `SocketAddr`, so +//! DNS is our responsibility — and it MUST ride the tunnel: a clearnet lookup +//! would leak exactly which relays the server contacts, defeating the mixnet. +//! Raw A-record queries go as UDP datagrams over +//! [`smolmix::Tunnel::udp_socket`] to public resolvers addressed BY IP. +//! Responses land in a TTL-respecting in-memory cache. IPv4-only, like the +//! Goblin original. +//! +//! Wire codec: hickory-proto — already in the dependency graph via +//! nym-http-api-client, so no vendored encode/parse is needed. + +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::{LazyLock, RwLock}; +use std::time::{Duration, Instant}; + +use hickory_proto::op::{Message, MessageType, Query, ResponseCode}; +use hickory_proto::rr::{Name, RData, RecordType}; +use log::{debug, warn}; +use smolmix::Tunnel; + +/// Public resolvers the tunnel queries, by IP (no bootstrap chicken-and-egg): +/// Cloudflare primary, Quad9 fallback. The exit gateway only ever sees a DNS +/// packet to a public resolver, never who asked. +const RESOLVERS: [SocketAddr; 2] = [ + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 53), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9)), 53), +]; + +/// Per-resolver answer wait. The mixnet adds multi-second round trips. +const QUERY_TIMEOUT: Duration = Duration::from_secs(15); + +/// TTL floor/ceiling for the cache: don't hammer resolvers for zero-TTL +/// records, don't trust a stale record for more than an hour. +const TTL_FLOOR_SECS: u32 = 60; +const TTL_CEILING_SECS: u32 = 3600; + +/// Cached answer for one host: addresses plus their expiry. +type CachedAnswer = (Vec, Instant); + +/// host → cached answer. +static CACHE: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +/// Resolve `host` to a socket address for `tcp_connect`, entirely over the +/// mixnet. IP-literal hosts skip DNS; cached answers are honored until their +/// (clamped) TTL lapses. Returns `None` when every resolver fails. +pub async fn resolve(tunnel: &Tunnel, host: &str, port: u16) -> Option { + // IP literals (v4 or v6) need no lookup at all. + if let Ok(ip) = host.parse::() { + return Some(SocketAddr::new(ip, port)); + } + if let Some(ip) = cached(host) { + return Some(SocketAddr::new(IpAddr::V4(ip), port)); + } + for resolver in RESOLVERS { + match query_a(tunnel, host, resolver).await { + Some((ips, ttl)) if !ips.is_empty() => { + let ttl = ttl.clamp(TTL_FLOOR_SECS, TTL_CEILING_SECS); + debug!( + "mix-dns: resolved {host} -> {} (ttl {ttl}s, via {resolver}, {} record(s))", + ips[0], + ips.len() + ); + let expiry = Instant::now() + Duration::from_secs(ttl as u64); + CACHE + .write() + .expect("dns cache lock") + .insert(host.to_string(), (ips.clone(), expiry)); + return Some(SocketAddr::new(IpAddr::V4(ips[0]), port)); + } + _ => { + warn!("mix-dns: no answer for {host} from {resolver}, trying next resolver"); + } + } + } + warn!("mix-dns: resolution failed for {host} (all resolvers)"); + None +} + +/// A cached, unexpired address for `host`. +fn cached(host: &str) -> Option { + let cache = CACHE.read().expect("dns cache lock"); + let (ips, expiry) = cache.get(host)?; + if Instant::now() < *expiry { + ips.first().copied() + } else { + None + } +} + +/// Cheap end-to-end liveness probe: one uncached A query for a stable name +/// against the primary resolver. Used by the tunnel keepalive/watchdog — it +/// exercises the full path (mixnet → IPR exit → internet and back) and, as a +/// side effect, keeps the gateway connection and IPR session from idling out. +pub async fn probe(tunnel: &Tunnel) -> bool { + query_a(tunnel, "example.com", RESOLVERS[0]).await.is_some() +} + +/// One A query/response round trip over the tunnel against `resolver`. +async fn query_a( + tunnel: &Tunnel, + host: &str, + resolver: SocketAddr, +) -> Option<(Vec, u32)> { + let udp = match tunnel.udp_socket().await { + Ok(s) => s, + Err(e) => { + warn!("mix-dns: udp socket failed: {e}"); + return None; + } + }; + let id = rand::random::(); + let query = encode_query(id, host)?; + if let Err(e) = udp.send_to(&query, resolver).await { + warn!("mix-dns: send to {resolver} failed: {e}"); + return None; + } + let mut buf = vec![0u8; 1500]; + let (n, from) = match tokio::time::timeout(QUERY_TIMEOUT, udp.recv_from(&mut buf)).await { + Ok(Ok(r)) => r, + Ok(Err(e)) => { + warn!("mix-dns: recv from {resolver} failed: {e}"); + return None; + } + Err(_) => { + warn!("mix-dns: query to {resolver} timed out"); + return None; + } + }; + if from != resolver { + warn!("mix-dns: dropping answer from unexpected source {from}"); + return None; + } + parse_response(id, &buf[..n]) +} + +/// Encode a recursive A query for `host` with transaction id `id`. +fn encode_query(id: u16, host: &str) -> Option> { + let name = Name::from_ascii(host).ok()?; + let mut msg = Message::query(); + msg.metadata.id = id; + msg.metadata.recursion_desired = true; + msg.add_query(Query::query(name, RecordType::A)); + msg.to_vec().ok() +} + +/// Parse a response to transaction `id`: all A records in the answer section +/// plus the smallest TTL among them. `None` on id mismatch, non-response, +/// error rcode or no A records (CNAMEs and other types are skipped). +fn parse_response(id: u16, raw: &[u8]) -> Option<(Vec, u32)> { + let msg = Message::from_vec(raw).ok()?; + if msg.metadata.id != id + || msg.metadata.message_type != MessageType::Response + || msg.metadata.response_code != ResponseCode::NoError + { + return None; + } + let mut ips = Vec::new(); + let mut ttl = u32::MAX; + for record in &msg.answers { + if let RData::A(a) = record.data { + ips.push(a.0); + ttl = ttl.min(record.ttl); + } + } + if ips.is_empty() { + None + } else { + Some((ips, ttl)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Query for `example.com` A/IN, id 0x1234, RD set — the canonical fixture + /// (same bytes smolmix's own docs use). + const QUERY_FIXTURE: &[u8] = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\ + \x07example\x03com\x00\x00\x01\x00\x01"; + + /// Response to `QUERY_FIXTURE`: flags 0x8180 (QR, RD, RA, NOERROR), one + /// question, two answers — a CNAME (ttl 3600, rdata = compression pointer + /// back to the qname) that must be skipped, then an A record for + /// 93.184.216.34 with ttl 300. + const RESPONSE_FIXTURE: &[u8] = b"\x12\x34\x81\x80\x00\x01\x00\x02\x00\x00\x00\x00\ + \x07example\x03com\x00\x00\x01\x00\x01\ + \xc0\x0c\x00\x05\x00\x01\x00\x00\x0e\x10\x00\x02\xc0\x0c\ + \xc0\x0c\x00\x01\x00\x01\x00\x00\x01\x2c\x00\x04\x5d\xb8\xd8\x22"; + + #[test] + fn encode_query_matches_fixture() { + let bytes = encode_query(0x1234, "example.com").unwrap(); + assert_eq!(bytes, QUERY_FIXTURE); + } + + #[test] + fn parse_response_extracts_a_records_and_min_ttl() { + let (ips, ttl) = parse_response(0x1234, RESPONSE_FIXTURE).unwrap(); + assert_eq!(ips, vec![Ipv4Addr::new(93, 184, 216, 34)]); + // The CNAME's larger ttl (3600) must not win: only A records count. + assert_eq!(ttl, 300); + } + + #[test] + fn parse_response_rejects_wrong_id() { + assert!(parse_response(0x5678, RESPONSE_FIXTURE).is_none()); + } + + #[test] + fn parse_response_rejects_query_and_garbage() { + // A query (QR=0) is not an answer. + assert!(parse_response(0x1234, QUERY_FIXTURE).is_none()); + // Truncated/garbage input parses to nothing. + assert!(parse_response(0x1234, &RESPONSE_FIXTURE[..7]).is_none()); + assert!(parse_response(0x1234, b"\x00").is_none()); + } + + #[test] + fn parse_response_rejects_error_rcode() { + // Same fixture with rcode NXDOMAIN (flags 0x8183) and no answers. + let nx: &[u8] = b"\x12\x34\x81\x83\x00\x01\x00\x00\x00\x00\x00\x00\ + \x07example\x03com\x00\x00\x01\x00\x01"; + assert!(parse_response(0x1234, nx).is_none()); + } + + #[test] + fn ttl_clamp_bounds() { + assert_eq!(5u32.clamp(TTL_FLOOR_SECS, TTL_CEILING_SECS), 60); + assert_eq!(999_999u32.clamp(TTL_FLOOR_SECS, TTL_CEILING_SECS), 3600); + assert_eq!(300u32.clamp(TTL_FLOOR_SECS, TTL_CEILING_SECS), 300); + } +} diff --git a/crates/gp-nostr/src/nym/mod.rs b/crates/gp-nostr/src/nym/mod.rs new file mode 100644 index 0000000..daeadcc --- /dev/null +++ b/crates/gp-nostr/src/nym/mod.rs @@ -0,0 +1,22 @@ +//! Nym mixnet transport, ported from Goblin's proven `src/nym` (G14). +//! Every relay websocket rides one in-process smolmix +//! [`Tunnel`](smolmix::Tunnel) over the 5-hop mixnet to an auto-selected IPR +//! exit. Hostnames resolve through the same tunnel ([`dns`], mix-dns), so +//! neither payload nor destination ever touches the clearnet. For a payment +//! server this is default-on: returning the S2 means outbound connections to +//! the payer's relays, which over clearnet would link the merchant identity +//! to a host IP. +//! +//! This tunnel carries ONLY the Nostr gift-wrap layer. The milestone-4 +//! node-confirmation reads (wallet -> node get_kernel/get_tip) deliberately do +//! NOT ride it: node traffic is a server concern that goes DIRECT over normal +//! HTTP (owner ruling), exactly like Goblin's own wallet -> node reads never +//! ride the mixnet. Those reads live in `gp-wallet`, which has no Nym linkage, +//! so the direct path is structural. Do not route node reads through here. + +pub mod dns; +pub mod nymproc; +pub mod transport; + +pub use nymproc::{is_ready, warm_up}; +pub use transport::NymWebSocketTransport; diff --git a/crates/gp-nostr/src/nym/nymproc.rs b/crates/gp-nostr/src/nym/nymproc.rs new file mode 100644 index 0000000..a6632bd --- /dev/null +++ b/crates/gp-nostr/src/nym/nymproc.rs @@ -0,0 +1,192 @@ +//! In-process Nym mixnet tunnel (ported from `goblin/src/nym/nymproc.rs`). +//! smolmix is linked directly — no sidecar subprocess, no loopback SOCKS5 +//! seam. One process-lifetime [`Tunnel`] carries every relay websocket as raw +//! TCP over the mixnet to an AUTO-SELECTED IPR exit gateway: losing any one +//! exit just re-selects, so there is no single-exit SPOF. Hostnames are +//! resolved through the same tunnel by [`super::dns`] (mix-dns); nothing goes +//! clearnet. +//! +//! Same liveness posture as Goblin: a fresh tunnel must pass an end-to-end +//! probe before it is published (some exits accept the IPR handshake but +//! never deliver data), and a keepalive watchdog rebuilds on sustained +//! failure. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::RwLock; +use std::thread; +use std::time::{Duration, Instant}; + +use log::{error, info, warn}; +use smolmix::Tunnel; + +/// The shared process-lifetime tunnel, set once the mixnet bootstrap finishes. +static TUNNEL: RwLock> = RwLock::new(None); + +/// Set once the tunnel is up (mirrors `TUNNEL`, but cheap to poll). +static MIXNET_READY: AtomicBool = AtomicBool::new(false); + +/// Guards the background bootstrap thread so `warm_up()` is idempotent. +static STARTED: AtomicBool = AtomicBool::new(false); + +/// Pre-warm the mixnet tunnel in the background so relays are ready by first +/// use. Idempotent — later calls (including the lazy-init path in +/// [`wait_for_tunnel`]) are no-ops. +pub fn warm_up() { + if STARTED.swap(true, Ordering::SeqCst) { + return; + } + thread::spawn(run_tunnel); +} + +/// Whether the mixnet tunnel is warm. Cheap and cached. Distinct from a +/// relay being connected. +pub fn is_ready() -> bool { + MIXNET_READY.load(Ordering::Relaxed) +} + +/// The shared tunnel, if it is up. Cloning is a cheap `Arc` bump. +pub fn tunnel() -> Option { + TUNNEL.read().expect("tunnel lock").clone() +} + +/// Wait until the shared tunnel is up, starting the bootstrap if nothing has +/// yet (lazy init on first use). Returns `None` once `timeout` lapses. +pub async fn wait_for_tunnel(timeout: Duration) -> Option { + warm_up(); + let deadline = Instant::now() + timeout; + loop { + if let Some(t) = tunnel() { + return Some(t); + } + if Instant::now() >= deadline { + return None; + } + tokio::time::sleep(Duration::from_millis(250)).await; + } +} + +/// Build the mixnet tunnel on a dedicated multi-thread tokio runtime, then +/// keep the tunnel (its bridge + smoltcp reactor tasks) AND the runtime alive +/// for the lifetime of the process. Retries with backoff on bootstrap failure +/// (a dead gateway pick just re-selects on the next attempt). Blocks the +/// calling thread. +fn run_tunnel() { + let rt = match tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + { + Ok(rt) => rt, + Err(e) => { + error!("nym: could not build mixnet runtime: {e}"); + return; + } + }; + rt.block_on(async move { + let mut delay = Duration::from_secs(5); + loop { + let started = Instant::now(); + info!("nym: starting in-process mixnet tunnel (smolmix, auto-selected exit)"); + match build_tunnel().await { + Ok(tunnel) => { + // Gate readiness on one end-to-end probe: some exits accept + // the IPR handshake but never deliver data (seen live); + // publishing such a tunnel would blackhole every consumer + // until the watchdog caught it minutes later. Re-select + // immediately instead. + if !probe_fresh(&tunnel).await { + error!( + "nym: fresh tunnel failed its liveness probe (dead exit); re-selecting" + ); + tunnel.shutdown().await; + delay = (delay * 2).min(Duration::from_secs(60)); + continue; + } + info!( + "nym: tunnel ready in ~{}ms (allocated ip {}, probe ok)", + started.elapsed().as_millis(), + tunnel.allocated_ips().ipv4 + ); + *TUNNEL.write().expect("tunnel lock") = Some(tunnel.clone()); + MIXNET_READY.store(true, Ordering::Relaxed); + delay = Duration::from_secs(5); + // Hold the tunnel warm for the whole process lifetime with + // a cheap keepalive: the probe keeps the gateway + // connection + IPR session from idling out while the relay + // subscription rides it — and verifies the path end to + // end. When the tunnel dies anyway (exit gateway gone), + // rebuild with a freshly auto-selected exit: losing any + // one exit must never take the server down. + watch_tunnel(&tunnel).await; + error!("nym: tunnel unresponsive; rebuilding with a fresh exit"); + MIXNET_READY.store(false, Ordering::Relaxed); + *TUNNEL.write().expect("tunnel lock") = None; + tunnel.shutdown().await; + } + Err(e) => { + error!( + "nym: mixnet tunnel failed to start: {e}; retrying in {}s", + delay.as_secs() + ); + tokio::time::sleep(delay).await; + delay = (delay * 2).min(Duration::from_secs(60)); + } + } + } + }); +} + +/// Two probe attempts before rejecting a fresh tunnel: mixnet UDP does lose +/// the odd datagram, and one lost packet must not condemn a healthy exit. +async fn probe_fresh(tunnel: &Tunnel) -> bool { + for _ in 0..2 { + if super::dns::probe(tunnel).await { + return true; + } + } + false +} + +/// Keepalive period and the consecutive probe failures that declare death. +const KEEPALIVE_PERIOD: Duration = Duration::from_secs(60); +const KEEPALIVE_MAX_FAILS: u32 = 3; + +/// Probe the tunnel every [`KEEPALIVE_PERIOD`] (one tiny DNS round trip over +/// the mixnet); returns once [`KEEPALIVE_MAX_FAILS`] probes fail in a row. +async fn watch_tunnel(tunnel: &Tunnel) { + let mut fails = 0u32; + loop { + tokio::time::sleep(KEEPALIVE_PERIOD).await; + if super::dns::probe(tunnel).await { + fails = 0; + } else { + fails += 1; + warn!("nym: tunnel keepalive probe failed ({fails}/{KEEPALIVE_MAX_FAILS})"); + if fails >= KEEPALIVE_MAX_FAILS { + return; + } + } + } +} + +/// Build the tunnel with an auto-selected IPR exit. Ephemeral in-memory keys +/// (a fresh mixnet identity per run — no sqlite, no persisted gateway). +/// +/// NEVER pin an exit here in shipped code: pinning turns off auto-selection +/// and re-introduces the single-exit SPOF. `GP_NYM_IPR` exists for DEBUGGING +/// only and defaults to unset. +async fn build_tunnel() -> Result { + let mut builder = Tunnel::builder(); + if let Ok(pin) = std::env::var("GP_NYM_IPR") { + if !pin.is_empty() { + match pin.parse() { + Ok(recipient) => { + warn!("nym: GP_NYM_IPR set — pinning IPR exit (debug only, SPOF!)"); + builder = builder.ipr_address(recipient); + } + Err(e) => warn!("nym: ignoring invalid GP_NYM_IPR: {e}"), + } + } + } + builder.build().await +} diff --git a/crates/gp-nostr/src/nym/transport.rs b/crates/gp-nostr/src/nym/transport.rs new file mode 100644 index 0000000..a374fb0 --- /dev/null +++ b/crates/gp-nostr/src/nym/transport.rs @@ -0,0 +1,150 @@ +//! WebSocket transport for the Nostr relay pool routed through the +//! in-process smolmix tunnel (ported from `goblin/src/nym/transport.rs`), so +//! every relay connection traverses the 5-hop Nym mixnet. The relay host is +//! resolved through the tunnel (mix-dns — the destination is never resolved +//! on the clear), the TCP stream is opened via `tunnel.tcp_connect`, then the +//! TLS (rustls, webpki roots) + websocket handshake runs over that tunneled +//! stream. Nothing goes clearnet. + +use std::fmt; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use async_wsocket::futures_util::{Sink, SinkExt, StreamExt}; +use async_wsocket::{ConnectionMode, Message}; +use nostr_relay_pool::transport::error::TransportError; +use nostr_relay_pool::transport::websocket::{WebSocketSink, WebSocketStream, WebSocketTransport}; +use nostr_sdk::util::BoxedFuture; +use nostr_sdk::Url; +use tokio_tungstenite::tungstenite::Message as TgMessage; + +/// Error type for transport failures outside the websocket layer. +#[derive(Debug)] +struct NymTransportError(String); + +impl fmt::Display for NymTransportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for NymTransportError {} + +fn terr(msg: impl Into) -> TransportError { + TransportError::backend(NymTransportError(msg.into())) +} + +/// Nostr websocket transport over the in-process Nym mixnet tunnel. +#[derive(Debug, Clone, Copy, Default)] +pub struct NymWebSocketTransport; + +impl WebSocketTransport for NymWebSocketTransport { + fn support_ping(&self) -> bool { + true + } + + fn connect<'a>( + &'a self, + url: &'a Url, + _mode: &'a ConnectionMode, + timeout: Duration, + ) -> BoxedFuture<'a, Result<(WebSocketSink, WebSocketStream), TransportError>> { + Box::pin(async move { + let host = url + .host_str() + .ok_or_else(|| terr("relay url has no host"))? + .to_string(); + let port = url.port().unwrap_or(match url.scheme() { + "ws" => 80, + _ => 443, + }); + + // The shared mixnet tunnel (lazy-started at server boot). + let tunnel = super::nymproc::wait_for_tunnel(timeout) + .await + .ok_or_else(|| terr("nym tunnel not ready"))?; + + // Resolve the relay host through the mixnet (mix-dns), so no + // clearnet DNS leak, then dial through the same tunnel. + let addr = tokio::time::timeout(timeout, super::dns::resolve(&tunnel, &host, port)) + .await + .map_err(|_| terr("mix-dns resolve timeout"))? + .ok_or_else(|| terr(format!("mix-dns could not resolve relay host {host}")))?; + let stream = tokio::time::timeout(timeout, tunnel.tcp_connect(addr)) + .await + .map_err(|_| terr("nym tunnel connect timeout"))? + .map_err(|e| terr(format!("nym tunnel connect failed: {e}")))?; + + // Perform TLS (for wss) + websocket handshake over the mixnet + // stream (rustls webpki roots; the ring provider is installed + // once at gp-server startup — the Build 65/66 rule). + let (ws, _response) = tokio::time::timeout( + timeout, + tokio_tungstenite::client_async_tls(url.as_str(), stream), + ) + .await + .map_err(|_| terr("websocket handshake timeout"))? + .map_err(|e| terr(format!("websocket handshake failed: {e}")))?; + + let (tx, rx) = ws.split(); + + let sink: WebSocketSink = Box::new(NymSink(tx)) as WebSocketSink; + let stream: WebSocketStream = Box::pin(rx.filter_map(|msg| async move { + match msg { + Ok(tg) => tg_to_message(tg).map(Ok), + Err(e) => Some(Err(TransportError::backend(e))), + } + })) as WebSocketStream; + + Ok((sink, stream)) + }) + } +} + +/// Convert a tungstenite message into an async-wsocket pool message. +/// Returns `None` for raw frames (never surfaced while reading). +fn tg_to_message(msg: TgMessage) -> Option { + match msg { + TgMessage::Text(text) => Some(Message::Text(text.to_string())), + TgMessage::Binary(data) => Some(Message::Binary(data.to_vec())), + TgMessage::Ping(data) => Some(Message::Ping(data.to_vec())), + TgMessage::Pong(data) => Some(Message::Pong(data.to_vec())), + TgMessage::Close(_) => Some(Message::Close(None)), + TgMessage::Frame(_) => None, + } +} + +/// Sink adapter converting pool messages into tungstenite messages. +struct NymSink(S); + +impl Sink for NymSink +where + S: Sink + Send + Unpin, +{ + type Error = TransportError; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0) + .poll_ready_unpin(cx) + .map_err(TransportError::backend) + } + + fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + Pin::new(&mut self.0) + .start_send_unpin(TgMessage::from(item)) + .map_err(TransportError::backend) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0) + .poll_flush_unpin(cx) + .map_err(TransportError::backend) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0) + .poll_close_unpin(cx) + .map_err(TransportError::backend) + } +} diff --git a/crates/gp-nostr/src/protocol.rs b/crates/gp-nostr/src/protocol.rs new file mode 100644 index 0000000..318a4db --- /dev/null +++ b/crates/gp-nostr/src/protocol.rs @@ -0,0 +1,180 @@ +//! Goblin payment message protocol over NIP-17 (kind 14 rumors), ported from +//! `goblin/src/nostr/protocol.rs` minus the request/void control messages a +//! receive-only server never sends or honors. +//! +//! Content layout: a one-line human readable preamble, a blank line and the +//! raw slatepack armor. The per-payment note travels in the standard +//! `subject` tag; a `goblin` tag marks the protocol version. Classification +//! NEVER trusts tags — only the parsed slate (gp-wallet enforces S1). + +use nostr_sdk::{Tag, TagKind, Tags}; + +/// Maximum gift wrap content size accepted before unwrapping. +pub const MAX_WRAP_CONTENT: usize = 64 * 1024; +/// Maximum rumor content size accepted after unwrapping. +pub const MAX_RUMOR_CONTENT: usize = 32 * 1024; +/// Maximum slatepack armor size accepted. +pub const MAX_SLATEPACK: usize = 30 * 1024; +/// Maximum note length in characters after sanitization. +pub const MAX_NOTE_CHARS: usize = 256; +/// Protocol marker tag name. +pub const GOBLIN_TAG: &str = "goblin"; +/// Protocol version value. +pub const PROTOCOL_VERSION: &str = "1"; + +/// Human readable preamble other NIP-17 clients render. +pub const PREAMBLE: &str = + "[Goblin] GRIN payment message — open in Goblin (https://goblin.st) to process."; + +const ARMOR_BEGIN: &str = "BEGINSLATEPACK."; +const ARMOR_END: &str = "ENDSLATEPACK."; + +/// Sanitize a user note: strip control characters, collapse whitespace, +/// trim and cap the length. Returns `None` when nothing readable remains. +pub fn sanitize_note(raw: &str) -> Option { + let cleaned: String = raw + .chars() + .map(|c| if c.is_control() { ' ' } else { c }) + .collect(); + let collapsed = cleaned.split_whitespace().collect::>().join(" "); + let trimmed = collapsed.trim(); + if trimmed.is_empty() { + return None; + } + Some(trimmed.chars().take(MAX_NOTE_CHARS).collect()) +} + +/// Build the kind-14 rumor content for a slatepack payment message. +pub fn build_payment_content(slatepack: &str) -> String { + format!("{}\n\n{}", PREAMBLE, slatepack.trim()) +} + +/// Build rumor tags: protocol marker plus optional subject note. +pub fn build_rumor_tags(note: Option<&str>) -> Vec { + let mut tags = vec![Tag::custom( + TagKind::custom(GOBLIN_TAG), + [PROTOCOL_VERSION.to_string()], + )]; + if let Some(note) = note.and_then(sanitize_note) { + tags.push(Tag::custom(TagKind::custom("subject"), [note])); + } + tags +} + +/// Extract exactly one slatepack armor block from rumor content. +/// More than one block, none at all, or an oversized block returns `None`. +/// (Same semantics as Goblin's non-greedy `BEGINSLATEPACK. .. ENDSLATEPACK.` +/// regex, hand-rolled so this crate needs no regex dependency.) +pub fn extract_slatepack(content: &str) -> Option { + if content.len() > MAX_RUMOR_CONTENT { + return None; + } + let start = content.find(ARMOR_BEGIN)?; + let end_rel = content[start..].find(ARMOR_END)?; + let end = start + end_rel + ARMOR_END.len(); + // A second complete block after the first is ambiguous: refuse. + let rest = &content[end..]; + if let Some(next) = rest.find(ARMOR_BEGIN) { + if rest[next..].contains(ARMOR_END) { + return None; + } + } + let armor = content[start..end].trim().to_string(); + if armor.len() > MAX_SLATEPACK { + return None; + } + Some(armor) +} + +/// Read the sanitized subject (note) from rumor tags. +pub fn extract_subject(tags: &Tags) -> Option { + for tag in tags.iter() { + let parts = tag.as_slice(); + if parts.first().map(|s| s.as_str()) == Some("subject") { + if let Some(value) = parts.get(1) { + return sanitize_note(value); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + const PACK: &str = "BEGINSLATEPACK. 4H1qx1wHe668tFW yC2gfL8PPd8kSgv \ + pcXQhyRkHbyKHZg GN75o7uWoT3dkib R2tj1fFGN2FoRLY oeBPyKizupksgRT \ + dXFdjEuMUuktR5r gCiVBSXcHSWW3KW Y56LTQ9z3QwUWmE 8sRtwR9Bn8oNN5K \ + zYbR6XLkP8cSC7. ENDSLATEPACK."; + + #[test] + fn extracts_single_slatepack() { + let content = format!("{}\n\n{}", PREAMBLE, PACK); + let got = extract_slatepack(&content).unwrap(); + assert!(got.starts_with("BEGINSLATEPACK.")); + assert!(got.ends_with("ENDSLATEPACK.")); + } + + #[test] + fn rejects_no_slatepack() { + assert!(extract_slatepack("hi there, no payment here").is_none()); + assert!(extract_slatepack("").is_none()); + assert!(extract_slatepack("BEGINSLATEPACK. truncated junk").is_none()); + } + + #[test] + fn rejects_two_slatepacks() { + let content = format!("{} {}", PACK, PACK); + assert!(extract_slatepack(&content).is_none()); + // But trailing garbage with only a BEGIN marker is not a second block. + let content = format!("{} BEGINSLATEPACK. trailing junk", PACK); + assert!(extract_slatepack(&content).is_some()); + } + + #[test] + fn rejects_oversize() { + let huge = format!( + "BEGINSLATEPACK. {} ENDSLATEPACK.", + "A".repeat(MAX_SLATEPACK + 1) + ); + assert!(extract_slatepack(&huge).is_none()); + let oversize_content = "x".repeat(MAX_RUMOR_CONTENT + 1); + assert!(extract_slatepack(&oversize_content).is_none()); + } + + #[test] + fn sanitizes_notes() { + assert_eq!(sanitize_note(" lunch :) "), Some("lunch :)".to_string())); + assert_eq!( + sanitize_note("a\u{0000}b\u{001b}[31mc"), + Some("a b [31mc".to_string()) + ); + assert_eq!( + sanitize_note("multi space\n\nnewline"), + Some("multi space newline".to_string()) + ); + assert_eq!(sanitize_note("\u{0007}\u{0008}"), None); + assert_eq!(sanitize_note(""), None); + let long = "y".repeat(MAX_NOTE_CHARS + 50); + assert_eq!( + sanitize_note(&long).unwrap().chars().count(), + MAX_NOTE_CHARS + ); + } + + #[test] + fn builds_content_with_preamble() { + let c = build_payment_content(PACK); + assert!(c.starts_with(PREAMBLE)); + assert!(extract_slatepack(&c).is_some()); + } + + #[test] + fn subject_round_trips_through_tags() { + let tags = Tags::from_list(build_rumor_tags(Some(" order #42 "))); + assert_eq!(extract_subject(&tags), Some("order #42".to_string())); + let no_note = Tags::from_list(build_rumor_tags(None)); + assert_eq!(extract_subject(&no_note), None); + } +} diff --git a/crates/gp-nostr/src/receipt.rs b/crates/gp-nostr/src/receipt.rs new file mode 100644 index 0000000..e6f4ca3 --- /dev/null +++ b/crates/gp-nostr/src/receipt.rs @@ -0,0 +1,254 @@ +//! Server-signed payment receipt: the DM-less trust primitive. +//! +//! A [`Receipt`] bundles what a store needs to trust a payment without relying +//! on a webhook payload or a DM: the payment id, amount, the on-chain kernel +//! excess, the confirmation height, and (when present) the receiver-side Grin +//! payment proof. [`sign_receipt`] signs it with the server's Nostr identity +//! key (BIP-340 Schnorr over SHA-256 of the canonical JSON, the same signature +//! scheme Nostr events use), producing a [`SignedReceipt`] any party can verify +//! against the server's known public key with [`verify_receipt`]. +//! +//! The receipt is a plain serde object, independent of Nostr event framing, so +//! a store backend (Eranos, WooCommerce, ...) can verify it with any BIP-340 +//! implementation. It is safe to expose publicly: it reveals only what the +//! payer already told the merchant, and it is self-authenticating. + +use nostr_sdk::Keys; +use secp256k1::hashes::{sha256, Hash}; +use secp256k1::schnorr::Signature; +use secp256k1::{Keypair, SecretKey, XOnlyPublicKey, SECP256K1}; +use serde::{Deserialize, Serialize}; + +/// Current receipt schema version. +pub const RECEIPT_VERSION: u8 = 1; + +/// The signed payload. Field order is fixed (serde serializes structs in +/// declaration order with no whitespace), so signer and verifier hash exactly +/// the same bytes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Receipt { + /// Schema version (`1`). + pub version: u8, + /// Payment identifier (the Grin slate UUID; also the public status token). + pub payment_id: String, + /// Amount in nanogrin. + pub amount: u64, + /// Tx kernel excess commitment, hex — the on-chain anchor. + pub kernel_excess: String, + /// Block height the kernel confirmed at, if confirmed. + pub confirmed_height: Option, + /// Confirmation depth at issue time, if confirmed. + pub confirmations: Option, + /// The receiver-side Grin payment proof (as its own JSON object), when the + /// payer requested one. A store can verify this independently. + pub proof: Option, + /// Issue time, ISO-8601 UTC. + pub issued_at: String, + /// The server identity npub-hex (x-only) this receipt is about. Bound into + /// the signature so a receipt cannot be replayed under another identity. + pub server_pubkey: String, +} + +/// A [`Receipt`] plus the server's BIP-340 Schnorr signature over it. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignedReceipt { + /// The signed payload. + pub receipt: Receipt, + /// Signature, hex (64 bytes). + pub sig: String, +} + +/// Receipt signing/verification errors. +#[derive(Debug)] +pub enum ReceiptError { + /// The identity secret key could not be used for signing. + Key(String), + /// Canonical serialization failed. + Serialize(String), +} + +impl std::fmt::Display for ReceiptError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReceiptError::Key(m) => write!(f, "receipt key error: {m}"), + ReceiptError::Serialize(m) => write!(f, "receipt serialize error: {m}"), + } + } +} + +impl std::error::Error for ReceiptError {} + +/// Sign a receipt with the server identity keys. The receipt's `server_pubkey` +/// is overwritten with the signer's x-only key so the object is internally +/// consistent regardless of what the caller passed. +pub fn sign_receipt(keys: &Keys, mut receipt: Receipt) -> Result { + let sk = SecretKey::from_byte_array(keys.secret_key().to_secret_bytes()) + .map_err(|e| ReceiptError::Key(format!("bad identity secret key: {e}")))?; + let keypair = Keypair::from_secret_key(SECP256K1, &sk); + let (xonly, _parity) = keypair.x_only_public_key(); + receipt.server_pubkey = encode_hex(&xonly.serialize()); + + let digest = receipt_digest(&receipt)?; + let sig = SECP256K1.sign_schnorr_no_aux_rand(&digest, &keypair); + Ok(SignedReceipt { + receipt, + sig: encode_hex(&sig.to_byte_array()), + }) +} + +/// Verify a signed receipt's signature against the public key embedded in the +/// receipt (`receipt.server_pubkey`). Returns `false` on any malformed field +/// or signature mismatch. Callers must still check the embedded key is the +/// server they trust (or use [`verify_receipt_from`]). +pub fn verify_receipt(signed: &SignedReceipt) -> bool { + let Ok(digest) = receipt_digest(&signed.receipt) else { + return false; + }; + let Some(pk_bytes) = decode_fixed::<32>(&signed.receipt.server_pubkey) else { + return false; + }; + let Some(sig_bytes) = decode_fixed::<64>(&signed.sig) else { + return false; + }; + let Ok(pubkey) = XOnlyPublicKey::from_byte_array(pk_bytes) else { + return false; + }; + let signature = Signature::from_byte_array(sig_bytes); + SECP256K1 + .verify_schnorr(&signature, &digest, &pubkey) + .is_ok() +} + +/// Verify a signed receipt AND that it was signed by `expected_pubkey_hex` +/// (the x-only server identity a store trusts out of band). +pub fn verify_receipt_from(signed: &SignedReceipt, expected_pubkey_hex: &str) -> bool { + signed + .receipt + .server_pubkey + .eq_ignore_ascii_case(expected_pubkey_hex.trim()) + && verify_receipt(signed) +} + +/// SHA-256 of the canonical receipt JSON (the signed message). +fn receipt_digest(receipt: &Receipt) -> Result<[u8; 32], ReceiptError> { + let bytes = serde_json::to_vec(receipt).map_err(|e| ReceiptError::Serialize(e.to_string()))?; + Ok(sha256::Hash::hash(&bytes).to_byte_array()) +} + +fn encode_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + s.push_str(&format!("{b:02x}")); + } + s +} + +fn decode_fixed(hex: &str) -> Option<[u8; N]> { + let hex = hex.trim(); + if hex.len() != N * 2 { + return None; + } + let mut out = [0u8; N]; + for (i, byte) in out.iter_mut().enumerate() { + *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?; + } + Some(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample() -> Receipt { + Receipt { + version: RECEIPT_VERSION, + payment_id: "b6f7c2a0-1234-5678-9abc-def012345678".into(), + amount: 2_500_000_000, + kernel_excess: "09".repeat(33), + confirmed_height: Some(3_900_000), + confirmations: Some(11), + proof: Some(serde_json::json!({ + "amount": 2_500_000_000u64, + "kernel_excess": "09".repeat(33), + })), + issued_at: "2026-07-01T12:00:00Z".into(), + server_pubkey: String::new(), + } + } + + #[test] + fn sign_then_verify_round_trips() { + let keys = Keys::generate(); + let signed = sign_receipt(&keys, sample()).unwrap(); + assert!(verify_receipt(&signed)); + // The embedded pubkey is the signer's x-only key. + assert_eq!(signed.receipt.server_pubkey, keys.public_key().to_hex()); + } + + #[test] + fn verify_from_matches_expected_key_only() { + let keys = Keys::generate(); + let signed = sign_receipt(&keys, sample()).unwrap(); + assert!(verify_receipt_from(&signed, &keys.public_key().to_hex())); + // A different expected key is rejected even though the sig is valid. + let other = Keys::generate(); + assert!(!verify_receipt_from(&signed, &other.public_key().to_hex())); + } + + #[test] + fn tampering_any_field_breaks_verification() { + let keys = Keys::generate(); + let signed = sign_receipt(&keys, sample()).unwrap(); + + let mut t = signed.clone(); + t.receipt.amount += 1; + assert!(!verify_receipt(&t), "amount tamper must fail"); + + let mut t = signed.clone(); + t.receipt.confirmed_height = Some(999_999); + assert!(!verify_receipt(&t), "height tamper must fail"); + + let mut t = signed.clone(); + t.receipt.kernel_excess = "0a".repeat(33); + assert!(!verify_receipt(&t), "excess tamper must fail"); + + let mut t = signed.clone(); + t.receipt.proof = Some(serde_json::json!({"amount": 1})); + assert!(!verify_receipt(&t), "proof tamper must fail"); + + let mut t = signed.clone(); + t.receipt.payment_id = "other".into(); + assert!(!verify_receipt(&t), "id tamper must fail"); + } + + #[test] + fn signature_from_another_key_is_rejected() { + let keys = Keys::generate(); + let mut signed = sign_receipt(&keys, sample()).unwrap(); + // Replace the embedded pubkey with a stranger's: the signature no + // longer matches the (now different) advertised signer. + let other = Keys::generate(); + signed.receipt.server_pubkey = other.public_key().to_hex(); + assert!(!verify_receipt(&signed)); + } + + #[test] + fn malformed_fields_do_not_panic() { + let keys = Keys::generate(); + let mut signed = sign_receipt(&keys, sample()).unwrap(); + signed.sig = "zz".into(); + assert!(!verify_receipt(&signed)); + signed.sig = String::new(); + assert!(!verify_receipt(&signed)); + } + + #[test] + fn json_round_trips() { + let keys = Keys::generate(); + let signed = sign_receipt(&keys, sample()).unwrap(); + let json = serde_json::to_string(&signed).unwrap(); + let back: SignedReceipt = serde_json::from_str(&json).unwrap(); + assert_eq!(signed, back); + assert!(verify_receipt(&back)); + } +} diff --git a/crates/gp-nostr/src/relays.rs b/crates/gp-nostr/src/relays.rs new file mode 100644 index 0000000..aaf8e03 --- /dev/null +++ b/crates/gp-nostr/src/relays.rs @@ -0,0 +1,35 @@ +//! Default relay set and helpers (mirrors `goblin/src/nostr/relays.rs`). + +/// Default DM relays: the Goblin relay plus large public relays for +/// redundancy. Used when `GP_RELAYS` is unset (the bundled relay is a later +/// milestone; until then `bundled` mode serves this set too). +pub const DEFAULT_RELAYS: &[&str] = &[ + "wss://relay.goblin.st", + "wss://relay.damus.io", + "wss://nos.lol", +]; + +/// Maximum relays published in the kind 10050 DM relay list (NIP-17 +/// guidance) and read from a payer's list. +pub const MAX_DM_RELAYS: usize = 3; + +/// The relay set to run with: the configured external list, else defaults. +pub fn resolve(configured: &[String]) -> Vec { + if configured.is_empty() { + DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect() + } else { + configured.to_vec() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolves_defaults_and_overrides() { + assert_eq!(resolve(&[]), DEFAULT_RELAYS.to_vec()); + let own = vec!["wss://relay.example".to_string()]; + assert_eq!(resolve(&own), own); + } +} diff --git a/crates/gp-nostr/src/service.rs b/crates/gp-nostr/src/service.rs new file mode 100644 index 0000000..4918159 --- /dev/null +++ b/crates/gp-nostr/src/service.rs @@ -0,0 +1,491 @@ +//! The daemon service loop, adapted from `goblin/src/nostr/client.rs` +//! (`run_service`): connect the relay pool over the in-process Nym mixnet, +//! publish the kind 10050 inbox (with the NIP-17 `encryption` capability +//! tag) and its kind 10002 mirror, catch up on missed gift wraps, subscribe +//! live, and for every received payment dispatch the S2 reply to the payer's +//! advertised relays (their 10050; our own set as the fallback), encrypted +//! with the best mutual NIP-44 version. +//! +//! No UI, no contacts, no relay-pool gist (G10 is pending): the relay set is +//! configuration plus defaults. + +use std::sync::Arc; +use std::time::Duration; + +use log::{error, info, warn}; +use nostr_sdk::{ + Client, Event, EventBuilder, Filter, Keys, Kind, PublicKey, RelayPoolNotification, RelayUrl, + Tag, TagKind, Timestamp, +}; + +use crate::ingest::{Ingest, IngestOutcome, PendingReply}; +use crate::nym::NymWebSocketTransport; +use crate::relays::MAX_DM_RELAYS; +use crate::unix_time; +use crate::wrap; +use crate::{KeyDirectory, MasterDirectory, SlatepackReceiver}; + +/// Subscription look-back window: gift wrap timestamps are randomized up to +/// 2 days into the past (NIP-59), use 3 (Goblin's constant). Cross-restart +/// dedupe is the wallet's already-received guard plus the payment table. +const LOOKBACK_SECS: i64 = 3 * 86_400; +/// Catch-up fetch timeout. +const FETCH_TIMEOUT: Duration = Duration::from_secs(30); +/// Send dispatch timeout. +const SEND_TIMEOUT: Duration = Duration::from_secs(40); +/// How long to wait for the mixnet tunnel before dialing relays anyway. +const NYM_WARM_WAIT: Duration = Duration::from_secs(30); + +/// Service configuration (already resolved from the environment). +#[derive(Debug, Clone)] +pub struct ServiceOptions { + /// Relay set to listen on and publish to. + pub relays: Vec, + /// Route everything over the Nym mixnet (default on; clearnet is a + /// debugging escape hatch only). + pub nym: bool, + /// Optional NIP-17 payment DMs (milestone 6, all off by default). + pub notify: NotifyOptions, +} + +/// Optional payment-notification DMs (milestone 6). Both are off by default. +#[derive(Debug, Clone, Default)] +pub struct NotifyOptions { + /// Merchant public key for the confirmed-payment DM. + pub merchant: Option, + /// Send the merchant a NIP-17 DM on a received payment. + pub merchant_dm: bool, + /// Send the payer a NIP-17 receipt DM. + pub payer_receipt: bool, +} + +/// Merchant DM text for a received payment. +pub fn merchant_dm_text(amount: u64, slate_id: &str) -> String { + format!( + "[GoblinPay] Received {} GRIN (slate {}).", + gp_core::webhook::nanogrin_to_grin(amount), + slate_id + ) +} + +/// Payer receipt DM text. +pub fn payer_receipt_text(amount: u64) -> String { + format!( + "[GoblinPay] Payment of {} GRIN received. Thank you.", + gp_core::webhook::nanogrin_to_grin(amount) + ) +} + +/// Start the ingest service on its own thread with its own tokio runtime +/// (mirrors Goblin's service thread; keeps relay I/O off the HTTP runtime). +/// Watches the master identity only. +pub fn spawn(keys: Keys, opts: ServiceOptions, receiver: R) -> std::thread::JoinHandle<()> +where + R: SlatepackReceiver + 'static, +{ + let directory: Arc = Arc::new(MasterDirectory(keys.clone())); + spawn_with_directory(keys, opts, receiver, directory) +} + +/// Like [`spawn`] but with a multi-identity directory (master + per-invoice and +/// per-user derived children), so payments to any watched endpub are received +/// and replied from the right identity. +pub fn spawn_with_directory( + keys: Keys, + opts: ServiceOptions, + receiver: R, + directory: Arc, +) -> std::thread::JoinHandle<()> +where + R: SlatepackReceiver + 'static, +{ + std::thread::Builder::new() + .name("gp-nostr".into()) + .spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .expect("build gp-nostr runtime"); + rt.block_on(run(keys, opts, receiver, directory)); + }) + .expect("spawn gp-nostr thread") +} + +/// The service loop. Runs until the process exits (a payment server has no +/// reason to stop listening). +pub async fn run( + keys: Keys, + opts: ServiceOptions, + receiver: R, + directory: Arc, +) { + let client = if opts.nym { + // Wait for the in-process Nym mixnet tunnel before any network work: + // dialing before it is up drops every relay into the pool's + // backing-off reconnect (Goblin's wallet-open ordering lesson). + crate::nym::warm_up(); + let waited = std::time::Instant::now(); + while !crate::nym::is_ready() && waited.elapsed() < NYM_WARM_WAIT { + tokio::time::sleep(Duration::from_millis(500)).await; + } + if crate::nym::is_ready() { + info!( + "nostr: Nym tunnel ready after ~{}ms", + waited.elapsed().as_millis() + ); + } else { + warn!("nostr: Nym tunnel still warming; relays will retry through it"); + } + Client::builder() + .websocket_transport(NymWebSocketTransport) + .build() + } else { + warn!("nostr: GP_NYM=off — relay traffic goes CLEARNET (debugging only)"); + Client::builder().build() + }; + + let ingest = Ingest::with_directory(keys.clone(), receiver, directory); + let npub_prefix: String = keys.public_key().to_hex().chars().take(8).collect(); + info!( + "nostr: starting service for {npub_prefix}… with {} relay(s)", + opts.relays.len() + ); + for relay in &opts.relays { + if let Err(e) = client.add_relay(relay.clone()).await { + warn!("nostr: add relay failed: {e}"); + } + } + client.connect().await; + + // Publish the replaceable identity events: kind 10050 DM relays with the + // encryption capability tag, plus the kind 10002 (NIP-65) mirror. No + // kind 0 — the till is anonymous by design. + publish_inbox(&client, &keys, &opts.relays).await; + + // Re-dispatch stored replies that never verifiably left (crash between + // receive_tx and the reply send) before processing anything new. + reconcile(&client, &ingest, &opts.relays).await; + + // Catch-up + live subscription for gift wraps addressed to any identity we + // watch: the master, plus per-invoice (matching mode 2) and per-user (5b) + // derived children the directory currently holds. Targeted at our OWN + // advertised set only (a pool-wide subscription would leak the listener + // filter to relays added later for reply fan-out). The watched set is + // snapshotted here; rotation refreshes it on the next service restart or + // re-subscribe (a live refresh tick is the multi-tenant follow-up). + let since = (unix_time() - LOOKBACK_SECS).max(0) as u64; + let filter = Filter::new() + .kind(Kind::GiftWrap) + .pubkeys(ingest.watched()) + .since(Timestamp::from_secs(since)); + match client + .fetch_events_from(&opts.relays, filter.clone(), FETCH_TIMEOUT) + .await + { + Ok(events) => { + info!("nostr: catch-up fetched {} wrap(s)", events.len()); + for event in events.into_iter() { + handle(&client, &ingest, &keys, &opts.notify, &event, &opts.relays).await; + } + } + Err(e) => warn!("nostr: catch-up fetch failed: {e}"), + } + if let Err(e) = client.subscribe_to(&opts.relays, filter, None).await { + error!("nostr: subscribe failed: {e}"); + } + + let mut notifications = client.notifications(); + loop { + match notifications.recv().await { + Ok(RelayPoolNotification::Event { event, .. }) => { + handle(&client, &ingest, &keys, &opts.notify, &event, &opts.relays).await; + } + Ok(_) => {} + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + warn!("nostr: notifications lagged by {n}"); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + } + } + error!("nostr: notification stream closed; service stopped"); +} + +/// Handle one incoming event end to end: ingest, dispatch the reply, then +/// (if configured) send the optional merchant / payer NIP-17 DMs. +async fn handle( + client: &Client, + ingest: &Ingest, + keys: &Keys, + notify: &NotifyOptions, + event: &Event, + own_relays: &[String], +) { + match ingest.handle_wrap(event).await { + IngestOutcome::Received { + slate_id, + amount, + reply, + } => { + // Optional notifications (M6): merchant DM from the server identity, + // payer receipt from the identity that received. Best effort; a + // failed DM never affects the money or the reply. + if notify.merchant_dm { + if let Some(merchant) = ¬ify.merchant { + send_dm( + client, + keys, + merchant, + merchant_dm_text(amount, &slate_id), + own_relays, + ) + .await; + } + } + if notify.payer_receipt { + send_dm( + client, + &reply.from, + &reply.payer, + payer_receipt_text(amount), + own_relays, + ) + .await; + } + if deliver_reply(client, &reply, own_relays).await { + ingest.receiver().mark_replied(&slate_id).await; + } else { + // Left in status 'received': the boot-time reconcile (or a + // restart) re-sends it. The payment itself is safe in the + // wallet either way. + warn!("nostr: S2 reply dispatch failed for slate {slate_id}, will reconcile"); + } + } + IngestOutcome::Dropped(reason) => { + info!("nostr: dropped wrap {}…: {reason}", &event.id.to_hex()[..8]); + } + IngestOutcome::RateLimited => {} + IngestOutcome::Failed(e) => { + error!("nostr: receive failed (will retry on catch-up): {e}"); + } + } +} + +/// Gift wrap and publish one S2 reply, FROM the identity that received the +/// payment (master or the derived child the payer addressed). Targets the +/// payer's advertised 10050 relays when discoverable, else our own set +/// (Goblin's send-target fallback); the encryption version is the best mutual +/// method from the same 10050 (absent = v2). Returns true when a relay +/// accepted the event. +async fn deliver_reply(client: &Client, reply: &PendingReply, own_relays: &[String]) -> bool { + let (mut targets, encryption) = recipient_hints(client, &reply.payer, own_relays).await; + if targets.is_empty() { + // NIP-17 pragmatic fallback: the wrap reached us through a shared + // relay, so our own set is the best remaining route. + targets = own_relays.to_vec(); + } + let version = wrap::choose_version(encryption.as_deref()); + let event = match wrap::gift_wrap(&reply.from, &reply.payer, reply.rumor.clone(), version) { + Ok(event) => event, + Err(e) => { + error!("nostr: reply wrap failed: {e}"); + return false; + } + }; + // Dial any target relays we don't already hold (the payer's relays may + // differ from ours), then publish to exactly that set. + connect_relays(client, &targets).await; + match tokio::time::timeout(SEND_TIMEOUT, client.send_event_to(&targets, &event)).await { + Ok(Ok(output)) => { + info!( + "nostr: S2 reply {}… published ({:?}, {} relay(s) ok)", + &output.val.to_hex()[..8], + version, + output.success.len() + ); + !output.success.is_empty() + } + Ok(Err(e)) => { + warn!("nostr: reply publish failed: {e}"); + false + } + Err(_) => { + warn!("nostr: reply publish timed out"); + false + } + } +} + +/// Send a plain NIP-17 DM `from` an identity `to` a recipient (the optional +/// M6 merchant/payer notifications). Version is negotiated from the +/// recipient's 10050 like a reply; best effort, errors are logged only. +async fn send_dm( + client: &Client, + from: &Keys, + to: &PublicKey, + content: String, + own_relays: &[String], +) { + let rumor = EventBuilder::new(Kind::PrivateDirectMessage, content) + .tags([Tag::public_key(*to)]) + .build(from.public_key()); + let (mut targets, encryption) = recipient_hints(client, to, own_relays).await; + if targets.is_empty() { + targets = own_relays.to_vec(); + } + let version = wrap::choose_version(encryption.as_deref()); + let event = match wrap::gift_wrap(from, to, rumor, version) { + Ok(event) => event, + Err(e) => { + warn!("nostr: notify DM wrap failed: {e}"); + return; + } + }; + connect_relays(client, &targets).await; + match tokio::time::timeout(SEND_TIMEOUT, client.send_event_to(&targets, &event)).await { + Ok(Ok(_)) => info!("nostr: notify DM sent to {}…", &to.to_hex()[..8]), + Ok(Err(e)) => warn!("nostr: notify DM send failed: {e}"), + Err(_) => warn!("nostr: notify DM send timed out"), + } +} + +/// Fetch the payer's kind 10050: their advertised DM relays (capped) and the +/// `encryption` capability tag. Queried from our own relay set — most Goblin +/// peers share the Goblin relay; the discovery-indexer fan-out arrives with +/// the G10 relay-strategy work. +async fn recipient_hints( + client: &Client, + payer: &PublicKey, + own_relays: &[String], +) -> (Vec, Option) { + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(*payer) + .limit(1); + let events = match client + .fetch_events_from(own_relays, filter, FETCH_TIMEOUT) + .await + { + Ok(events) => events, + Err(e) => { + warn!("nostr: 10050 lookup failed: {e}"); + return (vec![], None); + } + }; + let Some(event) = events.first() else { + return (vec![], None); + }; + let mut relays = vec![]; + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.first().map(|s| s.as_str()) == Some("relay") { + if let Some(url) = parts.get(1) { + if relays.len() < MAX_DM_RELAYS { + relays.push(url.trim_end_matches('/').to_string()); + } + } + } + } + (relays, wrap::encryption_capability(event)) +} + +/// Publish the kind 10050 inbox (relay tags + encryption capability) and the +/// kind 10002 mirror, signed once, to the advertised set. +async fn publish_inbox(client: &Client, keys: &Keys, relays: &[String]) { + let advertised: Vec = relays.iter().take(MAX_DM_RELAYS).cloned().collect(); + let mut dm_tags: Vec = advertised + .iter() + .map(|r| Tag::custom(TagKind::custom("relay"), [r.clone()])) + .collect(); + // The NIP-17 extension: ["encryption", "nip44_v3 nip44_v2"], best first. + dm_tags.push(wrap::capability_tag()); + + let builders = vec![ + EventBuilder::new(Kind::InboxRelays, "").tags(dm_tags), + // The NIP-65 list mirrors the same set, unmarked (read + write). + EventBuilder::relay_list( + advertised + .iter() + .filter_map(|r| RelayUrl::parse(r).ok()) + .map(|u| (u, None)), + ), + ]; + for builder in builders { + match builder.sign_with_keys(keys) { + Ok(event) => { + if let Err(e) = client.send_event_to(&advertised, &event).await { + warn!("nostr: publish kind {} failed: {e}", event.kind); + } + } + Err(e) => warn!("nostr: identity event signing failed: {e}"), + } + } +} + +/// Re-dispatch stored S2 replies that never verifiably left (Goblin's +/// reconcile, narrowed to the one message type a till sends). +async fn reconcile( + client: &Client, + ingest: &Ingest, + own_relays: &[String], +) { + for pending in ingest.receiver().unreplied().await { + let Ok(payer) = PublicKey::from_hex(&pending.payer_hex) else { + warn!( + "nostr: reconcile skipped slate {} (bad payer key)", + pending.slate_id + ); + continue; + }; + // Rebuild the identity that received it, so the re-dispatched reply is + // signed by the same key (master or the derived child) the payer paid. + let Some(from) = ingest.resolve(&pending.recipient_hex) else { + warn!( + "nostr: reconcile skipped slate {} (unwatched recipient)", + pending.slate_id + ); + continue; + }; + info!( + "nostr: reconcile re-dispatch S2 for slate {}", + pending.slate_id + ); + let reply = ingest.build_reply(from, payer, &pending.s2_armor); + if deliver_reply(client, &reply, own_relays).await { + ingest.receiver().mark_replied(&pending.slate_id).await; + } + } +} + +/// Add + dial every relay in `urls` so a targeted send reaches relays we +/// don't already hold (Goblin's `connect_relays`: idempotent add, short +/// bounded dial, concurrent so one dead relay doesn't stall the rest). +async fn connect_relays(client: &Client, urls: &[String]) { + let dials = urls.iter().map(|url| { + let url = url.clone(); + async move { + let _ = client.add_relay(&url).await; + // Short cap: a reachable relay connects in ~2-4s over the mixnet; + // one dead relay in the list must not stall the whole send. + let _ = client.try_connect_relay(&url, Duration::from_secs(6)).await; + } + }); + async_wsocket::futures_util::future::join_all(dials).await; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn notification_dm_text() { + assert_eq!( + merchant_dm_text(2_500_000_000, "slate-1"), + "[GoblinPay] Received 2.5 GRIN (slate slate-1)." + ); + assert_eq!( + payer_receipt_text(1_000_000_000), + "[GoblinPay] Payment of 1 GRIN received. Thank you." + ); + } +} diff --git a/crates/gp-nostr/src/wrap.rs b/crates/gp-nostr/src/wrap.rs new file mode 100644 index 0000000..1f0f969 --- /dev/null +++ b/crates/gp-nostr/src/wrap.rs @@ -0,0 +1,399 @@ +//! NIP-59 gift wrap build/unwrap with the NIP-17 backward-compat extension +//! (NIP-44 v3 context binding), all in one place. +//! +//! - **v2** rides nostr-sdk exactly as Goblin ships today: the seal content +//! is `nip44::encrypt(.., Version::V2)` and the outer wrap goes through +//! nostr-sdk's own `EventBuilder::gift_wrap_from_seal` (which hardcodes +//! v2 — the reason v3 needs the manual path below). +//! - **v3** is built manually against the companion `nip44` crate: the seal +//! (kind 13) encrypts the rumor JSON with context `kind=13, scope=""`, the +//! wrap (kind 1059) encrypts the seal JSON with `kind=1059, scope=""`, per +//! the extension spec. Everything else mirrors what nostr-sdk does for v2: +//! `rumor.ensure_id()`, seal signed by the sender with NO tags, wrap signed +//! by a fresh ephemeral key with the receiver `p` tag, and `created_at` +//! fuzzed up to two days into the past on both. +//! - **Decrypt** dispatches per layer on the payload version byte +//! (`0x02`/`0x03`), so mixed peers interoperate and a v2-only Goblin can +//! always read us. +//! +//! Negotiation: we advertise `["encryption", "nip44_v3 nip44_v2"]` on our +//! kind 10050; on send we take the FIRST method of the recipient's +//! (best-first) list that we support; no tag means v2 only. + +use std::fmt; + +use nostr_sdk::nips::nip44 as sdk_nip44; +use nostr_sdk::nips::nip59::RANGE_RANDOM_TIMESTAMP_TWEAK; +use nostr_sdk::{ + Event, EventBuilder, JsonUtil, Keys, Kind, PublicKey, Tag, Timestamp, UnsignedEvent, +}; + +/// Tag name on kind 10050 advertising supported encryption methods. +pub const ENCRYPTION_TAG: &str = "encryption"; +/// Our capabilities, space separated, best first. +pub const ENCRYPTION_CAPABILITIES: &str = "nip44_v3 nip44_v2"; + +/// v3 context values fixed by the NIP-17 extension: seals bind `kind=13`, +/// gift wraps bind `kind=1059`, scope is empty for both. +const SEAL_KIND: u32 = 13; +const WRAP_KIND: u32 = 1059; +const EMPTY_SCOPE: &[u8] = b""; + +/// Which NIP-44 version to encrypt a seal + wrap with. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WrapVersion { + V2, + V3, +} + +/// Errors from wrapping or unwrapping. +#[derive(Debug)] +pub enum WrapError { + /// The outer event is not a kind 1059 gift wrap. + NotGiftWrap, + /// The decrypted inner event is not a kind 13 seal. + NotSeal, + /// The rumor author does not match the seal signer (NIP-17 requirement). + SenderMismatch, + /// Encryption/decryption failure (wrong key, bad MAC, bad context, ...). + Crypto(String), + /// Event build/parse/signature failure. + Event(String), +} + +impl fmt::Display for WrapError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WrapError::NotGiftWrap => write!(f, "not a gift wrap event"), + WrapError::NotSeal => write!(f, "inner event is not a seal"), + WrapError::SenderMismatch => write!(f, "rumor author differs from seal signer"), + WrapError::Crypto(m) => write!(f, "wrap crypto error: {m}"), + WrapError::Event(m) => write!(f, "wrap event error: {m}"), + } + } +} + +impl std::error::Error for WrapError {} + +/// An unwrapped gift: the seal-verified sender and the rumor. Mirrors +/// nostr-sdk's `UnwrappedGift`, produced by the version-dispatching path. +#[derive(Debug, Clone)] +pub struct Unwrapped { + /// The seal signer (verified signature) — the authenticated sender. + pub sender: PublicKey, + /// The unsigned rumor. + pub rumor: UnsignedEvent, +} + +/// Pick the encryption version for a recipient from their kind 10050 +/// `encryption` tag value (space separated, best first). The best mutual +/// method wins in THEIR preference order; an absent tag, or a tag with no +/// mutual method, means the mandatory v2 baseline. +pub fn choose_version(recipient_encryption: Option<&str>) -> WrapVersion { + if let Some(tag) = recipient_encryption { + for method in tag.split_whitespace() { + match method { + "nip44_v3" => return WrapVersion::V3, + "nip44_v2" => return WrapVersion::V2, + _ => {} + } + } + } + WrapVersion::V2 +} + +/// Read the `encryption` tag value from a kind 10050 event, if present. +pub fn encryption_capability(event: &Event) -> Option { + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.first().map(|s| s.as_str()) == Some(ENCRYPTION_TAG) { + return parts.get(1).cloned(); + } + } + None +} + +/// The `encryption` tag we publish on our own kind 10050. +pub fn capability_tag() -> Tag { + Tag::custom( + nostr_sdk::TagKind::custom(ENCRYPTION_TAG), + [ENCRYPTION_CAPABILITIES.to_string()], + ) +} + +/// Gift wrap `rumor` from `sender` to `receiver` with the given version. +pub fn gift_wrap( + sender: &Keys, + receiver: &PublicKey, + mut rumor: UnsignedEvent, + version: WrapVersion, +) -> Result { + // Fix the rumor id BEFORE encrypting, exactly like nostr-sdk's + // `make_seal`, so both peers agree on the rumor identity. + rumor.ensure_id(); + match version { + WrapVersion::V2 => { + let content = sdk_nip44::encrypt( + sender.secret_key(), + receiver, + rumor.as_json(), + sdk_nip44::Version::V2, + ) + .map_err(|e| WrapError::Crypto(e.to_string()))?; + let seal = EventBuilder::new(Kind::Seal, content) + .custom_created_at(Timestamp::tweaked(RANGE_RANDOM_TIMESTAMP_TWEAK)) + .sign_with_keys(sender) + .map_err(|e| WrapError::Event(e.to_string()))?; + // The proven nostr-sdk outer wrap (ephemeral key, `p` tag, + // created_at fuzz — and v2 encryption, which is what we want here). + EventBuilder::gift_wrap_from_seal(receiver, &seal, []) + .map_err(|e| WrapError::Event(e.to_string())) + } + WrapVersion::V3 => { + let ck = v3_conversation_key(sender, receiver)?; + let content = + nip44::encrypt_v3(&ck, rumor.as_json().as_bytes(), SEAL_KIND, EMPTY_SCOPE) + .map_err(|e| WrapError::Crypto(e.to_string()))?; + let seal = EventBuilder::new(Kind::Seal, content) + .custom_created_at(Timestamp::tweaked(RANGE_RANDOM_TIMESTAMP_TWEAK)) + .sign_with_keys(sender) + .map_err(|e| WrapError::Event(e.to_string()))?; + + let ephemeral = Keys::generate(); + let wck = v3_conversation_key(&ephemeral, receiver)?; + let wrapped = + nip44::encrypt_v3(&wck, seal.as_json().as_bytes(), WRAP_KIND, EMPTY_SCOPE) + .map_err(|e| WrapError::Crypto(e.to_string()))?; + EventBuilder::new(Kind::GiftWrap, wrapped) + .tags([Tag::public_key(*receiver)]) + .custom_created_at(Timestamp::tweaked(RANGE_RANDOM_TIMESTAMP_TWEAK)) + .sign_with_keys(&ephemeral) + .map_err(|e| WrapError::Event(e.to_string())) + } + } +} + +/// Unwrap a gift wrap addressed to `receiver`, dispatching each layer on its +/// NIP-44 version byte. Verifies the seal signature and the NIP-17 +/// author-equals-signer rule; for v3 layers the kind/scope context binding is +/// enforced by the `nip44` crate against the expected values (13/1059, ""). +pub fn unwrap_gift_wrap(receiver: &Keys, wrap: &Event) -> Result { + if wrap.kind != Kind::GiftWrap { + return Err(WrapError::NotGiftWrap); + } + let seal_json = decrypt_layer(receiver, &wrap.pubkey, &wrap.content, WRAP_KIND)?; + let seal = Event::from_json(seal_json).map_err(|e| WrapError::Event(e.to_string()))?; + seal.verify().map_err(|e| WrapError::Event(e.to_string()))?; + if seal.kind != Kind::Seal { + return Err(WrapError::NotSeal); + } + let rumor_json = decrypt_layer(receiver, &seal.pubkey, &seal.content, SEAL_KIND)?; + let rumor = + UnsignedEvent::from_json(rumor_json).map_err(|e| WrapError::Event(e.to_string()))?; + if rumor.pubkey != seal.pubkey { + return Err(WrapError::SenderMismatch); + } + Ok(Unwrapped { + sender: seal.pubkey, + rumor, + }) +} + +/// Decrypt one layer, branching on the version byte: `0x02` goes to +/// nostr-sdk's v2, `0x03` to the `nip44` crate with the expected context. +fn decrypt_layer( + keys: &Keys, + author: &PublicKey, + content: &str, + expected_kind: u32, +) -> Result { + match nip44::payload_version(content).map_err(|e| WrapError::Crypto(e.to_string()))? { + 2 => sdk_nip44::decrypt(keys.secret_key(), author, content) + .map_err(|e| WrapError::Crypto(e.to_string())), + 3 => { + let ck = v3_conversation_key(keys, author)?; + let plain = nip44::decrypt_v3(&ck, content, expected_kind, EMPTY_SCOPE) + .map_err(|e| WrapError::Crypto(e.to_string()))?; + String::from_utf8(plain).map_err(|e| WrapError::Crypto(e.to_string())) + } + v => Err(WrapError::Crypto(format!("unsupported nip44 version {v}"))), + } +} + +/// The v3 conversation key (raw ECDH x coordinate) between our secret key and +/// a peer's x-only public key. nostr-sdk is on secp256k1 0.29 while the nip44 +/// crate speaks 0.31, so the conversion goes through raw bytes. +fn v3_conversation_key(ours: &Keys, theirs: &PublicKey) -> Result<[u8; 32], WrapError> { + let sk = secp256k1::SecretKey::from_byte_array(ours.secret_key().to_secret_bytes()) + .map_err(|e| WrapError::Crypto(format!("bad secret key: {e}")))?; + let pk = secp256k1::XOnlyPublicKey::from_byte_array(theirs.to_bytes()) + .map_err(|e| WrapError::Crypto(format!("bad public key: {e}")))?; + Ok(nip44::get_conversation_key_v3(sk, pk)) +} + +#[cfg(test)] +mod tests { + use nostr_sdk::nips::nip59::UnwrappedGift; + + use super::*; + + fn rumor(sender: &Keys, receiver: &PublicKey, text: &str) -> UnsignedEvent { + EventBuilder::new(Kind::PrivateDirectMessage, text) + .tags([Tag::public_key(*receiver)]) + .build(sender.public_key()) + } + + fn version_byte(event: &Event) -> u8 { + nip44::payload_version(&event.content).unwrap() + } + + #[test] + fn v3_seal_and_wrap_round_trip() { + let alice = Keys::generate(); + let bob = Keys::generate(); + let r = rumor(&alice, &bob.public_key(), "hello over v3"); + + let wrap = gift_wrap(&alice, &bob.public_key(), r.clone(), WrapVersion::V3).unwrap(); + assert_eq!(wrap.kind, Kind::GiftWrap); + assert_eq!(version_byte(&wrap), 3, "outer layer must be v3"); + // Signed by a fresh ephemeral key, never by Alice. + assert_ne!(wrap.pubkey, alice.public_key()); + wrap.verify().unwrap(); + // Addressed to Bob via the p tag, timestamp fuzzed into the past. + assert!(wrap + .tags + .iter() + .any(|t| t.as_slice().get(1).map(|s| s.as_str()) + == Some(bob.public_key().to_hex().as_str()))); + assert!(wrap.created_at <= Timestamp::now()); + + let unwrapped = unwrap_gift_wrap(&bob, &wrap).unwrap(); + assert_eq!(unwrapped.sender, alice.public_key()); + assert_eq!(unwrapped.rumor.pubkey, alice.public_key()); + assert_eq!(unwrapped.rumor.kind, Kind::PrivateDirectMessage); + assert_eq!(unwrapped.rumor.content, "hello over v3"); + + // A stranger cannot open it. + let mallory = Keys::generate(); + assert!(unwrap_gift_wrap(&mallory, &wrap).is_err()); + // And the sender cannot open their own wrap (it is not wrapped to them). + assert!(unwrap_gift_wrap(&alice, &wrap).is_err()); + } + + #[test] + fn v2_interop_with_nostr_sdk_both_directions() { + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + let alice = Keys::generate(); + let bob = Keys::generate(); + + // Ours -> stock nostr-sdk (what today's v2-only Goblin runs). + let r = rumor(&alice, &bob.public_key(), "ours to sdk"); + let wrap = gift_wrap(&alice, &bob.public_key(), r, WrapVersion::V2).unwrap(); + assert_eq!(version_byte(&wrap), 2); + let gift = rt + .block_on(UnwrappedGift::from_gift_wrap(&bob, &wrap)) + .unwrap(); + assert_eq!(gift.sender, alice.public_key()); + assert_eq!(gift.rumor.content, "ours to sdk"); + + // Stock nostr-sdk -> ours (a v2 Goblin paying us). + let r = rumor(&alice, &bob.public_key(), "sdk to ours"); + let wrap = rt + .block_on(EventBuilder::gift_wrap(&alice, &bob.public_key(), r, [])) + .unwrap(); + let unwrapped = unwrap_gift_wrap(&bob, &wrap).unwrap(); + assert_eq!(unwrapped.sender, alice.public_key()); + assert_eq!(unwrapped.rumor.content, "sdk to ours"); + } + + #[test] + fn unwrap_dispatches_on_version_byte() { + // The receiver is never told which version arrived — the payload + // version byte decides, per layer. + let alice = Keys::generate(); + let bob = Keys::generate(); + for (version, byte) in [(WrapVersion::V2, 2u8), (WrapVersion::V3, 3u8)] { + let r = rumor(&alice, &bob.public_key(), "dispatch"); + let wrap = gift_wrap(&alice, &bob.public_key(), r, version).unwrap(); + assert_eq!(version_byte(&wrap), byte); + let unwrapped = unwrap_gift_wrap(&bob, &wrap).unwrap(); + assert_eq!(unwrapped.sender, alice.public_key()); + } + } + + #[test] + fn v3_context_binding_is_enforced() { + // A v3 payload sealed for one context must not open under another: + // decrypting the WRAP layer content as if it were a SEAL fails on the + // kind binding even with the right conversation key. + let alice = Keys::generate(); + let bob = Keys::generate(); + let r = rumor(&alice, &bob.public_key(), "context"); + let wrap = gift_wrap(&alice, &bob.public_key(), r, WrapVersion::V3).unwrap(); + + let ck = v3_conversation_key(&bob, &wrap.pubkey).unwrap(); + assert!(nip44::decrypt_v3(&ck, &wrap.content, WRAP_KIND, EMPTY_SCOPE).is_ok()); + assert!( + nip44::decrypt_v3(&ck, &wrap.content, SEAL_KIND, EMPTY_SCOPE).is_err(), + "kind binding must reject a cross-context decrypt" + ); + assert!( + nip44::decrypt_v3(&ck, &wrap.content, WRAP_KIND, b"other").is_err(), + "scope binding must reject a cross-context decrypt" + ); + } + + #[test] + fn rumor_id_is_fixed_before_encryption() { + let alice = Keys::generate(); + let bob = Keys::generate(); + let r = rumor(&alice, &bob.public_key(), "id check"); + let wrap = gift_wrap(&alice, &bob.public_key(), r.clone(), WrapVersion::V3).unwrap(); + let mut unwrapped = unwrap_gift_wrap(&bob, &wrap).unwrap(); + let mut original = r; + assert_eq!(unwrapped.rumor.id(), original.id()); + } + + #[test] + fn chooses_best_mutual_version() { + assert_eq!(choose_version(None), WrapVersion::V2); + assert_eq!(choose_version(Some("nip44_v2")), WrapVersion::V2); + assert_eq!(choose_version(Some("nip44_v3 nip44_v2")), WrapVersion::V3); + // Their list is best-first: respect a peer that prefers v2. + assert_eq!(choose_version(Some("nip44_v2 nip44_v3")), WrapVersion::V2); + // Unknown methods are skipped; nothing mutual falls back to v2. + assert_eq!(choose_version(Some("mls nip44_v3")), WrapVersion::V3); + assert_eq!(choose_version(Some("mls")), WrapVersion::V2); + assert_eq!(choose_version(Some("")), WrapVersion::V2); + } + + #[test] + fn reads_capability_from_10050() { + let keys = Keys::generate(); + let event = EventBuilder::new(Kind::InboxRelays, "") + .tags([ + Tag::custom( + nostr_sdk::TagKind::custom("relay"), + ["wss://relay.example".to_string()], + ), + capability_tag(), + ]) + .sign_with_keys(&keys) + .unwrap(); + assert_eq!( + encryption_capability(&event).as_deref(), + Some(ENCRYPTION_CAPABILITIES) + ); + assert_eq!( + choose_version(encryption_capability(&event).as_deref()), + WrapVersion::V3 + ); + + let bare = EventBuilder::new(Kind::InboxRelays, "") + .sign_with_keys(&keys) + .unwrap(); + assert_eq!(encryption_capability(&bare), None); + } +} diff --git a/crates/gp-server/Cargo.toml b/crates/gp-server/Cargo.toml new file mode 100644 index 0000000..4ae13f7 --- /dev/null +++ b/crates/gp-server/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "gp-server" +description = "GoblinPay HTTP server (Actix-Web, in-process rustls TLS)" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "gp-server" +path = "src/main.rs" + +[dependencies] +gp-core = { path = "../gp-core" } +gp-nostr = { path = "../gp-nostr" } +gp-wallet = { path = "../gp-wallet" } +actix-web = { version = "4", default-features = false, features = [ + "macros", + "http2", + "rustls-0_23", +] } +askama = "0.14" +rustls = { version = "0.23", default-features = false, features = [ + "ring", + "logging", + "std", + "tls12", +] } +rustls-pemfile = "2" +serde = { workspace = true } +serde_json.workspace = true +sqlx = { workspace = true } +log = "0.4" +# Outbound webhook delivery. `rustls-no-provider` reuses the process-installed +# ring crypto provider (no aws-lc-rs build) and brings platform-verifier roots; +# no JSON feature (we send a pre-signed body). +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] } +# Stderr logger for the gp-nostr/nym `log` output; no regex filtering needed. +env_logger = { version = "0.11", default-features = false, features = ["humantime"] } + +[dev-dependencies] +# The milestone-3 end-to-end test: a stand-in payer built from nostr-sdk +# gift-wraps a REAL S1 (generated by the gp-goblin-sender subprocess), the +# ingest pipeline receives it through the real WalletReceiver, and the reply +# is decrypted and finalized by Goblin's wallet stack. +nostr-sdk = { version = "0.44", features = ["nip44", "nip49", "nip59"] } +nip44 = { path = "../../../nip44" } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +rand = "0.6" +grin_keychain = "=5.4.1" +grin_core = "=5.4.1" diff --git a/crates/gp-server/askama.toml b/crates/gp-server/askama.toml new file mode 100644 index 0000000..47e0b8d --- /dev/null +++ b/crates/gp-server/askama.toml @@ -0,0 +1,3 @@ +# Templates live at the workspace root (see the repo layout in the plan). +[general] +dirs = ["../../templates"] diff --git a/crates/gp-server/src/admin.rs b/crates/gp-server/src/admin.rs new file mode 100644 index 0000000..7a70640 --- /dev/null +++ b/crates/gp-server/src/admin.rs @@ -0,0 +1,386 @@ +//! The authenticated admin surface (`GP_ADMIN_TOKEN`): a zero-JS dashboard +//! plus the JSON management API for per-user endpubs (milestone 5b) and webhook +//! deliveries. Everything here is server-rendered or plain JSON, no build step. + +use actix_web::{web, HttpRequest, HttpResponse, Responder}; +use askama::Template; +use gp_core::config::Config; +use gp_core::endpub; +use gp_core::webhook::nanogrin_to_grin; +use gp_nostr::{Keys, PublicKey}; +use log::error; +use serde::Deserialize; +use sqlx::SqlitePool; + +use crate::auth::authorized; +use crate::payments::ReceiptSigner; + +/// Register the admin routes. +pub fn configure(cfg: &mut web::ServiceConfig) { + cfg.route("/admin", web::get().to(dashboard)) + .route("/admin/payments", web::get().to(list_payments)) + .route("/admin/users", web::get().to(list_users)) + .route("/admin/users", web::post().to(create_user)) + .route("/admin/users/{id}", web::get().to(get_user)) + .route("/admin/users/{id}/rotate", web::post().to(rotate_user)) + .route( + "/admin/users/{id}/rotate-interval", + web::post().to(set_rotate_interval), + ) + .route("/admin/webhooks", web::get().to(list_webhooks)); +} + +fn deny() -> HttpResponse { + HttpResponse::Unauthorized().json(serde_json::json!({"error": "unauthorized"})) +} + +fn is_admin(req: &HttpRequest, cfg: &Config) -> bool { + authorized(req, cfg.admin_token.as_ref().map(|s| s.reveal())) +} + +fn master_secret(keys: &Keys) -> [u8; 32] { + keys.secret_key().to_secret_bytes() +} + +/// npub for an x-only pubkey hex (or empty on parse failure). +fn npub_of_hex(hex: &str) -> String { + PublicKey::from_hex(hex) + .map(gp_nostr::npub_of) + .unwrap_or_default() +} + +// ----- dashboard (HTML) ----- + +struct PaymentRow { + slate_id: String, + amount_grin: String, + status: String, + invoice_id: String, + user_id: String, + created_at: String, +} + +struct BalanceRow { + user_id: String, + npub: String, + epoch: i64, + balance_grin: String, +} + +#[derive(Template)] +#[template(path = "admin.html")] +struct AdminPage { + payments: Vec, + balances: Vec, + node_url: String, + match_mode: String, + nym: bool, + ingest: bool, + relay_count: usize, + webhook_configured: bool, + pending_webhooks: i64, + rotate_interval: i64, + overlap_epochs: i64, +} + +async fn dashboard( + req: HttpRequest, + pool: web::Data, + cfg: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return HttpResponse::Unauthorized().body("unauthorized"); + } + let payments = recent_payment_rows(pool.get_ref()).await; + let balances = balance_rows(pool.get_ref()).await; + let pending_webhooks: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM webhook_delivery WHERE delivered = 0") + .fetch_one(pool.get_ref()) + .await + .unwrap_or(0); + let page = AdminPage { + payments, + balances, + node_url: cfg.node_url.clone(), + match_mode: format!("{:?}", cfg.match_mode).to_lowercase(), + nym: cfg.nym, + ingest: cfg.ingest, + relay_count: gp_nostr::relays::resolve(&cfg.relays).len(), + webhook_configured: cfg.webhook_url.is_some(), + pending_webhooks, + rotate_interval: cfg.endpub_rotate_interval, + overlap_epochs: cfg.endpub_overlap_epochs, + }; + match page.render() { + Ok(html) => HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + Err(e) => { + error!("admin render: {e}"); + HttpResponse::InternalServerError().body("template error") + } + } +} + +async fn recent_payment_rows(pool: &SqlitePool) -> Vec { + #[allow(clippy::type_complexity)] // a flat sqlx row tuple, mapped immediately below + let rows: Vec<(String, i64, String, Option, Option, String)> = sqlx::query_as( + "SELECT slate_id, amount, status, invoice_id, user_id, created_at FROM payment \ + ORDER BY created_at DESC LIMIT 50", + ) + .fetch_all(pool) + .await + .unwrap_or_default(); + rows.into_iter() + .map( + |(slate_id, amount, status, invoice_id, user_id, created_at)| PaymentRow { + slate_id, + amount_grin: nanogrin_to_grin(amount as u64), + status, + invoice_id: invoice_id.unwrap_or_default(), + user_id: user_id.unwrap_or_default(), + created_at, + }, + ) + .collect() +} + +async fn balance_rows(pool: &SqlitePool) -> Vec { + endpub::list_with_balances(pool) + .await + .unwrap_or_default() + .into_iter() + .map(|b| BalanceRow { + user_id: b.user_id, + npub: npub_of_hex(&b.endpub), + epoch: b.epoch, + balance_grin: nanogrin_to_grin(b.balance.max(0) as u64), + }) + .collect() +} + +// ----- JSON API ----- + +async fn list_payments( + req: HttpRequest, + pool: web::Data, + cfg: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return deny(); + } + #[allow(clippy::type_complexity)] // a flat sqlx row tuple, mapped immediately below + let rows: Vec<( + String, + i64, + Option, + String, + Option, + Option, + String, + )> = sqlx::query_as( + "SELECT slate_id, amount, payer, status, invoice_id, user_id, created_at \ + FROM payment ORDER BY created_at DESC LIMIT 200", + ) + .fetch_all(pool.get_ref()) + .await + .unwrap_or_default(); + let list: Vec<_> = rows + .into_iter() + .map( + |(id, amount, payer, status, invoice_id, user_id, created_at)| { + serde_json::json!({ + "payment_id": id, "amount": amount, "payer": payer, "status": status, + "invoice_id": invoice_id, "user_id": user_id, "created_at": created_at, + }) + }, + ) + .collect(); + HttpResponse::Ok().json(serde_json::json!({ "payments": list })) +} + +#[derive(Deserialize)] +struct CreateUserBody { + user_id: Option, + rotate_interval: Option, +} + +fn endpub_json(cfg: &Config, user_id: &str, epoch: i64, pubkey: &str) -> serde_json::Value { + let relays = gp_nostr::relays::resolve(&cfg.relays); + let (npub, nprofile, qr) = match PublicKey::from_hex(pubkey) { + Ok(pk) => ( + gp_nostr::npub_of(pk), + gp_nostr::nprofile(pk, &relays), + gp_core::qr::svg(&gp_nostr::nprofile(pk, &relays), cfg.qr_logo_href()) + .unwrap_or_default(), + ), + Err(_) => (String::new(), String::new(), String::new()), + }; + serde_json::json!({ + "user_id": user_id, "epoch": epoch, "pubkey": pubkey, + "npub": npub, "nprofile": nprofile, "qr_svg": qr, + }) +} + +async fn create_user( + req: HttpRequest, + body: web::Json, + pool: web::Data, + cfg: web::Data, + signer: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return deny(); + } + let Some(keys) = signer.0.as_ref() else { + return HttpResponse::ServiceUnavailable() + .json(serde_json::json!({"error": "server identity not loaded"})); + }; + let sk = master_secret(keys); + let body = body.into_inner(); + match endpub::create_user(pool.get_ref(), &sk, body.user_id, body.rotate_interval).await { + Ok((user, ep)) => { + HttpResponse::Ok().json(endpub_json(cfg.get_ref(), &user.id, ep.epoch, &ep.pubkey)) + } + Err(e) => { + error!("create user: {e}"); + HttpResponse::InternalServerError().json(serde_json::json!({"error": "internal"})) + } + } +} + +async fn list_users( + req: HttpRequest, + pool: web::Data, + cfg: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return deny(); + } + let balances = endpub::list_with_balances(pool.get_ref()) + .await + .unwrap_or_default(); + let list: Vec<_> = balances + .into_iter() + .map(|b| { + serde_json::json!({ + "user_id": b.user_id, "epoch": b.epoch, + "endpub": b.endpub, "npub": npub_of_hex(&b.endpub), "balance": b.balance, + }) + }) + .collect(); + HttpResponse::Ok().json(serde_json::json!({ "users": list })) +} + +async fn get_user( + req: HttpRequest, + path: web::Path, + pool: web::Data, + cfg: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return deny(); + } + let id = path.into_inner(); + match endpub::current_endpub(pool.get_ref(), &id).await { + Ok(Some(ep)) => { + HttpResponse::Ok().json(endpub_json(cfg.get_ref(), &id, ep.epoch, &ep.pubkey)) + } + Ok(None) => HttpResponse::NotFound().json(serde_json::json!({"error": "user not found"})), + Err(e) => { + error!("get user: {e}"); + HttpResponse::InternalServerError().json(serde_json::json!({"error": "internal"})) + } + } +} + +async fn rotate_user( + req: HttpRequest, + path: web::Path, + pool: web::Data, + cfg: web::Data, + signer: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return deny(); + } + let Some(keys) = signer.0.as_ref() else { + return HttpResponse::ServiceUnavailable() + .json(serde_json::json!({"error": "server identity not loaded"})); + }; + let sk = master_secret(keys); + let id = path.into_inner(); + match endpub::rotate(pool.get_ref(), &sk, &id).await { + Ok(ep) => HttpResponse::Ok().json(endpub_json(cfg.get_ref(), &id, ep.epoch, &ep.pubkey)), + Err(e) => { + error!("rotate user: {e}"); + HttpResponse::InternalServerError().json(serde_json::json!({"error": "internal"})) + } + } +} + +#[derive(Deserialize)] +struct RotateIntervalBody { + /// New interval in seconds; null clears the per-user override. + interval: Option, +} + +async fn set_rotate_interval( + req: HttpRequest, + path: web::Path, + body: web::Json, + pool: web::Data, + cfg: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return deny(); + } + let id = path.into_inner(); + match endpub::set_rotate_interval(pool.get_ref(), &id, body.into_inner().interval).await { + Ok(true) => HttpResponse::Ok().json(serde_json::json!({"user_id": id, "updated": true})), + Ok(false) => HttpResponse::NotFound().json(serde_json::json!({"error": "user not found"})), + Err(e) => { + error!("set rotate interval: {e}"); + HttpResponse::InternalServerError().json(serde_json::json!({"error": "internal"})) + } + } +} + +async fn list_webhooks( + req: HttpRequest, + pool: web::Data, + cfg: web::Data, +) -> impl Responder { + if !is_admin(&req, cfg.get_ref()) { + return deny(); + } + #[allow(clippy::type_complexity)] // a flat sqlx row tuple, mapped immediately below + let rows: Vec<( + String, + Option, + String, + i64, + i64, + String, + Option, + )> = sqlx::query_as( + "SELECT id, payment_id, event_type, attempts, delivered, next_attempt_at, last_error \ + FROM webhook_delivery ORDER BY created_at DESC LIMIT 200", + ) + .fetch_all(pool.get_ref()) + .await + .unwrap_or_default(); + let list: Vec<_> = rows + .into_iter() + .map( + |(id, payment_id, event_type, attempts, delivered, next_attempt_at, last_error)| { + serde_json::json!({ + "event_id": id, "payment_id": payment_id, "event_type": event_type, + "attempts": attempts, "delivered": delivered == 1, + "next_attempt_at": next_attempt_at, "last_error": last_error, + }) + }, + ) + .collect(); + HttpResponse::Ok().json(serde_json::json!({ "deliveries": list })) +} diff --git a/crates/gp-server/src/auth.rs b/crates/gp-server/src/auth.rs new file mode 100644 index 0000000..6ef74e2 --- /dev/null +++ b/crates/gp-server/src/auth.rs @@ -0,0 +1,28 @@ +//! Bearer-token authorization for the write and admin surfaces. +//! +//! Two independent tokens: `GP_API_TOKEN` gates the connector/create-invoice +//! API, `GP_ADMIN_TOKEN` gates the admin dashboard and endpub/webhook +//! management. A route whose token is unset is closed (401), never open. The +//! public-by-token surfaces (`/pay/`, payment status) carry their own +//! unguessable capability and do not use these. + +use actix_web::HttpRequest; + +/// The bearer token from the `Authorization: Bearer ` header, if any. +pub fn bearer(req: &HttpRequest) -> Option { + let value = req.headers().get("Authorization")?.to_str().ok()?; + value + .strip_prefix("Bearer ") + .or_else(|| value.strip_prefix("bearer ")) + .map(|s| s.trim().to_string()) +} + +/// Is the request authorized against `expected`? An unset expected token +/// (feature not configured) is always unauthorized. The comparison is +/// constant time. +pub fn authorized(req: &HttpRequest, expected: Option<&str>) -> bool { + match (expected, bearer(req)) { + (Some(exp), Some(got)) => gp_core::ct_eq(got.as_bytes(), exp.as_bytes()), + _ => false, + } +} diff --git a/crates/gp-server/src/checkout.rs b/crates/gp-server/src/checkout.rs new file mode 100644 index 0000000..a5a841f --- /dev/null +++ b/crates/gp-server/src/checkout.rs @@ -0,0 +1,245 @@ +//! The hosted, zero-JS checkout: the `/pay/` page (shared renderer for +//! embedded and hosted use), its live status, and the manual-slatepack +//! fallback. +//! +//! The page shows the amount, a server-generated QR SVG of the recipient +//! `nprofile`, the `nprofile`/`npub` strings, live status via a +//! `` while open, and a ` + +
+ Can't scan? Pay manually with a slatepack + {% if wallet_available %} +
    +
  1. In your wallet, send {{ info.amount_display }} using the manual / slatepack option.
  2. +
  3. Paste the generated S1 slatepack below and submit.
  4. +
  5. Copy the response slatepack we return, back into your wallet to finalize and post.
  6. +
+
+ + + +
+ {% else %} +

Manual receive is unavailable on this instance.

+ {% endif %} +
+ {% endif %} + + {% if let Some(memo) = info.memo %}

{{ memo }}

{% endif %} + + + + diff --git a/templates/pay_result.html b/templates/pay_result.html new file mode 100644 index 0000000..ff44e64 --- /dev/null +++ b/templates/pay_result.html @@ -0,0 +1,28 @@ + + + + + +Slatepack response + + + +
+

{% if ok %}Payment received{% else %}Could not receive{% endif %}

+

{{ message }}

+ + {% if ok %} + + +
    +
  1. Select all of the text above and copy it.
  2. +
  3. Paste it back into your wallet to finalize the transaction.
  4. +
  5. Your wallet posts it to the chain; GoblinPay confirms it on receipt.
  6. +
+ {% endif %} + +

Back to the invoice

+ +
+ +