From 98341ed4672688d8987a79437ecbbd7dce759d98 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 20 Oct 2026 17:44:47 +0530 Subject: [PATCH 1/7] Integrate pinned BFA graph and authenticated invoice sends --- BFA-MIGRATION.md | 27 + Cargo.lock | 401 +-- Cargo.toml | 9 +- index.d.ts | 22 + index.js | 7 +- json-boundary.js | 53 +- package-lock.json | 55 +- package.json | 19 +- patches/release-adapter-v0.17.0-beta.3.patch | 2384 +++++++++++++----- scripts/burn-types.ts | 8 + scripts/contract.test.js | 29 +- scripts/install-overlay-addon.js | 2 + scripts/install-overlay-addon.test.js | 6 +- scripts/invoice-boundary.test.js | 59 + scripts/native-admission.test.js | 2 +- scripts/release-contract.js | 29 +- scripts/source-credential.js | 4 +- scripts/source-credential.test.js | 4 +- scripts/with-source-access.js | 2 +- src/lib.rs | 8 +- 20 files changed, 2189 insertions(+), 941 deletions(-) create mode 100644 BFA-MIGRATION.md create mode 100644 scripts/invoice-boundary.test.js diff --git a/BFA-MIGRATION.md b/BFA-MIGRATION.md new file mode 100644 index 0000000..318fcb9 --- /dev/null +++ b/BFA-MIGRATION.md @@ -0,0 +1,27 @@ +# Unreleased rgb-lib-bfa migration candidate + +Candidate version: `0.2.0-bfa.0`. This branch has not been merged or published. Published beta.5 remains an independent baseline. The native graph is pinned in package.json, the exact maintained patch, Cargo locks and runtime contract; all generated binaries must match those identities. + +The candidate adds `SdkNode.sendRgbInvoice` for protocol-1.0 proxy transfers. Pass the complete invoice without reconstructing its recipient keys or encryption fields: + +```js +await node.sendRgbInvoice({ + invoice, + asset_id: assetId, + amount: '1000000', + witness_data: null, + donation: false, + fee_rate: '2', + min_confirmations: 1 +}) +``` + +Use canonical decimal strings when values may exceed JavaScript's safe integer range. Witness invoices additionally need their explicit Bitcoin output amount; proxy witness blinding is derived by the library and must not be supplied. An acknowledged proxy send needs at least 15 minutes remaining; wallet UI uses a 16-minute preflight margin. Decode returns additive `public_key` and `encryption`, with legacy `proxy_recipient_id` nullable. The old recipient-group boundary cannot invent missing proxy authentication data. + +Iris remains on-chain only. Signer shutdown, retained-clone revocation, owned task/listener cleanup, native external signing, BIP-322, recovery access and the durable default-on VSS policy are preserved in the maintained adapter. VSS manifest v2 uses immutable generation keys and confirms the canonical remote commit before acknowledging a content-bound snapshot; interrupted/uncertain outcomes retain pending state. Fresh migration test stores are required; this branch does not claim old manifest format migration. + +The new library does not provide old IFA linking, deferred consume for channel sweeps or custom channel funding locktime. RLN rejects these specific unsupported boundaries before signing or unsafe mutation. Compilation is not generic Lightning/channel qualification. The external-signer VSS public-bootstrap limitation is unchanged and must not be worked around by supplying a seed to RGB. + +The supplied Signet public contract is **Bridged USDT / BUSDT / 6**, asset `rgb:OG9No4hZ-UrxoWmR-09ohtOk-ppxBN7h-g~KkQro-ncKc9A4`, SHA-256 `dfb49261fed5ad3dd237dd3a03913793b76fe70e486372bbeb0769b82158a895`. Native import validates it and creates no balance. Funded regtest tests must use a fresh local BFA contract with its own identity. + +Native artifact rebuild, all-target package admission, actual consumers and funded/device/bridge tests are separate acceptance gates. Keep the normal strict source, wrapper, lock, distribution and binary identity checks enabled. The candidate CI workflow builds artifacts without publication. No hosted dev bridge operations are authorized for these automated tests. diff --git a/Cargo.lock b/Cargo.lock index b1a7ddb..a49d6db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,8 +95,6 @@ dependencies = [ "amplify_syn", "ascii", "rand 0.8.7", - "serde", - "stringly_conversions", "wasm-bindgen", ] @@ -129,7 +127,6 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afed304556696656d2d71495e1e5f2c4b524a3fb6eb0f2f3778ffc482a40b8a8" dependencies = [ - "serde", "wasm-bindgen", ] @@ -400,9 +397,6 @@ name = "ascii" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" -dependencies = [ - "serde", -] [[package]] name = "askama" @@ -501,9 +495,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.0" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -511,9 +505,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.44.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", @@ -679,6 +673,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -715,6 +715,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bdk_coin_select" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43064fa3dd1d3a8b24079cfcb5ede6b785857edc782277f17a1736511ccc1916" + [[package]] name = "bdk_core" version = "0.6.3" @@ -747,24 +753,25 @@ dependencies = [ ] [[package]] -name = "bdk_file_store" -version = "0.22.0" +name = "bdk_tx" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b9a98edb58d02b601239832674a9ca9f1465e08e1dd1fac3a0e34004372406" +checksum = "40406c9952f15ed30893826bd83b7d67b6bf11d56574c1efc3db3a5d70cdfcea" dependencies = [ - "bdk_core", - "bincode", - "serde", + "bdk_coin_select", + "miniscript", + "rand 0.8.7", + "rand_core 0.6.4", ] [[package]] name = "bdk_wallet" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" +checksum = "163ec032e0a5a795ded2a7e47fc40a887c75743326483147e4465159c53abdc4" dependencies = [ "bdk_chain", - "bdk_file_store", + "bdk_tx", "bip39", "bitcoin", "miniscript", @@ -829,7 +836,7 @@ dependencies = [ "biscuit-parser", "biscuit-quote", "ecdsa", - "ed25519-dalek", + "ed25519-dalek 2.2.0", "elliptic-curve", "getrandom 0.2.17", "hex", @@ -998,8 +1005,8 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b65c2e1ab98050c93cc9ada070d5070e014329c65196d03f6f8f1f75c2666f37" dependencies = [ - "rustls 0.23.45", - "rustls-webpki 0.103.15", + "rustls 0.23.42", + "rustls-webpki 0.103.13", "tokio", "tokio-rustls", "webpki-roots 1.0.9", @@ -1110,9 +1117,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecheck" @@ -1150,9 +1157,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "camino" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] @@ -1602,12 +1609,27 @@ dependencies = [ "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto", + "fiat-crypto 0.2.9", "rustc_version", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rustc_version", + "subtle", +] + [[package]] name = "curve25519-dalek-derive" version = "0.1.1" @@ -1910,7 +1932,7 @@ dependencies = [ "elliptic-curve", "rfc6979", "serdect", - "signature", + "signature 2.2.0", "spki 0.7.3", ] @@ -1921,7 +1943,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8 0.10.2", - "signature", + "signature 2.2.0", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "signature 3.0.0", ] [[package]] @@ -1930,8 +1961,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", "rand_core 0.6.4", "serde", "sha2 0.10.9", @@ -1939,6 +1970,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519 3.0.0", + "sha2 0.11.0", + "subtle", +] + [[package]] name = "either" version = "1.17.0" @@ -1958,7 +2001,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.45", + "rustls 0.23.42", "serde", "serde_json", "webpki-roots 0.25.4", @@ -1975,7 +2018,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.45", + "rustls 0.23.42", "serde", "serde_json", "webpki-roots 0.25.4", @@ -2057,7 +2100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2133,6 +2176,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "file-format" version = "0.29.0" @@ -2439,9 +2488,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.16" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -2729,7 +2778,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.45", + "rustls 0.23.42", "tokio", "tokio-rustls", "tower-service", @@ -2973,7 +3022,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3103,13 +3152,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -3203,14 +3251,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "bitflags 2.13.1", "libc", "plain", - "redox_syscall 0.7.5", + "redox_syscall 0.9.0", ] [[package]] @@ -3260,6 +3308,7 @@ dependencies = [ "rgb-lib", "rgb-strict-encoding", "serde", + "serde_json", "tokio", ] @@ -3712,22 +3761,13 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonasync" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81f7335a3fa37124e24461d6164485760d4c056dd92a81a70c23fc28c2b63c8" -dependencies = [ - "amplify", -] - [[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.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3829,9 +3869,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags 2.13.1", "cfg-if", @@ -3869,9 +3909,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -4145,9 +4185,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" @@ -4412,7 +4452,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.45", + "rustls 0.23.42", "socket2", "thiserror 2.0.19", "tokio", @@ -4434,7 +4474,7 @@ dependencies = [ "rand_pcg", "ring", "rustc-hash", - "rustls 0.23.45", + "rustls 0.23.42", "rustls-pki-types", "slab", "thiserror 2.0.19", @@ -4454,7 +4494,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4598,9 +4638,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.5" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ "bitflags 2.13.1", ] @@ -4738,7 +4778,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.45", + "rustls 0.23.42", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4768,16 +4808,14 @@ dependencies = [ [[package]] name = "rgb-aluvm" -version = "0.11.1-rc.4" +version = "0.11.2-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf4d92478fcff567ff0bd9f18684fd954f8426c74f16e5517254b0a2d34023a" +checksum = "21de8dc75b4ce90dffe66cff585053e9f5d2abe6436589c95e6308f0605195f4" dependencies = [ "amplify", "baid64", "blake3", - "getrandom 0.3.4", "half", - "paste", "rgb-ascii-armor", "rgb-strict-encoding", "rgb-strict-types", @@ -4789,9 +4827,9 @@ dependencies = [ [[package]] name = "rgb-ascii-armor" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcde5a129540e5911c24930fd854ea658a715b50a690d254273088f907a63516" +checksum = "0b3a739537822793adce1f9539bb3fae3d54a16f409e8353ac85281bdfcec078" dependencies = [ "amplify", "baid64", @@ -4802,8 +4840,9 @@ dependencies = [ [[package]] name = "rgb-consensus" -version = "0.11.1-rc.11" -source = "git+https://github.com/UTEXO-Protocol/rgb-consensus-s-bfa?rev=2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757#2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757" +version = "0.11.2-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd5d6c21f60f24976d8b983b6b5925cd87a4865af8ad6d965d45360518bbfd24" dependencies = [ "amplify", "baid64", @@ -4816,11 +4855,11 @@ dependencies = [ "hex-conservative 1.2.0", "rand 0.9.5", "rgb-aluvm", + "rgb-ascii-armor", "rgb-strict-encoding", "rgb-strict-types", "ripemd", - "secp256k1 0.31.1", - "secp256k1 0.32.0-beta.2", + "secp256k1 0.33.1", "serde", "sha2 0.11.0", "wasm-bindgen", @@ -4828,8 +4867,9 @@ dependencies = [ [[package]] name = "rgb-invoicing" -version = "0.11.1-rc.11" -source = "git+https://github.com/UTEXO-Protocol/rgb-ops-s-bfa?rev=773766b5ca7a9e705ec68c2f9ed0917f505cd6e3#773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" +version = "0.11.2-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76692ae2017e3b4789d2539dcf483b5e5e9f4468030d9ffb619aabeab7cd43b4" dependencies = [ "amplify", "baid64", @@ -4846,17 +4886,20 @@ dependencies = [ [[package]] name = "rgb-lib" version = "0.3.0-beta.7" -source = "git+https://github.com/UTEXO-Protocol/rgb-lib.git?tag=v0.3.0-beta.50-bfa#1b951af0c8233308c919975552487be19ad2a930" +source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655" dependencies = [ "amplify", - "base64 0.22.1", + "base64 0.23.1", "bdk_electrum", "bdk_esplora", "bdk_wallet", "chacha20poly1305 0.11.0", + "ed25519-dalek 3.0.0", "file-format", "hex", - "hkdf 0.12.4", + "hkdf 0.13.0", + "hmac 0.13.0", + "libc", "rand 0.10.2", "reqwest 0.13.4", "rgb-invoicing", @@ -4866,13 +4909,13 @@ dependencies = [ "rgb-schemas", "rgb-strict-encoding", "rgb-strict-types", - "rustls 0.23.45", + "rustls 0.23.42", "scrypt 0.12.0", "sea-orm 2.0.2", "sea-query 1.0.2", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "slog", "slog-async", "slog-term", @@ -4881,15 +4924,15 @@ dependencies = [ "time", "tokio", "url", - "vss-client-ng", "walkdir", + "x25519-dalek", "zip 8.6.0", ] [[package]] name = "rgb-lib-migration" version = "0.3.0-beta.5" -source = "git+https://github.com/UTEXO-Protocol/rgb-lib.git?tag=v0.3.0-beta.50-bfa#1b951af0c8233308c919975552487be19ad2a930" +source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655" dependencies = [ "sea-orm-migration 2.0.2", "tokio", @@ -4917,7 +4960,9 @@ dependencies = [ "electrum-client 0.24.1", "esplora-client", "futures", + "hex", "hex-conservative 0.3.2", + "hkdf 0.12.4", "libc", "lightning 0.2.2", "lightning-background-processor", @@ -4936,11 +4981,12 @@ dependencies = [ "rgb-lib", "rgb-psbt-utils", "rln-migration", - "rustls 0.23.45", + "rustls 0.23.42", "scrypt 0.11.0", "sea-orm 1.1.20", "serde", "serde_json", + "sha2 0.10.9", "signer-external", "tempfile", "thiserror 2.0.19", @@ -4966,8 +5012,9 @@ dependencies = [ [[package]] name = "rgb-ops" -version = "0.11.1-rc.11" -source = "git+https://github.com/UTEXO-Protocol/rgb-ops-s-bfa?rev=773766b5ca7a9e705ec68c2f9ed0917f505cd6e3#773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" +version = "0.11.2-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b0b332a32be0e41ce328d534d559e67a0add6381d250bdc188e44eb8777ca7" dependencies = [ "amplify", "baid64", @@ -4975,8 +5022,6 @@ dependencies = [ "electrum-client 0.25.0", "esplora-client", "getrandom 0.3.4", - "nonasync", - "rand 0.9.5", "rgb-aluvm", "rgb-ascii-armor", "rgb-consensus", @@ -4990,9 +5035,9 @@ dependencies = [ [[package]] name = "rgb-psbt-utils" -version = "0.11.1-rc.11" +version = "0.11.2-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df8f48e7c851814306af91411d7d6f7e8571939d509d4a786597a5eba0efbbf" +checksum = "16167fb6520c432702b42dbc6344c94573ff8873082386c129718a66bcb535ce" dependencies = [ "amplify", "getrandom 0.3.4", @@ -5004,8 +5049,9 @@ dependencies = [ [[package]] name = "rgb-schemas" -version = "0.11.1-rc.11" -source = "git+https://github.com/UTEXO-Protocol/rgb-schemas-s-bfa?rev=238dfe7a89db998107fd87d919bf2757a2746d7f#238dfe7a89db998107fd87d919bf2757a2746d7f" +version = "0.11.2-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "480f3d71e0f1b23b3c75b59255d62de178259e074f4a74410c330c734f52246f" dependencies = [ "amplify", "rgb-aluvm", @@ -5015,9 +5061,9 @@ dependencies = [ [[package]] name = "rgb-strict-encoding" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f68326e14d4b627f86634ea324510135cb551e27c5583a74811fa230b0cf34d" +checksum = "43cf06a3afea7b29f92f279a5fabb39215d1287f796a8d1a75ce77dd4f807fd4" dependencies = [ "amplify", "bitcoin", @@ -5028,9 +5074,9 @@ dependencies = [ [[package]] name = "rgb-strict-encoding-derive" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33367536cfef03cb582cd9f8f25e193c2298d3ec3112eb59018a376b465f1ed9" +checksum = "473d4bc563f8db593abf7213f82fafc5bc667125f2c39222cd66ead369e60421" dependencies = [ "amplify_syn", "heck 0.5.0", @@ -5041,9 +5087,9 @@ dependencies = [ [[package]] name = "rgb-strict-types" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e920bdf61b662a3743572a03696b0d88992eeb7d4f7afa0902915968327b3ce6" +checksum = "5efea5be04a66a22f96580fb8c7fd9558f7c5d74039c096bb024e4f325f4529a" dependencies = [ "amplify", "baid64", @@ -5156,7 +5202,7 @@ dependencies = [ "pkcs1", "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", + "signature 2.2.0", "spki 0.7.3", "subtle", "zeroize", @@ -5223,7 +5269,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5240,16 +5286,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.15", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -5287,14 +5333,14 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.45", + "rustls 0.23.42", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.15", + "rustls-webpki 0.103.13", "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5315,9 +5361,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -5770,24 +5816,12 @@ dependencies = [ [[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.5", - "secp256k1-sys 0.11.0", - "serde", -] - -[[package]] -name = "secp256k1" -version = "0.32.0-beta.2" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5fdc7d6e800869d3fd60ff857c479bf0a83ea7bf44b389e64461e844204994" +checksum = "d7f404a8dab7a7a5a631e741d8699aa9c8e1d689fc26ccf897c7187565490b69" dependencies = [ "rand 0.9.5", - "secp256k1-sys 0.12.0", + "secp256k1-sys 0.14.1", "serde", ] @@ -5802,18 +5836,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - -[[package]] -name = "secp256k1-sys" -version = "0.12.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3be00697c88c00fe102af8dc316038cc2062eab8da646e7463f4c0e70ca9fd" +checksum = "f6b2992d4a3cd244539a7d5d0966aadbe5ca7fb868a5d7e38c29499b9e709bbd" dependencies = [ "cc", ] @@ -5936,16 +5961,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_str_helpers" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b744a7c94f2f3785496af33a0d93857dfc0c521e25c38e993e9c5bb45f09c841" -dependencies = [ - "serde", - "serde_derive", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6101,10 +6116,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" + [[package]] name = "signer-external" version = "0.1.0-alpha" -source = "git+https://github.com/UTEXO-Protocol/rln-external-signer.git?branch=main#0fb005ec4b927ddbe13e1646d247b5bb11e8ffed" +source = "git+https://github.com/UTEXO-Protocol/rln-external-signer.git?rev=0fb005ec4b927ddbe13e1646d247b5bb11e8ffed#0fb005ec4b927ddbe13e1646d247b5bb11e8ffed" dependencies = [ "base64 0.22.1", "bitcoin", @@ -6124,9 +6145,9 @@ dependencies = [ [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -6291,7 +6312,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.45", + "rustls 0.23.42", "serde", "serde_json", "sha2 0.10.9", @@ -6329,7 +6350,7 @@ dependencies = [ "memchr", "percent-encoding", "rust_decimal", - "rustls 0.23.45", + "rustls 0.23.42", "serde", "serde_json", "sha2 0.10.9", @@ -6634,16 +6655,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stringly_conversions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff63080f492dd4d289ffcaed8d7ece38adfb423db910eb342c0e04d409536a7a" -dependencies = [ - "paste", - "serde_str_helpers", -] - [[package]] name = "stringprep" version = "0.1.5" @@ -6778,10 +6789,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6790,7 +6801,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6958,7 +6969,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.45", + "rustls 0.23.42", "tokio", ] @@ -7073,16 +7084,16 @@ dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -7523,7 +7534,7 @@ dependencies = [ [[package]] name = "vls-core" version = "0.14.0" -source = "git+https://github.com/UTEXO-Protocol/vls-core.git?branch=feat%2Frgb-compatibility#45c72edfd58620849eb486925439a76526b415ae" +source = "git+https://github.com/UTEXO-Protocol/vls-core.git?rev=45c72edfd58620849eb486925439a76526b415ae#45c72edfd58620849eb486925439a76526b415ae" dependencies = [ "ahash 0.8.12", "anyhow", @@ -7613,7 +7624,7 @@ dependencies = [ [[package]] name = "vls-protocol-signer" version = "0.14.0" -source = "git+https://github.com/UTEXO-Protocol/vls-protocol-signer.git?branch=feat%2Frgb-compatibility#dddd336fcdf84c4febdd7af8363bda8d1721d893" +source = "git+https://github.com/UTEXO-Protocol/vls-protocol-signer.git?rev=dddd336fcdf84c4febdd7af8363bda8d1721d893#dddd336fcdf84c4febdd7af8363bda8d1721d893" dependencies = [ "bit-vec", "log", @@ -7667,9 +7678,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] @@ -7682,9 +7693,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -7696,9 +7707,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -7706,9 +7717,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7716,9 +7727,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -7729,18 +7740,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -7758,9 +7769,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -7848,7 +7859,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -8095,9 +8106,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -8123,6 +8134,16 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek 5.0.0", + "rand_core 0.10.1", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 8fdce51..38eaa73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ napi-build = "2" lto = true opt-level = 3 strip = "symbols" +overflow-checks = true codegen-units = 1 [profile.dev] @@ -51,12 +52,8 @@ lightning-net-tokio = { path = ".native-source/rust-lightning/lightni lightning-persister = { path = ".native-source/rust-lightning/lightning-persister" } lightning-rapid-gossip-sync = { path = ".native-source/rust-lightning/lightning-rapid-gossip-sync" } lightning-transaction-sync = { path = ".native-source/rust-lightning/lightning-transaction-sync" } -vls-core = { git = "https://github.com/UTEXO-Protocol/vls-core.git", branch = "feat/rgb-compatibility" } -vls-protocol-signer = { git = "https://github.com/UTEXO-Protocol/vls-protocol-signer.git", branch = "feat/rgb-compatibility" } -rgb-consensus = { git = "https://github.com/UTEXO-Protocol/rgb-consensus-s-bfa", rev = "2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757" } -rgb-ops = { git = "https://github.com/UTEXO-Protocol/rgb-ops-s-bfa", rev = "773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" } -rgb-invoicing = { git = "https://github.com/UTEXO-Protocol/rgb-ops-s-bfa", rev = "773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" } -rgb-schemas = { git = "https://github.com/UTEXO-Protocol/rgb-schemas-s-bfa", rev = "238dfe7a89db998107fd87d919bf2757a2746d7f" } +vls-core = { git = "https://github.com/UTEXO-Protocol/vls-core.git", rev = "45c72edfd58620849eb486925439a76526b415ae" } +vls-protocol-signer = { git = "https://github.com/UTEXO-Protocol/vls-protocol-signer.git", rev = "dddd336fcdf84c4febdd7af8363bda8d1721d893" } # Keep compile-time proc-macros dyld-compatible on macOS 27 (Rust 1.94). # Shipped optimized addons retain [profile.release] strip = "symbols". diff --git a/index.d.ts b/index.d.ts index baad0db..afefdd9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -123,6 +123,26 @@ export interface DecodedRgbInvoice { network: string expiration_timestamp: number | null transport_endpoints: string[] + /** Legacy field; protocol-1.0 invoices may return null. */ + proxy_recipient_id?: string | null + public_key: string | null + encryption: 'Required' | 'Optional' | 'Unsupported' +} + +/** Invoice-first send; Rust validates the complete proxy 1.0 recipient context. */ +export interface SendRgbInvoiceRequest { + invoice: string + asset_id: string + amount: ExactInteger + witness_data?: { amount_sat: ExactInteger; blinding?: ExactInteger | null } | null + donation: boolean + fee_rate: ExactInteger + min_confirmations: number +} + +export interface SendRgbResponse { + txid: string + batch_transfer_idx: number } export interface BtcSendRequest { @@ -313,6 +333,7 @@ export class SdkNode { rgbInvoice(request: JsonRequest): JsonObject decodeRgbInvoice(invoice: string): DecodedRgbInvoice sendRgb(request: JsonRequest): JsonValue + sendRgbInvoice(request: SendRgbInvoiceRequest): SendRgbResponse /** Validated metadata import for an already accepted transfer; does not accept ownership. */ importRgbTransferConsignment(request: ImportRgbTransferConsignmentRequest): ImportRgbResponse /** Validated public contract metadata import; does not create allocations or a balance. */ @@ -362,6 +383,7 @@ export interface RuntimeInfo { readonly rln_version: '0.17.0-beta.3' readonly rln_commit: string readonly lightning_commit: string + readonly rgb_lib_commit: string readonly adapter_sha256: string readonly wrapper_sha256: string readonly lock_sha256: string diff --git a/index.js b/index.js index d33d9f4..50f020b 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,7 @@ const os = require('os') const path = require('path') const fs = require('fs') -const { parse, stringify, bip322Request, paymentRequest, burnRequest, unsupported, UnsupportedCapabilityError } = require('./json-boundary') +const { parse, stringify, bip322Request, paymentRequest, burnRequest, sendRgbInvoiceRequest, unsupported, UnsupportedCapabilityError } = require('./json-boundary') // ─── 1. Native addon resolution ────────────────────────────────────────── @@ -57,6 +57,7 @@ for (const [key, expected] of Object.entries({ rln_version: nativeConfig.rlnVersion, rln_commit: expectedIdentity.commit, lightning_commit: expectedIdentity.lightningCommit, + rgb_lib_commit: expectedIdentity.rgbLibCommit, adapter_sha256: expectedIdentity.patchSha256, wrapper_sha256: expectedIdentity.wrapperSha256, lock_sha256: expectedIdentity.lockSha256, @@ -298,6 +299,10 @@ class SdkNode { return parse(this._inner.sendRgb(stringify(request))) } + sendRgbInvoice (request) { + return parse(this._inner.sendRgbInvoice(sendRgbInvoiceRequest(request))) + } + importRgbTransferConsignment (request) { return parse(this._inner.importRgbTransferConsignment(stringify(request))) } diff --git a/json-boundary.js b/json-boundary.js index 6b8ee68..8e9e795 100644 --- a/json-boundary.js +++ b/json-boundary.js @@ -111,4 +111,55 @@ function bip322Request (request) { return stringify(request) } -module.exports = { bip322Request, parse, stringify, paymentRequest, burnRequest, unsupported, UnsupportedCapabilityError } +// Keep the invoice intact: proxy 1.0 recipient keys, encryption and expiry must +// be parsed and validated together by Rust, never reconstructed from an address. +function sendRgbInvoiceRequest (request) { + const object = (value, allowed, field) => { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new TypeError(`${field} must be an object`) + } + if (Object.keys(value).some(key => !allowed.includes(key))) { + throw new TypeError(`Unknown ${field} field`) + } + } + object(request, ['invoice', 'asset_id', 'amount', 'witness_data', 'donation', 'fee_rate', 'min_confirmations'], 'sendRgbInvoice') + const { invoice, asset_id: assetId, amount, witness_data: witness, donation, fee_rate: feeRate, min_confirmations: confirmations } = request + if (typeof invoice !== 'string' || invoice.length === 0 || invoice.length > 16384 || /[\u0000-\u0020\u007f]/.test(invoice)) { + throw new TypeError('sendRgbInvoice.invoice must be a bounded RGB invoice without whitespace or control characters') + } + if (typeof assetId !== 'string' || assetId.length === 0 || assetId.length > 512) { + throw new TypeError('sendRgbInvoice.asset_id must be a bounded non-empty string') + } + if (typeof donation !== 'boolean') throw new TypeError('sendRgbInvoice.donation must be a boolean') + if (!Number.isInteger(confirmations) || confirmations < 0 || confirmations > 255) { + throw new RangeError('sendRgbInvoice.min_confirmations must be an unsigned 8-bit integer') + } + const uint64 = (value, field, positive = false) => { + checkedNumber(value) + if (!((typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) || + (typeof value === 'string' && /^(0|[1-9][0-9]*)$/.test(value))) || + BigInt(value) > 18446744073709551615n || (positive && BigInt(value) === 0n)) { + throw new RangeError(`sendRgbInvoice.${field} must be an exact ${positive ? 'positive' : 'unsigned'} 64-bit integer`) + } + return new LosslessNumber(String(value)) + } + let witnessData = null + if (witness != null) { + object(witness, ['amount_sat', 'blinding'], 'sendRgbInvoice.witness_data') + witnessData = { + amount_sat: uint64(witness.amount_sat, 'witness_data.amount_sat', true), + blinding: witness.blinding == null ? null : uint64(witness.blinding, 'witness_data.blinding') + } + } + return stringifyLossless({ + invoice, + asset_id: assetId, + amount: uint64(amount, 'amount', true), + witness_data: witnessData, + donation, + fee_rate: uint64(feeRate, 'fee_rate', true), + min_confirmations: confirmations + }) +} + +module.exports = { bip322Request, parse, stringify, paymentRequest, burnRequest, sendRgbInvoiceRequest, unsupported, UnsupportedCapabilityError } diff --git a/package-lock.json b/package-lock.json index 3eaf693..03e48c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@utexo/rgb-lightning-node-nodejs", - "version": "0.2.0-beta.5", + "version": "0.2.0-bfa.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@utexo/rgb-lightning-node-nodejs", - "version": "0.2.0-beta.5", + "version": "0.2.0-bfa.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -645,9 +645,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -665,9 +662,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -685,9 +679,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -705,9 +696,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -725,9 +713,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -745,9 +730,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -765,9 +747,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -983,9 +962,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1003,9 +979,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1023,9 +996,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1043,9 +1013,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1063,9 +1030,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1083,9 +1047,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1300,9 +1261,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1320,9 +1278,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1340,9 +1295,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1360,9 +1312,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 54903c1..39b00a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@utexo/rgb-lightning-node-nodejs", - "version": "0.2.0-beta.5", + "version": "0.2.0-bfa.0", "description": "Node.js (napi-rs) bindings for rgb-lightning-node", "keywords": [ "rgb", @@ -31,7 +31,8 @@ "SIGNER-LIFECYCLE.md", "NATIVE-UPGRADE-20261015.md", "BIP322.md", - "VSS-SAFETY.md" + "VSS-SAFETY.md", + "BFA-MIGRATION.md" ], "scripts": { "prepublishOnly": "node scripts/verify-release-artifacts.js", @@ -59,14 +60,16 @@ "author": "UTEXO", "utexoNativeOverlay": { "repository": "https://github.com/UTEXO-Protocol/rgb-lightning-node.git", - "ref": "ecc69f20308231400561f6f060fea88acfa40cc4", - "commit": "ecc69f20308231400561f6f060fea88acfa40cc4", - "lightningCommit": "79b9eb5166546c62692d3a672986f385c7fe047d", + "ref": "cd3be114a6d0e41af8ce3f29719d210e105234dc", + "commit": "cd3be114a6d0e41af8ce3f29719d210e105234dc", + "lightningCommit": "510189d5700a6d1406229ce7523e181c3838715e", "patch": "patches/release-adapter-v0.17.0-beta.3.patch", - "patchSha256": "6d46717b80ffba8ed8af2de023a536af242aadcacbbb799fb9bb89f8933e4ca5", - "cffiLockSha256": "5fcdc9ad9179d3230e48973cdfa514a863d13a837752027febf8ea04713bbeb5", + "patchSha256": "3662f1e7d97f5bb540b1e6cb0bce98a2e0b243868417e481bcd0c27e15819702", + "cffiLockSha256": "460728009d9c1db3abf9a2f2ad4d76f777589b55eff96ff7be1f3c4d301dab30", "rustToolchain": "1.94.0", - "rlnVersion": "0.17.0-beta.3" + "rlnVersion": "0.17.0-beta.3", + "rgbLibRepository": "https://github.com/UTEXO-Protocol/rgb-lib-bfa.git", + "rgbLibCommit": "962730a211f0f7569d7ad8ae777052f433d87655" }, "dependencies": { "lossless-json": "4.3.1" diff --git a/patches/release-adapter-v0.17.0-beta.3.patch b/patches/release-adapter-v0.17.0-beta.3.patch index c7585bb..5bc97cc 100644 --- a/patches/release-adapter-v0.17.0-beta.3.patch +++ b/patches/release-adapter-v0.17.0-beta.3.patch @@ -1,100 +1,270 @@ diff --git a/bindings/c-ffi/Cargo.lock b/bindings/c-ffi/Cargo.lock -index be5eede..f880358 100644 +index be5eede..5fda28c 100644 --- a/bindings/c-ffi/Cargo.lock +++ b/bindings/c-ffi/Cargo.lock -@@ -501,9 +501,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +@@ -95,8 +95,6 @@ dependencies = [ + "amplify_syn", + "ascii", + "rand 0.8.7", +- "serde", +- "stringly_conversions", + "wasm-bindgen", + ] + +@@ -129,7 +127,6 @@ version = "0.5.4" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "afed304556696656d2d71495e1e5f2c4b524a3fb6eb0f2f3778ffc482a40b8a8" + dependencies = [ +- "serde", + "wasm-bindgen", + ] + +@@ -400,9 +397,6 @@ name = "ascii" + version = "1.1.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +-dependencies = [ +- "serde", +-] + + [[package]] + name = "askama" +@@ -679,6 +673,12 @@ version = "0.22.1" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ++[[package]] ++name = "base64" ++version = "0.23.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" ++ + [[package]] + name = "base64ct" + version = "1.8.3" +@@ -715,6 +715,12 @@ dependencies = [ + "serde", + ] + ++[[package]] ++name = "bdk_coin_select" ++version = "0.4.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "43064fa3dd1d3a8b24079cfcb5ede6b785857edc782277f17a1736511ccc1916" ++ [[package]] - name = "aws-lc-rs" --version = "1.17.3" -+version = "1.18.0" + name = "bdk_core" + version = "0.6.3" +@@ -747,24 +753,25 @@ dependencies = [ + ] + + [[package]] +-name = "bdk_file_store" +-version = "0.22.0" ++name = "bdk_tx" ++version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" --checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" -+checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +-checksum = "c7b9a98edb58d02b601239832674a9ca9f1465e08e1dd1fac3a0e34004372406" ++checksum = "40406c9952f15ed30893826bd83b7d67b6bf11d56574c1efc3db3a5d70cdfcea" dependencies = [ - "aws-lc-sys", - "zeroize", -@@ -511,9 +511,9 @@ dependencies = [ +- "bdk_core", +- "bincode", +- "serde", ++ "bdk_coin_select", ++ "miniscript", ++ "rand 0.8.7", ++ "rand_core 0.6.4", + ] [[package]] - name = "aws-lc-sys" --version = "0.43.0" -+version = "0.44.0" + name = "bdk_wallet" +-version = "3.1.0" ++version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" --checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" -+checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +-checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" ++checksum = "163ec032e0a5a795ded2a7e47fc40a887c75743326483147e4465159c53abdc4" dependencies = [ - "cc", - "cmake", -@@ -998,8 +998,8 @@ version = "0.3.7" + "bdk_chain", +- "bdk_file_store", ++ "bdk_tx", + "bip39", + "bitcoin", + "miniscript", +@@ -829,7 +836,7 @@ dependencies = [ + "biscuit-parser", + "biscuit-quote", + "ecdsa", +- "ed25519-dalek", ++ "ed25519-dalek 2.2.0", + "elliptic-curve", + "getrandom 0.2.17", + "hex", +@@ -1110,9 +1117,9 @@ dependencies = [ + + [[package]] + name = "bumpalo" +-version = "3.20.2" ++version = "3.20.3" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" ++checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + + [[package]] + name = "bytecheck" +@@ -1150,9 +1157,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + + [[package]] + name = "camino" +-version = "1.2.4" ++version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" - checksum = "b65c2e1ab98050c93cc9ada070d5070e014329c65196d03f6f8f1f75c2666f37" +-checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" ++checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ -- "rustls 0.23.42", -- "rustls-webpki 0.103.13", -+ "rustls 0.23.45", -+ "rustls-webpki 0.103.15", - "tokio", - "tokio-rustls", - "webpki-roots 1.0.9", -@@ -1939,7 +1939,7 @@ dependencies = [ - "byteorder", - "libc", - "log", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "serde", - "serde_json", - "webpki-roots 0.25.4", -@@ -1956,7 +1956,7 @@ dependencies = [ - "byteorder", - "libc", - "log", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "serde", - "serde_json", - "webpki-roots 0.25.4", -@@ -2038,7 +2038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" - checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" + "serde_core", + ] +@@ -1583,12 +1590,27 @@ dependencies = [ + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", +- "fiat-crypto", ++ "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", + ] + ++[[package]] ++name = "curve25519-dalek" ++version = "5.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" ++dependencies = [ ++ "cfg-if", ++ "cpufeatures 0.3.0", ++ "curve25519-dalek-derive", ++ "digest 0.11.3", ++ "fiat-crypto 0.3.0", ++ "rustc_version", ++ "subtle", ++] ++ + [[package]] + name = "curve25519-dalek-derive" + version = "0.1.1" +@@ -1891,7 +1913,7 @@ dependencies = [ + "elliptic-curve", + "rfc6979", + "serdect", +- "signature", ++ "signature 2.2.0", + "spki 0.7.3", + ] + +@@ -1902,7 +1924,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "libc", -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", + "pkcs8 0.10.2", +- "signature", ++ "signature 2.2.0", ++] ++ ++[[package]] ++name = "ed25519" ++version = "3.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" ++dependencies = [ ++ "signature 3.0.0", + ] + + [[package]] +@@ -1911,8 +1942,8 @@ version = "2.2.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" + dependencies = [ +- "curve25519-dalek", +- "ed25519", ++ "curve25519-dalek 4.1.3", ++ "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", +@@ -1920,6 +1951,18 @@ dependencies = [ + "zeroize", ] ++[[package]] ++name = "ed25519-dalek" ++version = "3.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" ++dependencies = [ ++ "curve25519-dalek 5.0.0", ++ "ed25519 3.0.0", ++ "sha2 0.11.0", ++ "subtle", ++] ++ + [[package]] + name = "either" + version = "1.17.0" +@@ -2114,6 +2157,12 @@ version = "0.2.9" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + ++[[package]] ++name = "fiat-crypto" ++version = "0.3.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" ++ [[package]] -@@ -2420,9 +2420,9 @@ dependencies = [ + name = "file-format" + version = "0.29.0" +@@ -3084,13 +3133,12 @@ dependencies = [ [[package]] - name = "h2" --version = "0.4.15" -+version = "0.4.16" + name = "js-sys" +-version = "0.3.98" ++version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" --checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" -+checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +-checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" ++checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "atomic-waker", - "bytes", -@@ -2710,7 +2710,7 @@ dependencies = [ - "http", - "hyper", - "hyper-util", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "tokio", - "tokio-rustls", - "tower-service", -@@ -2954,7 +2954,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" + "cfg-if", + "futures-util", +- "once_cell", + "wasm-bindgen", + ] + +@@ -3174,14 +3222,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + + [[package]] + name = "libredox" +-version = "0.1.16" ++version = "0.1.18" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" ++checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "hermit-abi", + "bitflags 2.13.1", "libc", -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", + "plain", +- "redox_syscall 0.7.5", ++ "redox_syscall 0.9.0", ] [[package]] -@@ -3356,14 +3356,12 @@ dependencies = [ +@@ -3231,6 +3279,7 @@ dependencies = [ + "rgb-lib", + "rgb-strict-encoding", + "serde", ++ "serde_json", + "tokio", + ] + +@@ -3356,14 +3405,12 @@ dependencies = [ [[package]] name = "lightning-transaction-sync" version = "0.2.1" @@ -110,193 +280,607 @@ index be5eede..f880358 100644 ] [[package]] -@@ -3641,7 +3639,7 @@ version = "0.50.3" +@@ -3626,15 +3673,6 @@ dependencies = [ + "minimal-lexical", + ] + +-[[package]] +-name = "nonasync" +-version = "0.1.3" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "d81f7335a3fa37124e24461d6164485760d4c056dd92a81a70c23fc28c2b63c8" +-dependencies = [ +- "amplify", +-] +- + [[package]] + name = "nu-ansi-term" + version = "0.50.3" +@@ -3743,9 +3781,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + + [[package]] + name = "openssl" +-version = "0.10.80" ++version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" - checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +-checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" ++checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", - ] + "bitflags 2.13.1", + "cfg-if", +@@ -3783,9 +3821,9 @@ dependencies = [ [[package]] -@@ -4326,7 +4324,7 @@ dependencies = [ - "quinn-proto", - "quinn-udp", - "rustc-hash", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "socket2", - "thiserror 2.0.19", - "tokio", -@@ -4348,7 +4346,7 @@ dependencies = [ - "rand_pcg", - "ring", - "rustc-hash", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "rustls-pki-types", - "slab", - "thiserror 2.0.19", -@@ -4368,7 +4366,7 @@ dependencies = [ - "once_cell", - "socket2", - "tracing", -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", + name = "openssl-sys" +-version = "0.9.116" ++version = "0.9.117" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" ++checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" + dependencies = [ + "cc", + "libc", +@@ -4059,9 +4097,9 @@ dependencies = [ + + [[package]] + name = "portable-atomic" +-version = "1.13.1" ++version = "1.14.0" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" ++checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + + [[package]] + name = "portable-atomic-util" +@@ -4512,9 +4550,9 @@ dependencies = [ + + [[package]] + name = "redox_syscall" +-version = "0.7.5" ++version = "0.9.0" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" ++checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" + dependencies = [ + "bitflags 2.13.1", ] +@@ -4682,16 +4720,14 @@ dependencies = [ [[package]] -@@ -4652,7 +4650,7 @@ dependencies = [ - "percent-encoding", - "pin-project-lite", - "quinn", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", -@@ -4780,7 +4778,7 @@ dependencies = [ - "rgb-schemas", + name = "rgb-aluvm" +-version = "0.11.1-rc.4" ++version = "0.11.2-rc.1" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "dcf4d92478fcff567ff0bd9f18684fd954f8426c74f16e5517254b0a2d34023a" ++checksum = "21de8dc75b4ce90dffe66cff585053e9f5d2abe6436589c95e6308f0605195f4" + dependencies = [ + "amplify", + "baid64", + "blake3", +- "getrandom 0.3.4", + "half", +- "paste", + "rgb-ascii-armor", + "rgb-strict-encoding", + "rgb-strict-types", +@@ -4703,9 +4739,9 @@ dependencies = [ + + [[package]] + name = "rgb-ascii-armor" +-version = "1.0.2" ++version = "1.0.5" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "fcde5a129540e5911c24930fd854ea658a715b50a690d254273088f907a63516" ++checksum = "0b3a739537822793adce1f9539bb3fae3d54a16f409e8353ac85281bdfcec078" + dependencies = [ + "amplify", + "baid64", +@@ -4716,8 +4752,9 @@ dependencies = [ + + [[package]] + name = "rgb-consensus" +-version = "0.11.1-rc.11" +-source = "git+https://github.com/UTEXO-Protocol/rgb-consensus-s-bfa?rev=2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757#2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757" ++version = "0.11.2-rc.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "cd5d6c21f60f24976d8b983b6b5925cd87a4865af8ad6d965d45360518bbfd24" + dependencies = [ + "amplify", + "baid64", +@@ -4730,11 +4767,11 @@ dependencies = [ + "hex-conservative 1.2.0", + "rand 0.9.5", + "rgb-aluvm", ++ "rgb-ascii-armor", "rgb-strict-encoding", "rgb-strict-types", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "scrypt 0.12.0", - "sea-orm 2.0.2", + "ripemd", +- "secp256k1 0.31.1", +- "secp256k1 0.32.0-beta.2", ++ "secp256k1 0.33.1", + "serde", + "sha2 0.11.0", + "wasm-bindgen", +@@ -4742,8 +4779,9 @@ dependencies = [ + + [[package]] + name = "rgb-invoicing" +-version = "0.11.1-rc.11" +-source = "git+https://github.com/UTEXO-Protocol/rgb-ops-s-bfa?rev=773766b5ca7a9e705ec68c2f9ed0917f505cd6e3#773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" ++version = "0.11.2-rc.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "76692ae2017e3b4789d2539dcf483b5e5e9f4468030d9ffb619aabeab7cd43b4" + dependencies = [ + "amplify", + "baid64", +@@ -4760,17 +4798,20 @@ dependencies = [ + [[package]] + name = "rgb-lib" + version = "0.3.0-beta.7" +-source = "git+https://github.com/UTEXO-Protocol/rgb-lib.git?tag=v0.3.0-beta.50-bfa#1b951af0c8233308c919975552487be19ad2a930" ++source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655" + dependencies = [ + "amplify", +- "base64 0.22.1", ++ "base64 0.23.1", + "bdk_electrum", + "bdk_esplora", + "bdk_wallet", + "chacha20poly1305 0.11.0", ++ "ed25519-dalek 3.0.0", + "file-format", + "hex", +- "hkdf 0.12.4", ++ "hkdf 0.13.0", ++ "hmac 0.13.0", ++ "libc", + "rand 0.10.2", + "reqwest 0.13.4", + "rgb-invoicing", +@@ -4786,7 +4827,7 @@ dependencies = [ "sea-query 1.0.2", -@@ -4850,7 +4848,7 @@ dependencies = [ - "rgb-lib", - "rgb-psbt-utils", - "rln-migration", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "scrypt 0.11.0", - "sea-orm 1.1.20", "serde", -@@ -5127,7 +5125,7 @@ dependencies = [ - "errno", - "libc", - "linux-raw-sys 0.12.1", -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", + "serde_json", +- "sha2 0.10.9", ++ "sha2 0.11.0", + "slog", + "slog-async", + "slog-term", +@@ -4795,15 +4836,15 @@ dependencies = [ + "time", + "tokio", + "url", +- "vss-client-ng", + "walkdir", ++ "x25519-dalek", + "zip 8.6.0", ] [[package]] -@@ -5144,16 +5142,16 @@ dependencies = [ + name = "rgb-lib-migration" + version = "0.3.0-beta.5" +-source = "git+https://github.com/UTEXO-Protocol/rgb-lib.git?tag=v0.3.0-beta.50-bfa#1b951af0c8233308c919975552487be19ad2a930" ++source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655" + dependencies = [ + "sea-orm-migration 2.0.2", + "tokio", +@@ -4831,7 +4872,9 @@ dependencies = [ + "electrum-client 0.24.1", + "esplora-client", + "futures", ++ "hex", + "hex-conservative 0.3.2", ++ "hkdf 0.12.4", + "libc", + "lightning 0.2.2", + "lightning-background-processor", +@@ -4855,6 +4898,7 @@ dependencies = [ + "sea-orm 1.1.20", + "serde", + "serde_json", ++ "sha2 0.10.9", + "signer-external", + "tempfile", + "thiserror 2.0.19", +@@ -4880,8 +4924,9 @@ dependencies = [ + + [[package]] + name = "rgb-ops" +-version = "0.11.1-rc.11" +-source = "git+https://github.com/UTEXO-Protocol/rgb-ops-s-bfa?rev=773766b5ca7a9e705ec68c2f9ed0917f505cd6e3#773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" ++version = "0.11.2-rc.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a0b0b332a32be0e41ce328d534d559e67a0add6381d250bdc188e44eb8777ca7" + dependencies = [ + "amplify", + "baid64", +@@ -4889,8 +4934,6 @@ dependencies = [ + "electrum-client 0.25.0", + "esplora-client", + "getrandom 0.3.4", +- "nonasync", +- "rand 0.9.5", + "rgb-aluvm", + "rgb-ascii-armor", + "rgb-consensus", +@@ -4904,9 +4947,9 @@ dependencies = [ [[package]] - name = "rustls" --version = "0.23.42" -+version = "0.23.45" + name = "rgb-psbt-utils" +-version = "0.11.1-rc.11" ++version = "0.11.2-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" --checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" -+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +-checksum = "9df8f48e7c851814306af91411d7d6f7e8571939d509d4a786597a5eba0efbbf" ++checksum = "16167fb6520c432702b42dbc6344c94573ff8873082386c129718a66bcb535ce" dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", -- "rustls-webpki 0.103.13", -+ "rustls-webpki 0.103.15", + "amplify", + "getrandom 0.3.4", +@@ -4918,8 +4961,9 @@ dependencies = [ + + [[package]] + name = "rgb-schemas" +-version = "0.11.1-rc.11" +-source = "git+https://github.com/UTEXO-Protocol/rgb-schemas-s-bfa?rev=238dfe7a89db998107fd87d919bf2757a2746d7f#238dfe7a89db998107fd87d919bf2757a2746d7f" ++version = "0.11.2-rc.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "480f3d71e0f1b23b3c75b59255d62de178259e074f4a74410c330c734f52246f" + dependencies = [ + "amplify", + "rgb-aluvm", +@@ -4929,9 +4973,9 @@ dependencies = [ + + [[package]] + name = "rgb-strict-encoding" +-version = "1.0.2" ++version = "1.0.5" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "0f68326e14d4b627f86634ea324510135cb551e27c5583a74811fa230b0cf34d" ++checksum = "43cf06a3afea7b29f92f279a5fabb39215d1287f796a8d1a75ce77dd4f807fd4" + dependencies = [ + "amplify", + "bitcoin", +@@ -4942,9 +4986,9 @@ dependencies = [ + + [[package]] + name = "rgb-strict-encoding-derive" +-version = "1.0.2" ++version = "1.0.5" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "33367536cfef03cb582cd9f8f25e193c2298d3ec3112eb59018a376b465f1ed9" ++checksum = "473d4bc563f8db593abf7213f82fafc5bc667125f2c39222cd66ead369e60421" + dependencies = [ + "amplify_syn", + "heck 0.5.0", +@@ -4955,9 +4999,9 @@ dependencies = [ + + [[package]] + name = "rgb-strict-types" +-version = "1.0.2" ++version = "1.0.5" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "e920bdf61b662a3743572a03696b0d88992eeb7d4f7afa0902915968327b3ce6" ++checksum = "5efea5be04a66a22f96580fb8c7fd9558f7c5d74039c096bb024e4f325f4529a" + dependencies = [ + "amplify", + "baid64", +@@ -5060,7 +5104,7 @@ dependencies = [ + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", +- "signature", ++ "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", +@@ -5674,24 +5718,12 @@ dependencies = [ + + [[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.5", +- "secp256k1-sys 0.11.0", +- "serde", +-] +- +-[[package]] +-name = "secp256k1" +-version = "0.32.0-beta.2" ++version = "0.33.1" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "3c5fdc7d6e800869d3fd60ff857c479bf0a83ea7bf44b389e64461e844204994" ++checksum = "d7f404a8dab7a7a5a631e741d8699aa9c8e1d689fc26ccf897c7187565490b69" + dependencies = [ + "rand 0.9.5", +- "secp256k1-sys 0.12.0", ++ "secp256k1-sys 0.14.1", + "serde", ] -@@ -5191,14 +5189,14 @@ dependencies = [ - "jni", - "log", - "once_cell", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "rustls-native-certs", - "rustls-platform-verifier-android", -- "rustls-webpki 0.103.13", -+ "rustls-webpki 0.103.15", - "security-framework", - "security-framework-sys", - "webpki-root-certs", -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", + +@@ -5706,18 +5738,9 @@ dependencies = [ + + [[package]] + name = "secp256k1-sys" +-version = "0.11.0" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +-dependencies = [ +- "cc", +-] +- +-[[package]] +-name = "secp256k1-sys" +-version = "0.12.0" ++version = "0.14.1" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "6d3be00697c88c00fe102af8dc316038cc2062eab8da646e7463f4c0e70ca9fd" ++checksum = "f6b2992d4a3cd244539a7d5d0966aadbe5ca7fb868a5d7e38c29499b9e709bbd" + dependencies = [ + "cc", + ] +@@ -5840,16 +5863,6 @@ dependencies = [ + "serde_core", + ] + +-[[package]] +-name = "serde_str_helpers" +-version = "0.1.2" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "b744a7c94f2f3785496af33a0d93857dfc0c521e25c38e993e9c5bb45f09c841" +-dependencies = [ +- "serde", +- "serde_derive", +-] +- + [[package]] + name = "serde_urlencoded" + version = "0.7.1" +@@ -6005,10 +6018,16 @@ dependencies = [ + "rand_core 0.6.4", ] ++[[package]] ++name = "signature" ++version = "3.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" ++ [[package]] -@@ -5219,9 +5217,9 @@ dependencies = [ + name = "signer-external" + version = "0.1.0-alpha" +-source = "git+https://github.com/UTEXO-Protocol/rln-external-signer.git?branch=main#0fb005ec4b927ddbe13e1646d247b5bb11e8ffed" ++source = "git+https://github.com/UTEXO-Protocol/rln-external-signer.git?rev=0fb005ec4b927ddbe13e1646d247b5bb11e8ffed#0fb005ec4b927ddbe13e1646d247b5bb11e8ffed" + dependencies = [ + "base64 0.22.1", + "bitcoin", +@@ -6028,9 +6047,9 @@ dependencies = [ [[package]] - name = "rustls-webpki" --version = "0.103.13" -+version = "0.103.15" + name = "simd_cesu8" +-version = "1.1.1" ++version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" --checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +-checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" ++checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ - "aws-lc-rs", - "ring", -@@ -6195,7 +6193,7 @@ dependencies = [ - "memchr", - "once_cell", - "percent-encoding", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "serde", - "serde_json", - "sha2 0.10.9", -@@ -6233,7 +6231,7 @@ dependencies = [ - "memchr", - "percent-encoding", - "rust_decimal", -- "rustls 0.23.42", -+ "rustls 0.23.45", - "serde", - "serde_json", - "sha2 0.10.9", -@@ -6682,10 +6680,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" - checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" + "rustc_version", + "simdutf8", +@@ -6538,16 +6557,6 @@ version = "1.1.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +-[[package]] +-name = "stringly_conversions" +-version = "0.1.1" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "ff63080f492dd4d289ffcaed8d7ece38adfb423db910eb342c0e04d409536a7a" +-dependencies = [ +- "paste", +- "serde_str_helpers", +-] +- + [[package]] + name = "stringprep" + version = "0.1.5" +@@ -6977,16 +6986,16 @@ dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", +- "winnow 1.0.3", ++ "winnow 1.0.4", + ] + + [[package]] + name = "toml_parser" +-version = "1.1.2+spec-1.1.0" ++version = "1.1.3+spec-1.1.0" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" ++checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" + dependencies = [ +- "winnow 1.0.3", ++ "winnow 1.0.4", + ] + + [[package]] +@@ -7421,7 +7430,7 @@ dependencies = [ + [[package]] + name = "vls-core" + version = "0.14.0" +-source = "git+https://github.com/UTEXO-Protocol/vls-core.git?branch=feat%2Frgb-compatibility#45c72edfd58620849eb486925439a76526b415ae" ++source = "git+https://github.com/UTEXO-Protocol/vls-core.git?rev=45c72edfd58620849eb486925439a76526b415ae#45c72edfd58620849eb486925439a76526b415ae" + dependencies = [ + "ahash 0.8.12", + "anyhow", +@@ -7511,7 +7520,7 @@ dependencies = [ + [[package]] + name = "vls-protocol-signer" + version = "0.14.0" +-source = "git+https://github.com/UTEXO-Protocol/vls-protocol-signer.git?branch=feat%2Frgb-compatibility#dddd336fcdf84c4febdd7af8363bda8d1721d893" ++source = "git+https://github.com/UTEXO-Protocol/vls-protocol-signer.git?rev=dddd336fcdf84c4febdd7af8363bda8d1721d893#dddd336fcdf84c4febdd7af8363bda8d1721d893" + dependencies = [ + "bit-vec", + "log", +@@ -7565,9 +7574,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + + [[package]] + name = "wasip2" +-version = "1.0.3+wasi-0.2.9" ++version = "1.0.4+wasi-0.2.12" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" ++checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" + dependencies = [ + "wit-bindgen", + ] +@@ -7580,9 +7589,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + + [[package]] + name = "wasm-bindgen" +-version = "0.2.121" ++version = "0.2.126" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" ++checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ - "fastrand", -- "getrandom 0.4.3", -+ "getrandom 0.3.4", + "cfg-if", "once_cell", - "rustix 1.1.4", -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", +@@ -7594,9 +7603,9 @@ dependencies = [ + + [[package]] + name = "wasm-bindgen-futures" +-version = "0.4.71" ++version = "0.4.76" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" ++checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" + dependencies = [ + "js-sys", + "wasm-bindgen", +@@ -7604,9 +7613,9 @@ dependencies = [ + + [[package]] + name = "wasm-bindgen-macro" +-version = "0.2.121" ++version = "0.2.126" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" ++checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" + dependencies = [ + "quote", + "wasm-bindgen-macro-support", +@@ -7614,9 +7623,9 @@ dependencies = [ + + [[package]] + name = "wasm-bindgen-macro-support" +-version = "0.2.121" ++version = "0.2.126" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" ++checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" + dependencies = [ + "bumpalo", + "proc-macro2", +@@ -7627,18 +7636,18 @@ dependencies = [ + + [[package]] + name = "wasm-bindgen-shared" +-version = "0.2.121" ++version = "0.2.126" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" ++checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" + dependencies = [ + "unicode-ident", ] [[package]] -@@ -6694,7 +6692,7 @@ version = "1.2.1" + name = "web-sys" +-version = "0.3.98" ++version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" - checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +-checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" ++checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ -- "windows-sys 0.61.2", -+ "windows-sys 0.59.0", + "js-sys", + "wasm-bindgen", +@@ -7656,9 +7665,9 @@ dependencies = [ + + [[package]] + name = "webpki-root-certs" +-version = "1.0.7" ++version = "1.0.9" + source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" ++checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" + dependencies = [ + "rustls-pki-types", + ] +@@ -7746,7 +7755,7 @@ version = "0.1.11" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" + dependencies = [ +- "windows-sys 0.48.0", ++ "windows-sys 0.61.2", ] [[package]] -@@ -6862,7 +6860,7 @@ version = "0.26.4" +@@ -7993,9 +8002,9 @@ dependencies = [ + + [[package]] + name = "winnow" +-version = "1.0.3" ++version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" - checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +-checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" ++checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ -- "rustls 0.23.42", -+ "rustls 0.23.45", - "tokio", + "memchr", + ] +@@ -8021,6 +8030,16 @@ dependencies = [ + "tap", ] ++[[package]] ++name = "x25519-dalek" ++version = "3.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" ++dependencies = [ ++ "curve25519-dalek 5.0.0", ++ "rand_core 0.10.1", ++] ++ + [[package]] + name = "yansi" + version = "1.0.1" diff --git a/bindings/c-ffi/Cargo.toml b/bindings/c-ffi/Cargo.toml -index 546e27d..d2807dc 100644 +index 546e27d..7ad7229 100644 --- a/bindings/c-ffi/Cargo.toml +++ b/bindings/c-ffi/Cargo.toml -@@ -55,6 +55,7 @@ lightning-invoice = { path = "../../rust-lightning/lightning-invoice" } +@@ -55,11 +55,20 @@ lightning-invoice = { path = "../../rust-lightning/lightning-invoice" } lightning-net-tokio = { path = "../../rust-lightning/lightning-net-tokio" } lightning-persister = { path = "../../rust-lightning/lightning-persister" } lightning-rapid-gossip-sync = { path = "../../rust-lightning/lightning-rapid-gossip-sync" } +-vls-core = { git = "https://github.com/UTEXO-Protocol/vls-core.git", branch = "feat/rgb-compatibility" } +-vls-protocol-signer = { git = "https://github.com/UTEXO-Protocol/vls-protocol-signer.git", branch = "feat/rgb-compatibility" } +-# BFA-enabled RGB crates required by the parent's rgb-lib revision; keep in lock-step with the +-# parent's pins. +-rgb-consensus = { git = "https://github.com/UTEXO-Protocol/rgb-consensus-s-bfa", rev = "2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757" } +-rgb-ops = { git = "https://github.com/UTEXO-Protocol/rgb-ops-s-bfa", rev = "773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" } +-rgb-invoicing = { git = "https://github.com/UTEXO-Protocol/rgb-ops-s-bfa", rev = "773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" } +-rgb-schemas = { git = "https://github.com/UTEXO-Protocol/rgb-schemas-s-bfa", rev = "238dfe7a89db998107fd87d919bf2757a2746d7f" } +lightning-transaction-sync = { path = "../../rust-lightning/lightning-transaction-sync" } - vls-core = { git = "https://github.com/UTEXO-Protocol/vls-core.git", branch = "feat/rgb-compatibility" } - vls-protocol-signer = { git = "https://github.com/UTEXO-Protocol/vls-protocol-signer.git", branch = "feat/rgb-compatibility" } - # BFA-enabled RGB crates required by the parent's rgb-lib revision; keep in lock-step with the -@@ -63,3 +64,12 @@ rgb-consensus = { git = "https://github.com/UTEXO-Protocol/rgb-consensus-s-bfa", - rgb-ops = { git = "https://github.com/UTEXO-Protocol/rgb-ops-s-bfa", rev = "773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" } - rgb-invoicing = { git = "https://github.com/UTEXO-Protocol/rgb-ops-s-bfa", rev = "773766b5ca7a9e705ec68c2f9ed0917f505cd6e3" } - rgb-schemas = { git = "https://github.com/UTEXO-Protocol/rgb-schemas-s-bfa", rev = "238dfe7a89db998107fd87d919bf2757a2746d7f" } ++vls-core = { git = "https://github.com/UTEXO-Protocol/vls-core.git", rev = "45c72edfd58620849eb486925439a76526b415ae" } ++vls-protocol-signer = { git = "https://github.com/UTEXO-Protocol/vls-protocol-signer.git", rev = "dddd336fcdf84c4febdd7af8363bda8d1721d893" } ++ ++# The new RGB library owns its complete graph; do not reintroduce the retired s-bfa overrides. ++# This separate workspace must preserve the checked arithmetic required by that graph. ++[profile.release] ++overflow-checks = true + +# Rust 1.94's proc-macro stripping emits LINKEDIT string pools rejected by +# macOS 27. These dependencies execute only during compilation, so this @@ -335,7 +919,7 @@ index f775f83..86daa71 100644 println!("cargo:rerun-if-changed=cbindgen.toml"); diff --git a/bindings/c-ffi/rln.h b/bindings/c-ffi/rln.h -index c8bb08b..eec9243 100644 +index c8bb08b..69493f2 100644 --- a/bindings/c-ffi/rln.h +++ b/bindings/c-ffi/rln.h @@ -42,6 +42,11 @@ struct CResultString rln_asset_link_create(const struct COpaqueStruct *node, @@ -455,8 +1039,13 @@ index c8bb08b..eec9243 100644 struct CResultString rln_sdk_shutdown(void); struct CResultString rln_send_btc(const struct COpaqueStruct *node, const char *request_json); -@@ -260,6 +299,9 @@ struct CResultString rln_send_rgb(const struct COpaqueStruct *node, const char * +@@ -258,8 +297,14 @@ struct CResultString rln_send_payment(const struct COpaqueStruct *node, const ch + struct CResultString rln_send_rgb(const struct COpaqueStruct *node, const char *request_json); + ++struct CResultString rln_send_rgb_invoice(const struct COpaqueStruct *node, ++ const char *request_json); ++ struct CResultString rln_sign_message(const struct COpaqueStruct *node, const char *message); +struct CResultString rln_sign_message_bip322(const struct COpaqueStruct *node, @@ -465,7 +1054,7 @@ index c8bb08b..eec9243 100644 struct CResultString rln_sync(const struct COpaqueStruct *node); struct CResultString rln_taker(const struct COpaqueStruct *node, const char *request_json); -@@ -271,3 +313,8 @@ struct CResultString rln_uniffi_is_initialized(void); +@@ -271,3 +316,8 @@ struct CResultString rln_uniffi_is_initialized(void); struct CResultString rln_verify_message(const struct COpaqueStruct *node, const char *message, const char *signature); @@ -475,7 +1064,7 @@ index c8bb08b..eec9243 100644 + +struct CResultString rln_verify_message_bip322_offline(const char *request_json); diff --git a/bindings/c-ffi/rln.hpp b/bindings/c-ffi/rln.hpp -index 498a7e1..84f528c 100644 +index 498a7e1..655ea84 100644 --- a/bindings/c-ffi/rln.hpp +++ b/bindings/c-ffi/rln.hpp @@ -43,6 +43,9 @@ CResultString rln_asset_link_create(const COpaqueStruct *node, const char *reque @@ -577,8 +1166,12 @@ index 498a7e1..84f528c 100644 CResultString rln_sdk_shutdown(); CResultString rln_send_btc(const COpaqueStruct *node, const char *request_json); -@@ -228,6 +253,8 @@ CResultString rln_send_rgb(const COpaqueStruct *node, const char *request_json); +@@ -226,8 +251,12 @@ CResultString rln_send_payment(const COpaqueStruct *node, const char *request_js + + CResultString rln_send_rgb(const COpaqueStruct *node, const char *request_json); ++CResultString rln_send_rgb_invoice(const COpaqueStruct *node, const char *request_json); ++ CResultString rln_sign_message(const COpaqueStruct *node, const char *message); +CResultString rln_sign_message_bip322(const COpaqueStruct *node, const char *request_json); @@ -586,7 +1179,7 @@ index 498a7e1..84f528c 100644 CResultString rln_sync(const COpaqueStruct *node); CResultString rln_taker(const COpaqueStruct *node, const char *request_json); -@@ -240,4 +267,8 @@ CResultString rln_verify_message(const COpaqueStruct *node, +@@ -240,4 +269,8 @@ CResultString rln_verify_message(const COpaqueStruct *node, const char *message, const char *signature); @@ -596,7 +1189,7 @@ index 498a7e1..84f528c 100644 + } // extern "C" diff --git a/bindings/c-ffi/src/api.rs b/bindings/c-ffi/src/api.rs -index 2b9e873..a0ce34f 100644 +index 2b9e873..69683c0 100644 --- a/bindings/c-ffi/src/api.rs +++ b/bindings/c-ffi/src/api.rs @@ -91,7 +91,7 @@ pub(crate) fn sdk_node_unlock( @@ -651,7 +1244,21 @@ index 2b9e873..a0ce34f 100644 } pub(crate) fn get_payment( -@@ -749,7 +776,7 @@ pub(crate) fn sdk_global_initialize(request_json: *const c_char) -> Result Res + json(JsonSendRgbResponse::from(resp)) + } + ++pub(crate) fn send_rgb_invoice(node: &COpaqueStruct, request_json: *const c_char) -> Result { ++ let node = require_handle(node)?; ++ let req: JsonSendRgbInvoiceRequest = parse_req(request_json)?; ++ let resp = node.sendrgbinvoice(req.try_into()?)?; ++ json(JsonSendRgbResponse::from(resp)) ++} ++ + pub(crate) fn import_rgb_transfer_consignment( + node: &COpaqueStruct, + request_json: *const c_char, +@@ -749,7 +783,7 @@ pub(crate) fn sdk_global_initialize(request_json: *const c_char) -> Result Result { @@ -660,7 +1267,7 @@ index 2b9e873..a0ce34f 100644 ok_void() } -@@ -783,6 +810,34 @@ pub(crate) fn native_external_signer_bootstrap(signer: &COpaqueStruct) -> Result +@@ -783,6 +817,34 @@ pub(crate) fn native_external_signer_bootstrap(signer: &COpaqueStruct) -> Result json(JsonSdkExternalSignerBootstrap::from(bootstrap)) } @@ -695,7 +1302,7 @@ index 2b9e873..a0ce34f 100644 pub(crate) fn sdk_node_init_with_native_external_signer( node: &COpaqueStruct, signer: &COpaqueStruct, -@@ -840,3 +895,62 @@ pub(crate) fn sdk_node_unlock_with_attached_external_signer( +@@ -840,3 +902,62 @@ pub(crate) fn sdk_node_unlock_with_attached_external_signer( node.unlock_with_attached_external_signer_request(r.into())?; ok_void() } @@ -759,9 +1366,24 @@ index 2b9e873..a0ce34f 100644 + )?) +} diff --git a/bindings/c-ffi/src/json_types.rs b/bindings/c-ffi/src/json_types.rs -index 5b606f1..b235b6e 100644 +index 5b606f1..9c9c59e 100644 --- a/bindings/c-ffi/src/json_types.rs +++ b/bindings/c-ffi/src/json_types.rs +@@ -34,10 +34,10 @@ use rgb_lightning_node::{ + SdkRefreshTransfersRequest, SdkRefreshTransfersResponse, SdkRgbInvoiceRequest, + SdkRgbInvoiceResponse, SdkSendBtcRequest, SdkSendBtcResponse, SdkSendOnionMessageRequest, + SdkSendPaymentRequest, SdkSendPaymentResponse, SdkTakerRequest, SdkUnlockRequest, +- SdkVssClearFenceRequest, SendRgbRequest, SendRgbResponse, SignMessageResponse, Swap, SwapList, +- SwapStatus, Token, TokenLight, Transaction, TransactionType, Transfer, +- TransferTransportEndpoint, TransportEndpoint, Txid, Unspent, Utxo, VerifyMessageResponse, +- WitnessData, ++ SdkVssClearFenceRequest, SendRgbInvoiceRequest, SendRgbRequest, SendRgbResponse, ++ SignMessageResponse, Swap, SwapList, SwapStatus, Token, TokenLight, Transaction, ++ TransactionType, Transfer, TransferTransportEndpoint, TransportEndpoint, Txid, Unspent, Utxo, ++ VerifyMessageResponse, WitnessData, + }; + use serde::{Deserialize, Serialize}; + @@ -136,6 +136,8 @@ pub(crate) struct JsonSdkInitRequest { #[serde(default)] pub vss_allow_http: bool, @@ -795,7 +1417,7 @@ index 5b606f1..b235b6e 100644 payment_hash: fmt_payment_hash(&r.payment_hash), payment_secret: r.payment_secret, payee_pubkey: r.payee_pubkey.as_ref().map(fmt_pubkey), -@@ -836,6 +841,45 @@ impl From for JsonDecodeLnInvoiceResponse { +@@ -836,31 +841,147 @@ impl From for JsonDecodeLnInvoiceResponse { } } @@ -841,7 +1463,8 @@ index 5b606f1..b235b6e 100644 #[derive(Debug, Serialize)] pub(crate) struct JsonDecodeRgbInvoiceResponse { pub recipient_id: String, -@@ -843,24 +887,65 @@ pub(crate) struct JsonDecodeRgbInvoiceResponse { +- pub proxy_recipient_id: String, ++ pub proxy_recipient_id: Option, pub recipient_type: String, pub asset_schema: Option, pub asset_id: Option, @@ -850,6 +1473,8 @@ index 5b606f1..b235b6e 100644 pub network: String, pub expiration_timestamp: Option, pub transport_endpoints: Vec, ++ pub public_key: Option, ++ pub encryption: String, } -impl From for JsonDecodeRgbInvoiceResponse { @@ -870,6 +1495,8 @@ index 5b606f1..b235b6e 100644 network: r.network, expiration_timestamp: r.expiration_timestamp, transport_endpoints: r.transport_endpoints, ++ public_key: r.public_key, ++ encryption: r.encryption, + }) + } +} @@ -909,10 +1536,140 @@ index 5b606f1..b235b6e 100644 + "Fungible(1)x", + ] { + assert!(parse_decoded_rgb_assignment(input).is_err(), "{input}"); ++ } ++ } ++ ++ #[test] ++ fn decoded_invoice_preserves_absent_proxy_identity_and_authenticated_fields() { ++ for proxy_recipient_id in [None, Some("proxy-recipient".to_owned())] { ++ let response = DecodeRgbInvoiceResponse { ++ recipient_id: "recipient".into(), ++ proxy_recipient_id: proxy_recipient_id.clone(), ++ recipient_type: "Witness".into(), ++ asset_schema: None, ++ asset_id: None, ++ assignment: "Fungible(100)".into(), ++ network: "regtest".into(), ++ expiration_timestamp: Some(2_000_000_000), ++ transport_endpoints: vec!["rpc://127.0.0.1:3000/json-rpc".into()], ++ public_key: Some("ab".repeat(32)), ++ encryption: "Required".into(), ++ }; ++ let json = ++ serde_json::to_value(JsonDecodeRgbInvoiceResponse::try_from(response).unwrap()) ++ .unwrap(); ++ assert_eq!( ++ json["proxy_recipient_id"], ++ serde_json::json!(proxy_recipient_id) ++ ); ++ assert_eq!(json["public_key"], "ab".repeat(32)); ++ assert_eq!(json["encryption"], "Required"); ++ assert_eq!( ++ json["assignment"], ++ serde_json::json!({"type":"Fungible", "value":100}) ++ ); } } } -@@ -2208,6 +2293,7 @@ impl From for JsonUtxo { +@@ -926,6 +1047,97 @@ pub(crate) struct JsonWitnessData { + pub blinding: Option, + } + ++#[derive(Debug, Deserialize)] ++#[serde(deny_unknown_fields)] ++pub(crate) struct JsonInvoiceWitnessData { ++ pub amount_sat: u64, ++ #[serde(default)] ++ pub blinding: Option, ++} ++ ++#[derive(Debug, Deserialize)] ++#[serde(deny_unknown_fields)] ++pub(crate) struct JsonSendRgbInvoiceRequest { ++ pub invoice: String, ++ pub asset_id: String, ++ pub amount: u64, ++ #[serde(default)] ++ pub witness_data: Option, ++ pub donation: bool, ++ pub fee_rate: u64, ++ pub min_confirmations: u8, ++} ++ ++impl TryFrom for SendRgbInvoiceRequest { ++ type Error = Error; ++ ++ fn try_from(j: JsonSendRgbInvoiceRequest) -> Result { ++ Ok(Self { ++ invoice: j.invoice, ++ asset_id: parse_contract_id(&j.asset_id)?, ++ amount: j.amount, ++ witness_data: j.witness_data.map(|w| WitnessData { ++ amount_sat: w.amount_sat, ++ blinding: w.blinding, ++ }), ++ donation: j.donation, ++ fee_rate: j.fee_rate, ++ min_confirmations: j.min_confirmations, ++ }) ++ } ++} ++ ++#[cfg(test)] ++mod invoice_first_boundary_tests { ++ use super::*; ++ ++ #[test] ++ fn complete_invoice_and_u64_values_are_preserved() { ++ let raw = r#"{"invoice":"rgb:invoice?pk=abc&enc=required&expiry=1893456000","asset_id":"rgb:asset","amount":18446744073709551615,"witness_data":{"amount_sat":9007199254740993,"blinding":null},"donation":false,"fee_rate":1,"min_confirmations":6}"#; ++ let request: JsonSendRgbInvoiceRequest = serde_json::from_str(raw).unwrap(); ++ assert_eq!(request.amount, u64::MAX); ++ assert_eq!( ++ request.invoice, ++ "rgb:invoice?pk=abc&enc=required&expiry=1893456000" ++ ); ++ assert_eq!( ++ request.witness_data.unwrap().amount_sat, ++ 9_007_199_254_740_993 ++ ); ++ } ++ ++ #[test] ++ fn unknown_policy_fields_and_invalid_integer_tokens_are_rejected() { ++ let base = serde_json::json!({"invoice":"rgb:invoice", "asset_id":"rgb:asset", "amount":1, ++ "donation":false, "fee_rate":1, "min_confirmations":6}); ++ for field in [ ++ "public_key", ++ "encryption", ++ "sender_proxy_url", ++ "expiration_timestamp", ++ "skip_sync", ++ ] { ++ let mut request = base.clone(); ++ request[field] = serde_json::json!("override"); ++ assert!(serde_json::from_value::(request).is_err()); ++ } ++ for (field, value) in [ ++ ("amount", serde_json::json!(-1)), ++ ("amount", serde_json::json!(1.5)), ++ ("amount", serde_json::json!("1")), ++ ("min_confirmations", serde_json::json!(256)), ++ ("donation", serde_json::json!("false")), ++ ] { ++ let mut request = base.clone(); ++ request[field] = value; ++ assert!(serde_json::from_value::(request).is_err()); ++ } ++ let mut request = base; ++ request["witness_data"] = serde_json::json!({"amount_sat":600, "public_key":"override"}); ++ assert!(serde_json::from_value::(request).is_err()); ++ } ++} ++ + impl From for WitnessData { + fn from(w: JsonWitnessData) -> Self { + WitnessData { +@@ -2208,6 +2420,7 @@ impl From for JsonUtxo { pub(crate) struct JsonUnspent { pub utxo: JsonUtxo, pub rgb_allocations: Vec, @@ -920,7 +1677,7 @@ index 5b606f1..b235b6e 100644 } impl From for JsonUnspent { -@@ -2215,6 +2301,30 @@ impl From for JsonUnspent { +@@ -2215,6 +2428,30 @@ impl From for JsonUnspent { JsonUnspent { utxo: u.utxo.into(), rgb_allocations: u.rgb_allocations.into_iter().map(Into::into).collect(), @@ -952,10 +1709,10 @@ index 5b606f1..b235b6e 100644 } } diff --git a/bindings/c-ffi/src/lib.rs b/bindings/c-ffi/src/lib.rs -index e1dded1..6eef53f 100644 +index e1dded1..42e6332 100644 --- a/bindings/c-ffi/src/lib.rs +++ b/bindings/c-ffi/src/lib.rs -@@ -89,6 +89,32 @@ pub extern "C" fn rln_free_string(s: *mut c_char) { +@@ -89,6 +89,33 @@ pub extern "C" fn rln_free_string(s: *mut c_char) { // Lifecycle // --------------------------------------------------------------------------- @@ -965,15 +1722,16 @@ index e1dded1..6eef53f 100644 + ffi_call!("rln_binding_build_info", Ok::(serde_json::json!({ + "abi_version": 1, + "rln_version": "0.17.0-beta.3", -+ "rln_commit": "ecc69f20308231400561f6f060fea88acfa40cc4", -+ "lightning_commit": "79b9eb5166546c62692d3a672986f385c7fe047d", ++ "rln_commit": "cd3be114a6d0e41af8ce3f29719d210e105234dc", ++ "lightning_commit": "510189d5700a6d1406229ce7523e181c3838715e", ++ "rgb_lib_commit": "962730a211f0f7569d7ad8ae777052f433d87655", + "adapter_sha256": env!("RLN_ADAPTER_SHA256"), + "wrapper_sha256": env!("RLN_WRAPPER_SHA256"), + "lock_sha256": env!("RLN_LOCK_SHA256"), + "target": env!("RLN_BUILD_TARGET"), + "capabilities": ["canonical-unlock-v1", "persistent-native-signer", "refresh-transfers-v1", + "apay-address", "decoded-invoice-cltv", "tagged-rgb-assignment", "pending-blinded-v1", -+ "rgb-contract-import-v1", "rgb-transfer-metadata-import-v1", "consignment-export-v1", "mainnet-lightning-rejection-v1", ++ "rgb-contract-import-v1", "rgb-transfer-metadata-import-v1", "invoice-first-send-v1", "rgb-proxy-v1", "consignment-export-v1", "mainnet-lightning-rejection-v1", + "external-signer-eth-rpc-v1", "external-signer-burn-v1", + "native-signer-shutdown-v1", "owned-session-cleanup-v1", "bip322-simple-v1", + "vss-backup-v1", "vss-backup-info-v1", "vss-restore-v1", "vss-automatic-backup-v1", "vss-backup-metadata-v1"], @@ -988,7 +1746,7 @@ index e1dded1..6eef53f 100644 #[unsafe(no_mangle)] pub extern "C" fn rln_sdk_node_new(request_json: *const c_char) -> CResult { ffi_call!("rln_sdk_node_new", api::sdk_node_new(request_json)) -@@ -122,11 +148,7 @@ pub extern "C" fn rln_sdk_node_shutdown(node: &COpaqueStruct) -> CResultString { +@@ -122,11 +149,7 @@ pub extern "C" fn rln_sdk_node_shutdown(node: &COpaqueStruct) -> CResultString { ffi_call!("rln_sdk_node_shutdown", api::sdk_node_shutdown(node)) } @@ -1001,7 +1759,7 @@ index e1dded1..6eef53f 100644 #[unsafe(no_mangle)] pub extern "C" fn rln_sdk_node_vss_clear_fence( node: &COpaqueStruct, -@@ -138,20 +160,55 @@ pub extern "C" fn rln_sdk_node_vss_clear_fence( +@@ -138,20 +161,55 @@ pub extern "C" fn rln_sdk_node_vss_clear_fence( ) } @@ -1066,7 +1824,19 @@ index e1dded1..6eef53f 100644 /// APay receiver-side registration with an LSP. Argument is the LSP's /// node_id as a hex string (compressed secp256k1). Returns JSON of /// `AsyncOrderNewResponse` (request_id, host_node_id, protocol_version, -@@ -733,6 +790,39 @@ pub extern "C" fn rln_sdk_shutdown() -> CResultString { +@@ -342,6 +400,11 @@ pub extern "C" fn rln_send_rgb(node: &COpaqueStruct, request_json: *const c_char + ffi_call!("rln_send_rgb", api::send_rgb(node, request_json)) + } + ++#[unsafe(no_mangle)] ++pub extern "C" fn rln_send_rgb_invoice(node: &COpaqueStruct, request_json: *const c_char) -> CResultString { ++ ffi_call!("rln_send_rgb_invoice", api::send_rgb_invoice(node, request_json)) ++} ++ + #[unsafe(no_mangle)] + pub extern "C" fn rln_import_rgb_transfer_consignment( + node: &COpaqueStruct, +@@ -733,6 +796,39 @@ pub extern "C" fn rln_sdk_shutdown() -> CResultString { // External-signer surface // --------------------------------------------------------------------------- @@ -1106,7 +1876,7 @@ index e1dded1..6eef53f 100644 /// Drop a `NativeExternalSigner` handle. Safe to call immediately after /// attach / init / unlock succeeds: RLN holds its own `Arc` clone. #[unsafe(no_mangle)] -@@ -754,6 +844,86 @@ mod free_tests { +@@ -754,6 +850,86 @@ mod free_tests { free_sdk_node(COpaqueStruct::null()); free_native_external_signer(COpaqueStruct::null()); } @@ -1193,7 +1963,7 @@ index e1dded1..6eef53f 100644 } #[unsafe(no_mangle)] -@@ -776,6 +946,21 @@ pub extern "C" fn rln_native_external_signer_bootstrap(signer: &COpaqueStruct) - +@@ -776,6 +952,21 @@ pub extern "C" fn rln_native_external_signer_bootstrap(signer: &COpaqueStruct) - ) } @@ -1215,7 +1985,7 @@ index e1dded1..6eef53f 100644 #[unsafe(no_mangle)] pub extern "C" fn rln_sdk_node_init_with_native_external_signer( node: &COpaqueStruct, -@@ -839,3 +1024,67 @@ pub extern "C" fn rln_sdk_node_unlock_with_attached_external_signer( +@@ -839,3 +1030,67 @@ pub extern "C" fn rln_sdk_node_unlock_with_attached_external_signer( api::sdk_node_unlock_with_attached_external_signer(node, request_json) ) } @@ -1320,7 +2090,7 @@ index 98c3372..c4db3c0 100644 #[cfg(test)] diff --git a/bindings/rgb_lightning_node.udl b/bindings/rgb_lightning_node.udl -index df823f2..3c09f98 100644 +index a58949a..8c72dfa 100644 --- a/bindings/rgb_lightning_node.udl +++ b/bindings/rgb_lightning_node.udl @@ -5,6 +5,7 @@ namespace rgb_lightning_node { @@ -1351,7 +2121,7 @@ index df823f2..3c09f98 100644 [Throws=RlnError] void connectpeer(string peer_pubkey_and_addr); [Throws=RlnError] -@@ -636,6 +650,7 @@ dictionary SdkInitRequest { +@@ -640,6 +654,7 @@ dictionary SdkInitRequest { string? lsp_bearer_token; string? vss_url = null; boolean vss_allow_http = false; @@ -1360,10 +2130,10 @@ index df823f2..3c09f98 100644 boolean reuse_addresses = false; }; diff --git a/src/ldk.rs b/src/ldk.rs -index 2ff6e98..619b628 100644 +index 86035df..78b61f7 100644 --- a/src/ldk.rs +++ b/src/ldk.rs -@@ -109,8 +109,8 @@ use std::collections::HashSet; +@@ -108,18 +108,17 @@ use std::collections::HashSet; use std::convert::TryInto; use std::fs; use std::hash::{DefaultHasher, Hash, Hasher}; @@ -1373,7 +2143,10 @@ index 2ff6e98..619b628 100644 use std::path::Path; use std::str::FromStr; #[cfg(test)] -@@ -120,7 +120,6 @@ use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; + use std::sync::atomic::AtomicUsize; + use std::sync::atomic::{AtomicBool, Ordering}; +-use std::sync::{Arc, Mutex, MutexGuard, RwLock}; ++use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; #[cfg(any(test, feature = "vss"))] use std::time::Instant; use std::time::{Duration, SystemTime}; @@ -1381,24 +2154,18 @@ index 2ff6e98..619b628 100644 use tokio::runtime::Handle; use tokio::sync::watch::Sender; use tokio::task::JoinHandle; -@@ -187,11 +186,11 @@ use crate::signer::{ +@@ -183,8 +182,8 @@ use crate::signer::{ }; - use crate::swap::{SwapData, SwapInfo}; + use crate::swap::SwapData; use crate::utils::{ - check_port_is_available, connect_peer_if_necessary, description_from_invoice, -- description_hash_from_invoice, do_connect_peer, get_current_timestamp, -- get_max_local_rgb_amount, hex_str, validate_and_parse_payment_hash, -- validate_and_parse_payment_preimage, AppState, CommonState, LightningState, StaticState, -- UnlockedAppState, FATAL_ERROR, PROXY_ENDPOINT_LOCAL, PROXY_ENDPOINT_PUBLIC, +- description_hash_from_invoice, do_connect_peer, get_current_timestamp, hex_str, + connect_peer_if_necessary, description_from_invoice, description_hash_from_invoice, + do_connect_peer, get_current_timestamp, get_max_local_rgb_amount, hex_str, -+ validate_and_parse_payment_hash, validate_and_parse_payment_preimage, AppState, CommonState, -+ LightningState, StaticState, UnlockedAppState, FATAL_ERROR, PROXY_ENDPOINT_LOCAL, -+ PROXY_ENDPOINT_PUBLIC, - }; - - const RGB_TRANSFER_CHAN_EXPIRATION_SECS: u64 = 86400; -@@ -341,14 +340,290 @@ pub(crate) fn node_override_matches( + validate_and_parse_payment_hash, validate_and_parse_payment_preimage, AppState, CommonState, + LightningState, StaticState, UnlockedAppState, FATAL_ERROR, PROXY_ENDPOINT_LOCAL, + PROXY_ENDPOINT_PUBLIC, +@@ -337,14 +336,290 @@ pub(crate) fn node_override_matches( .is_some_and(|id| *id == our_node_id) } @@ -1690,7 +2457,7 @@ index 2ff6e98..619b628 100644 #[derive(Clone, Debug)] pub(crate) struct PaymentInfo { pub(crate) preimage: Option, -@@ -1103,12 +1378,14 @@ impl LightningState { +@@ -1099,12 +1374,14 @@ impl LightningState { /// `FutureSpawner` backed by `tokio::spawn`, used to drive async monitor /// persistence completions for `MonitorUpdatingPersisterAsync`. #[cfg(feature = "vss")] @@ -1707,7 +2474,7 @@ index 2ff6e98..619b628 100644 } } -@@ -3796,18 +4073,26 @@ async fn handle_ldk_events( +@@ -3621,18 +3898,26 @@ async fn handle_ldk_events( .await } Event::ConnectionNeeded { node_id, addresses } => { @@ -1743,7 +2510,7 @@ index 2ff6e98..619b628 100644 } Event::SplicePending { .. } => { // We don't use the splice feature, so this event should never be seen. -@@ -4992,6 +5277,19 @@ impl NodeStartup { +@@ -4817,6 +5102,19 @@ impl NodeStartup { Some(mnemonic_str.clone()), ) }; @@ -1763,10 +2530,11 @@ index 2ff6e98..619b628 100644 let data_dir = static_state .storage_dir_path .clone() -@@ -5040,6 +5338,23 @@ impl NodeStartup { +@@ -4866,7 +5164,22 @@ impl NodeStartup { )), _ => None, }; +- // go_online and configure_vss_backup drive blocking rgb-lib HTTP clients; + #[cfg(feature = "vss")] + let snapshot_policy = match (&static_state.vss_url, &vss_identity) { + (Some(url), Some(_identity)) => { @@ -1782,60 +2550,62 @@ index 2ff6e98..619b628 100644 + } + _ => None, + }; -+ #[cfg(feature = "vss")] -+ let startup_snapshot_policy = snapshot_policy.clone(); - // go_online and configure_vss_backup drive blocking rgb-lib HTTP clients; ++ // go_online drives blocking rgb-lib HTTP clients; // run them off the async runtime so they don't fail on a single-vCPU host. let (rgb_wallet, rgb_online) = tokio::task::spawn_blocking(move || { -@@ -5069,7 +5384,7 @@ impl NodeStartup { - let vss_config = - rgb_lib::wallet::vss::VssBackupConfig::new(vss_url, rgb_store_id, signing_key) - .with_encryption(true) -- .with_auto_backup(true) -+ .with_auto_backup(false) - .with_backup_mode(rgb_lib::wallet::vss::VssBackupMode::Blocking); - // Fail closed: a misconfigured backup must not silently run local-only. - rgb_wallet.configure_vss_backup(vss_config).map_err(|e| { -@@ -5077,7 +5392,7 @@ impl NodeStartup { - "Failed to configure VSS backup for RGB wallet: {e}" - )) - })?; -- tracing::info!("VSS auto-backup (blocking) enabled for RGB wallet"); -+ tracing::info!("VSS snapshot client configured; maintained mutation policy owns automatic uploads"); - // Auto-backup only tracks local changes; an empty remote (fresh - // wallet or wiped store) needs an explicit upload. - if let Some(client) = rgb_wallet.vss_client() { -@@ -5085,11 +5400,11 @@ impl NodeStartup { - let info = rt - .block_on(rgb_wallet.vss_backup_info(&client)) - .map_err(|e| APIError::FailedVssInit(format!("VSS backup info: {e}")))?; -- if !info.backup_exists { -- rt.block_on(rgb_wallet.vss_backup(&client)).map_err(|e| { -- APIError::FailedVssInit(format!("initial RGB VSS backup failed: {e}")) -- })?; -- tracing::info!("Uploaded RGB wallet backup to empty VSS store"); -+ if let Some(policy) = &startup_snapshot_policy { -+ if policy.enabled.load(std::sync::atomic::Ordering::SeqCst) && (!info.backup_exists || info.backup_required || policy.pending()) { -+ policy.backup(&rgb_wallet)?; -+ tracing::info!("Uploaded initial RGB wallet snapshot"); -+ } - } - } - } -@@ -5133,23 +5448,13 @@ impl NodeStartup { - // client per stream avoids running two tokio runtimes for the same - // backups and removes the race between the two clients writing - // overlapping state. + let mut rgb_wallet = RgbLibWallet::new( +@@ -4951,16 +5264,14 @@ impl NodeStartup { + &master_fingerprint, + )?; + +- // No second VssBackupClient is constructed here: the manual /vssbackup +- // and /vssbackupinfo routes use the wallet's own client, retrievable via +- // `wallet.vss_client()` (R-lib.1 in rgb-lib's PR #31). Keeping a single +- // client per stream avoids running two tokio runtimes for the same +- // backups and removes the race between the two clients writing +- // overlapping state. - let rgb_wallet_wrapper = Arc::new(RgbLibWalletWrapper::new( - Arc::new(Mutex::new(rgb_wallet)), - rgb_online, - )); ++ // One wrapper client serves all manual and automatic uploads. The maintained ++ // mutation policy owns scheduling, acknowledgement and completion receipts. + let mut wrapper = RgbLibWalletWrapper::new(Arc::new(Mutex::new(rgb_wallet)), rgb_online); + #[cfg(feature = "vss")] + { + wrapper.snapshot_policy = snapshot_policy; + } + let rgb_wallet_wrapper = Arc::new(wrapper); + #[cfg(feature = "vss")] + if let Some((url, store_id, key)) = rgb_vss_backup { + let wrapper = Arc::clone(&rgb_wallet_wrapper); +@@ -4969,15 +5280,20 @@ impl NodeStartup { + .configure_vss_backup( + crate::rgb_vss::VssBackupConfig::new(url, store_id, key) + .with_encryption(true) +- .with_auto_backup(true) ++ .with_auto_backup(false) + .with_backup_mode(crate::rgb_vss::VssBackupMode::Blocking), + ) + .map_err(|error| APIError::FailedVssInit(error.to_string()))?; +- let client = wrapper.vss_client().expect("configured client"); ++ let client = wrapper.vss_client().ok_or_else(|| { ++ APIError::FailedVssInit("VSS snapshot client was not configured".into()) ++ })?; + let info = wrapper + .vss_backup_info(&client) + .map_err(|error| APIError::FailedVssInit(error.to_string()))?; +- if !info.backup_exists || info.backup_required { ++ if wrapper.snapshot_policy.as_ref().is_some_and(|policy| { ++ policy.enabled.load(std::sync::atomic::Ordering::SeqCst) ++ && (!info.backup_exists || info.backup_required || policy.pending()) ++ }) { + wrapper + .vss_backup(&client) + .map_err(|error| APIError::FailedVssInit(error.to_string()))?; +@@ -4990,18 +5306,6 @@ impl NodeStartup { + })??; + } - let node_id = match self.external_node_id.as_deref() { - Some(node_id) => PublicKey::from_str(node_id).map_err(|error| { @@ -1852,7 +2622,7 @@ index 2ff6e98..619b628 100644 Ok(Arc::new(CommonState { config: Arc::clone(&static_state.config), signer, -@@ -5518,6 +5823,44 @@ async fn prepare_node( +@@ -5380,6 +5684,44 @@ async fn prepare_node( async fn start_lightning( #[allow(unused_mut)] mut setup: NodeStartup, @@ -1897,7 +2667,7 @@ index 2ff6e98..619b628 100644 ) -> Result<(Option, Arc), APIError> { // Defense in depth: no chain backend, manager or worker precedes this check. setup.app_state.check_lightning_supported()?; -@@ -5525,6 +5868,29 @@ async fn start_lightning( +@@ -5387,6 +5729,29 @@ async fn start_lightning( let static_state = &app_state.static_state; let unlock_request = setup.unlock_request.clone(); let gossip_source_config = unlock_request.gossip_source.clone().unwrap_or_default(); @@ -1927,7 +2697,7 @@ index 2ff6e98..619b628 100644 let kv_store = Arc::clone(&setup.kv_store); let external_signer_mode = setup.external_signer_mode; let internal_mnemonic = setup.internal_mnemonic.clone(); -@@ -5541,6 +5907,19 @@ async fn start_lightning( +@@ -5403,6 +5768,19 @@ async fn start_lightning( let bitcoin_network = static_state.network; let network: Network = bitcoin_network.into(); let ldk_peer_listening_port = static_state.ldk_peer_listening_port; @@ -1947,7 +2717,7 @@ index 2ff6e98..619b628 100644 #[cfg(feature = "vss")] let monitor_kv_store = setup.monitor_kv_store.clone().ok_or_else(|| { APIError::Unexpected("Lightning monitor store is not available".to_string()) -@@ -5582,6 +5961,9 @@ async fn start_lightning( +@@ -5444,6 +5822,9 @@ async fn start_lightning( handle.clone(), Arc::clone(&logger), static_state.config.chain.fee_refresh_interval_secs, @@ -1957,7 +2727,7 @@ index 2ff6e98..619b628 100644 ) .await { -@@ -5590,7 +5972,11 @@ async fn start_lightning( +@@ -5452,7 +5833,11 @@ async fn start_lightning( }; // Check that the bitcoind we've connected to is running the network we expect @@ -1970,7 +2740,7 @@ index 2ff6e98..619b628 100644 if bitcoind_chain != match bitcoin_network { BitcoinNetwork::Mainnet => "main", -@@ -5606,7 +5992,11 @@ async fn start_lightning( +@@ -5468,7 +5853,11 @@ async fn start_lightning( // Poll for the best chain tip, used by the channel manager & spv client let polled_chain_tip = init::validate_best_block_header(bitcoind_client.as_ref()) .await @@ -1983,7 +2753,7 @@ index 2ff6e98..619b628 100644 let initial_best_block = polled_chain_tip.to_best_block(); ChainSetup { -@@ -5639,16 +6029,14 @@ async fn start_lightning( +@@ -5501,16 +5890,14 @@ async fn start_lightning( Arc::clone(&logger), static_state.config.chain.indexer_timeout_secs, static_state.config.chain.fee_refresh_interval_secs, @@ -2004,7 +2774,7 @@ index 2ff6e98..619b628 100644 ); let initial_best_block = indexer_client .get_best_block() -@@ -5684,7 +6072,7 @@ async fn start_lightning( +@@ -5546,7 +5933,7 @@ async fn start_lightning( let (chain_monitor, mut channelmonitors): (Arc, _) = { let persister = MonitorUpdatingPersisterAsync::new( Arc::clone(&monitor_kv_store), @@ -2013,7 +2783,7 @@ index 2ff6e98..619b628 100644 Arc::clone(&logger), 1000, Arc::clone(&keys_manager), -@@ -6090,7 +6478,7 @@ async fn start_lightning( +@@ -5952,7 +6339,7 @@ async fn start_lightning( }); // The UTXO verifier can only attach to a P2P sync, and only after PeerManager @@ -2022,7 +2792,7 @@ index 2ff6e98..619b628 100644 let (p2p_gossip_sync_for_verifier, route_handler): ( Option>, Arc, -@@ -6205,61 +6593,110 @@ async fn start_lightning( +@@ -6067,61 +6454,110 @@ async fn start_lightning( // provide a verifier, so announcements are checked whatever the sync mode is. if let Some(p2p) = &p2p_gossip_sync_for_verifier { let peer_manager_wake = Arc::new({ @@ -2162,7 +2932,7 @@ index 2ff6e98..619b628 100644 match backend { #[cfg(feature = "block-sync")] ChainBackend::BlockSync { client, .. } => { -@@ -6269,7 +6706,7 @@ async fn start_lightning( +@@ -6131,7 +6567,7 @@ async fn start_lightning( let chain_tip = block_sync_chain_tip.expect("block-sync chain tip is set while syncing listeners"); let mut cache = block_sync_cache; @@ -2171,7 +2941,7 @@ index 2ff6e98..619b628 100644 let chain_poller = poll::ChainPoller::new(client.as_ref(), network); let chain_listener = ( chain_monitor_listener, -@@ -6278,13 +6715,21 @@ async fn start_lightning( +@@ -6140,13 +6576,21 @@ async fn start_lightning( let mut spv_client = SpvClient::new(chain_tip, chain_poller, &mut cache, &chain_listener); loop { @@ -2197,7 +2967,7 @@ index 2ff6e98..619b628 100644 } }); } -@@ -6295,19 +6740,19 @@ async fn start_lightning( +@@ -6157,19 +6601,19 @@ async fn start_lightning( chain_monitor.clone(), output_sweeper.clone(), ]; @@ -2224,7 +2994,7 @@ index 2ff6e98..619b628 100644 } }); } -@@ -6508,6 +6953,7 @@ async fn start_lightning( +@@ -6370,6 +6814,7 @@ async fn start_lightning( let unlocked_state = Arc::new(LightningState { common: Arc::clone(&common), @@ -2232,9 +3002,9 @@ index 2ff6e98..619b628 100644 channel_manager: Arc::clone(&channel_manager), gossip_source: Arc::clone(&gossip_source), inbound_payments, -@@ -6540,15 +6986,19 @@ async fn start_lightning( - taker_swaps: Arc::clone(&taker_swaps), - })); +@@ -6397,15 +6842,19 @@ async fn start_lightning( + + asset_link_handler.set_authorizer(Arc::new(NodeAssetLinkAuthorizer)); - // Refresh the RGS snapshot on a fixed interval (RGS mode only). The first - // tick fires immediately, so a freshly unlocked node syncs right away. @@ -2260,7 +3030,7 @@ index 2ff6e98..619b628 100644 } let recent_payments_payment_ids = channel_manager -@@ -6615,17 +7065,22 @@ async fn start_lightning( +@@ -6472,17 +6921,22 @@ async fn start_lightning( let connect_cm = Arc::clone(&channel_manager); let connect_pm = Arc::clone(&peer_manager); let connect_db = static_state.db(); @@ -2287,7 +3057,7 @@ index 2ff6e98..619b628 100644 return; } let db = RlnDatabase::new((*connect_db).clone()); -@@ -6637,14 +7092,18 @@ async fn start_lightning( +@@ -6494,14 +6948,18 @@ async fn start_lightning( .map(|chan| chan.counterparty.node_id) .filter(|id| connect_pm.peer_by_node_id(id).is_none()) { @@ -2310,7 +3080,7 @@ index 2ff6e98..619b628 100644 } } } -@@ -6676,16 +7135,16 @@ async fn start_lightning( +@@ -6533,16 +6991,16 @@ async fn start_lightning( if let Some(link) = external_signer_link_watch { let su_channel_manager = Arc::clone(&channel_manager); let su_chain_monitor = Arc::clone(&chain_monitor); @@ -2331,7 +3101,7 @@ index 2ff6e98..619b628 100644 return; } if link_event && link.is_connected() { -@@ -6707,10 +7166,12 @@ async fn start_lightning( +@@ -6564,10 +7022,12 @@ async fn start_lightning( interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { tokio::select! { @@ -2345,7 +3115,7 @@ index 2ff6e98..619b628 100644 return; } su_chain_monitor.signer_unblocked(None); -@@ -6723,45 +7184,17 @@ async fn start_lightning( +@@ -6580,45 +7040,17 @@ async fn start_lightning( }); } @@ -2397,7 +3167,7 @@ index 2ff6e98..619b628 100644 } sweep_handler.sweep_stale_state(); } -@@ -6771,15 +7204,24 @@ async fn start_lightning( +@@ -6628,15 +7060,24 @@ async fn start_lightning( let chan_man = Arc::clone(&channel_manager); let announce_initial_delay_secs = static_state.config.node.announce_initial_delay_secs; let announce_refresh_interval_secs = static_state.config.node.announce_refresh_interval_secs; @@ -2426,7 +3196,7 @@ index 2ff6e98..619b628 100644 // Don't bother trying to announce if we don't have any public channls, though our // peers should drop such an announcement anyway. Note that announcement may not // propagate until we have a channel with 6+ confirmations. -@@ -6808,7 +7250,8 @@ async fn start_lightning( +@@ -6665,7 +7106,8 @@ async fn start_lightning( Ok(( Some(LdkBackgroundServices { stop_processing, @@ -2436,7 +3206,7 @@ index 2ff6e98..619b628 100644 peer_manager: peer_manager.clone(), bp_exit, background_processor: Some(background_processor), -@@ -6837,6 +7280,8 @@ pub(crate) fn attach_external_signer_transport( +@@ -6694,6 +7136,8 @@ pub(crate) fn attach_external_signer_transport( Ok(ExternalSignerAttachment { bootstrap, transport, @@ -2445,7 +3215,7 @@ index 2ff6e98..619b628 100644 }) } -@@ -6851,15 +7296,11 @@ impl AppState { +@@ -6708,15 +7152,11 @@ impl AppState { ldk_background_services .stop_processing .store(true, Ordering::Release); @@ -2464,7 +3234,7 @@ index 2ff6e98..619b628 100644 Some(ldk_background_services) } } -@@ -6980,13 +7421,19 @@ async fn release_vss_fence(kv_store: Arc, teardown: VssTeardown) +@@ -6837,13 +7277,19 @@ async fn release_vss_fence(kv_store: Arc, teardown: VssTeardown) // Runs while shutting down, possibly because the background processor itself // died, so its outcome is reported instead of unwrapped. @@ -2489,7 +3259,7 @@ index 2ff6e98..619b628 100644 } } -@@ -7052,7 +7499,16 @@ mod vss_teardown_tests { +@@ -6909,7 +7355,16 @@ mod vss_teardown_tests { } /// Stop the resources owned by the unlocked session, including wallet-only persistence. @@ -2507,7 +3277,7 @@ index 2ff6e98..619b628 100644 tracing::info!("Stopping node services"); // Wait for wallet operations holding the API mutex, then remove both session owners together. // New callers cannot use a wallet after its VSS fence has been handed over; concurrent stops -@@ -7074,19 +7530,33 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6931,19 +7386,33 @@ pub(crate) async fn stop_node(app_state: Arc) { ) }); @@ -2543,7 +3313,7 @@ index 2ff6e98..619b628 100644 } } } -@@ -7095,9 +7565,13 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6952,9 +7421,13 @@ pub(crate) async fn stop_node(app_state: Arc) { .as_mut() .and_then(|services| services.background_processor.take()) { @@ -2558,7 +3328,7 @@ index 2ff6e98..619b628 100644 #[cfg(feature = "vss")] if let Some((common, monitor_store)) = common_and_monitor { let deadline = Instant::now() + VSS_TEARDOWN_TIMEOUT; -@@ -7112,34 +7586,95 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6969,34 +7442,95 @@ pub(crate) async fn stop_node(app_state: Arc) { .is_err() { worker.abort(); @@ -2938,10 +3708,10 @@ index 5c6701e..6189956 100644 UtxoResult::Async(res) } diff --git a/src/ldk_chain_backend/transaction_sync.rs b/src/ldk_chain_backend/transaction_sync.rs -index cadf7fb..3903882 100644 +index a635117..d126aa3 100644 --- a/src/ldk_chain_backend/transaction_sync.rs +++ b/src/ldk_chain_backend/transaction_sync.rs -@@ -10,13 +10,13 @@ use rgb_lib::wallet::rust_only::IndexerProtocol as RgbLibIndexerProtocol; +@@ -10,13 +10,13 @@ use rgb_lib::wallet::IndexerProtocol as RgbLibIndexerProtocol; use std::collections::HashMap; use std::io; use std::sync::atomic::AtomicU32; @@ -2983,7 +3753,7 @@ index cadf7fb..3903882 100644 peer_manager_wake: Arc, } -@@ -78,14 +79,20 @@ impl IndexerClient { +@@ -78,6 +79,8 @@ impl IndexerClient { logger: Arc, timeout_secs: u64, fee_refresh_interval_secs: u64, @@ -2992,6 +3762,8 @@ index cadf7fb..3903882 100644 ) -> io::Result { let fees = Arc::new(default_fee_buckets()); let backend = match protocol { +@@ -88,9 +91,13 @@ impl IndexerClient { + } #[cfg(feature = "electrum")] RgbLibIndexerProtocol::Electrum => { - let client = Arc::new(ElectrumClient::new(&server_url).map_err(|e| { @@ -3007,7 +3779,7 @@ index cadf7fb..3903882 100644 client.server_features().map_err(|e| { io::Error::other(format!("failed to query electrum server features: {e}")) })?; -@@ -95,6 +102,7 @@ impl IndexerClient { +@@ -100,6 +107,7 @@ impl IndexerClient { logger.clone(), handle.clone(), fee_refresh_interval_secs, @@ -3015,7 +3787,7 @@ index cadf7fb..3903882 100644 ); IndexerBackend::Electrum(client) } -@@ -115,6 +123,7 @@ impl IndexerClient { +@@ -120,6 +128,7 @@ impl IndexerClient { logger.clone(), handle.clone(), fee_refresh_interval_secs, @@ -3023,7 +3795,7 @@ index cadf7fb..3903882 100644 ); IndexerBackend::Esplora(client) } -@@ -125,7 +134,7 @@ impl IndexerClient { +@@ -130,7 +139,7 @@ impl IndexerClient { Ok(Self { backend, fees, @@ -3032,7 +3804,7 @@ index cadf7fb..3903882 100644 logger, }) } -@@ -193,13 +202,15 @@ impl IndexerClient { +@@ -198,13 +207,15 @@ impl IndexerClient { impl IndexerGossipVerifier { pub(crate) fn new( client: Arc, @@ -3049,7 +3821,7 @@ index cadf7fb..3903882 100644 } } } -@@ -211,7 +222,14 @@ impl UtxoLookup for IndexerGossipVerifier { +@@ -216,7 +227,14 @@ impl UtxoLookup for IndexerGossipVerifier { let client = self.client.clone(); let gossiper = self.gossiper.clone(); let peer_manager_wake = self.peer_manager_wake.clone(); @@ -3065,7 +3837,7 @@ index cadf7fb..3903882 100644 let lookup = tokio::task::spawn_blocking(move || { let height = (short_channel_id >> 40) as u32; let tx_index = ((short_channel_id >> 16) & 0x00ff_ffff) as usize; -@@ -268,8 +286,12 @@ impl UtxoLookup for IndexerGossipVerifier { +@@ -273,8 +291,12 @@ impl UtxoLookup for IndexerGossipVerifier { }) .await .unwrap_or(Err(UtxoLookupError::UnknownTx)); @@ -3080,7 +3852,7 @@ index cadf7fb..3903882 100644 }); UtxoResult::Async(result) } -@@ -301,25 +323,28 @@ fn electrum_txid_from_pos( +@@ -306,28 +328,28 @@ fn electrum_txid_from_pos( } impl IndexerSyncClient { @@ -3092,6 +3864,9 @@ index cadf7fb..3903882 100644 logger: Arc, ) -> io::Result { - match protocol { +- RgbLibIndexerProtocol::Bitcoind => Err(io::Error::other( +- "bitcoind is unavailable for transaction sync", +- )), + // Reuse the explicitly time-bounded clients; the upstream convenience constructors + // otherwise create new clients with unbounded Electrum socket waits. + match &client.backend { @@ -3123,7 +3898,7 @@ index cadf7fb..3903882 100644 } } -@@ -377,7 +402,10 @@ impl BroadcasterInterface for IndexerClient { +@@ -385,7 +407,10 @@ impl BroadcasterInterface for IndexerClient { .collect::>(); let client = client.clone(); let logger = self.logger.clone(); @@ -3135,7 +3910,7 @@ index cadf7fb..3903882 100644 let res = tokio::task::spawn_blocking(move || { let mut last_error = None; for tx in txs { -@@ -413,7 +441,10 @@ impl BroadcasterInterface for IndexerClient { +@@ -421,7 +446,10 @@ impl BroadcasterInterface for IndexerClient { let txs = txs.iter().map(|tx| (*tx).clone()).collect::>(); let client = client.clone(); let logger = self.logger.clone(); @@ -3147,7 +3922,7 @@ index cadf7fb..3903882 100644 let res = tokio::task::spawn_blocking(move || { let mut last_error = None; for tx in txs { -@@ -455,9 +486,15 @@ fn poll_electrum_fee_estimates( +@@ -463,9 +491,15 @@ fn poll_electrum_fee_estimates( logger: Arc, handle: tokio::runtime::Handle, refresh_interval_secs: u64, @@ -3164,7 +3939,7 @@ index cadf7fb..3903882 100644 let res = tokio::task::spawn_blocking({ let client = client.clone(); move || { -@@ -495,7 +532,11 @@ fn poll_electrum_fee_estimates( +@@ -503,7 +537,11 @@ fn poll_electrum_fee_estimates( } } @@ -3177,7 +3952,7 @@ index cadf7fb..3903882 100644 } }); } -@@ -507,9 +548,15 @@ fn poll_esplora_fee_estimates( +@@ -515,9 +553,15 @@ fn poll_esplora_fee_estimates( logger: Arc, handle: tokio::runtime::Handle, refresh_interval_secs: u64, @@ -3194,7 +3969,7 @@ index cadf7fb..3903882 100644 let res = tokio::task::spawn_blocking({ let client = client.clone(); move || client.get_fee_estimates() -@@ -544,7 +591,11 @@ fn poll_esplora_fee_estimates( +@@ -552,7 +596,11 @@ fn poll_esplora_fee_estimates( } } @@ -3208,10 +3983,10 @@ index cadf7fb..3903882 100644 }); } diff --git a/src/lib.rs b/src/lib.rs -index 5279eed..f848a55 100644 +index c85e309..6bd8869 100644 --- a/src/lib.rs +++ b/src/lib.rs -@@ -69,6 +69,8 @@ pub mod test_utils; +@@ -76,6 +76,8 @@ pub mod test_utils; mod uniffi_api; mod utils; #[cfg(feature = "vss")] @@ -3221,10 +3996,10 @@ index 5279eed..f848a55 100644 pub use node::{NodeConfig, NodeHandle}; diff --git a/src/main.rs b/src/main.rs -index 027aab1..b6a58d9 100644 +index 003572d..7def4e9 100644 --- a/src/main.rs +++ b/src/main.rs -@@ -41,6 +41,8 @@ mod swap; +@@ -48,6 +48,8 @@ mod swap; mod synced_kv_store; mod utils; #[cfg(feature = "vss")] @@ -3233,7 +4008,7 @@ index 027aab1..b6a58d9 100644 mod vss_kv_store; // the test suite calls into `electrum_client` to wait for electrs to catch up with bitcoind, and -@@ -362,7 +364,9 @@ async fn shutdown_signal(app_state: Arc) { +@@ -371,7 +373,9 @@ async fn shutdown_signal(app_state: Arc) { tracing::info!("Will shutdown after change state is complete"); tokio::time::sleep(Duration::from_millis(300)).await; } @@ -3266,7 +4041,7 @@ index 0c75b17..ab46500 100644 assert!(!*fixture.state.changing_state.lock().unwrap()); } diff --git a/src/mainnet_tests.rs b/src/mainnet_tests.rs -index ee431cf..3655d0b 100644 +index d6f571f..8b920d5 100644 --- a/src/mainnet_tests.rs +++ b/src/mainnet_tests.rs @@ -80,11 +80,13 @@ impl Fixture { @@ -3315,7 +4090,7 @@ index f9c88b7..e040833 100644 assert!(common.persistence_worker.lock().unwrap().is_none()); } diff --git a/src/node.rs b/src/node.rs -index 18c80ee..ac6b491 100644 +index d504a55..9294786 100644 --- a/src/node.rs +++ b/src/node.rs @@ -3,7 +3,7 @@ use std::sync::Arc; @@ -3327,7 +4102,7 @@ index 18c80ee..ac6b491 100644 use crate::ldk::stop_node; use crate::utils::{start_daemon, AppState}; -@@ -77,14 +77,19 @@ impl NodeHandle { +@@ -82,14 +82,19 @@ impl NodeHandle { Ok(Self { state }) } @@ -3350,7 +4125,7 @@ index 18c80ee..ac6b491 100644 } #[cfg(feature = "uniffi")] -@@ -102,6 +107,86 @@ impl NodeHandle { +@@ -107,6 +112,86 @@ impl NodeHandle { mod tests { use super::*; @@ -3438,10 +4213,10 @@ index 18c80ee..ac6b491 100644 /// left in `uniffi_api::handle_from_request`) must be able to configure the remote-signer daemon /// address; without this field, `NodeHandle`-embedding callers could configure external-signer diff --git a/src/rgb.rs b/src/rgb.rs -index 05dc358..11c3457 100644 +index 83202d4..be14b41 100644 --- a/src/rgb.rs +++ b/src/rgb.rs -@@ -283,7 +283,7 @@ impl CommonState { +@@ -285,7 +285,7 @@ impl CommonState { // Keep preparation, signing and completion serialized against other wallet operations. // Persist Initiated before asking the signer: an interrupted call must retain its inputs. let wrapper = &self.rgb_wallet_wrapper; @@ -3450,77 +4225,63 @@ index 05dc358..11c3457 100644 let prepared = wallet.burn_begin( wrapper.online, asset_id, -@@ -690,14 +690,156 @@ fn validate_burn_signed_psbt(unsigned: &str, signed: &str) -> Result>, - pub(crate) online: Online, +@@ -658,71 +658,140 @@ fn validate_burn_signed_psbt(unsigned: &str, signed: &str) -> Result = MutexGuard<'a, RgbLibWallet>; ++pub(crate) struct RgbLibWalletWrapper { ++ pub(crate) wallet: Arc>, ++ pub(crate) online: Online, + #[cfg(feature = "vss")] + pub(crate) snapshot_policy: Option>, ++ #[cfg(feature = "vss")] ++ vss_client: Mutex>>, +} -+ -+/// Public wallet views cover balances, transaction identities/confirmation -+/// state and every known colored/vanilla UTXO. Sync changes no RGB stock data; -+/// stock/contract/receive operations use unconditional mutation admission. -+#[cfg(feature = "vss")] -+fn sync_snapshot(wallet: &mut RgbLibWallet, online: Online) -> Option { + ++/// Use the library's acknowledged content identity for no-op sync detection. ++/// Public balances alone omit BDK metadata, operation secrets and proxy keys. + #[cfg(feature = "vss")] ++fn snapshot_is_clean(wallet: &RgbLibWallet) -> bool { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { -+ sync_snapshot_inner(wallet, online) ++ matches!(wallet.backup_info(), Ok(false)) + })) -+ .ok() -+ .flatten() -+} -+#[cfg(feature = "vss")] -+fn sync_snapshot_inner(wallet: &mut RgbLibWallet, online: Online) -> Option { -+ let balance = wallet.get_btc_balance(Some(online), true).ok()?; -+ let transactions = wallet.list_transactions(Some(online), true).ok()?; -+ let unspents = wallet.list_unspents(Some(online), false, true).ok()?; -+ fn canonical(value: &mut serde_json::Value) { -+ match value { -+ serde_json::Value::Array(items) => { -+ for item in items.iter_mut() { -+ canonical(item); -+ } -+ items.sort_by_cached_key(|item| item.to_string()); -+ } -+ serde_json::Value::Object(map) => { -+ for item in map.values_mut() { -+ canonical(item); -+ } -+ } -+ _ => {} -+ } -+ } -+ let mut state = serde_json::to_value((balance, transactions, unspents)).ok()?; -+ canonical(&mut state); -+ Some(state) ++ .unwrap_or(false) +} + -+pub(crate) struct RgbWalletGuard<'a> { + pub(crate) struct RgbWalletGuard<'a> { +- inner: MutexGuard<'a, RgbLibWallet>, +- client: Option>, + guard: MutexGuard<'a, RgbLibWallet>, + mutated: bool, + wrapper: &'a RgbLibWalletWrapper, -+ sync_before: Option<(Option, bool)>, -+} -+impl std::ops::Deref for RgbWalletGuard<'_> { -+ type Target = RgbLibWallet; -+ fn deref(&self) -> &Self::Target { ++ sync_before: Option, + } +-#[cfg(feature = "vss")] + impl std::ops::Deref for RgbWalletGuard<'_> { + type Target = RgbLibWallet; + fn deref(&self) -> &Self::Target { +- &self.inner + &self.guard -+ } -+} -+impl std::ops::DerefMut for RgbWalletGuard<'_> { -+ fn deref_mut(&mut self) -> &mut Self::Target { + } + } +-#[cfg(feature = "vss")] + impl std::ops::DerefMut for RgbWalletGuard<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { +- &mut self.inner + &mut self.guard -+ } -+} -+impl Drop for RgbWalletGuard<'_> { -+ fn drop(&mut self) { + } + } +-#[cfg(feature = "vss")] + impl Drop for RgbWalletGuard<'_> { + fn drop(&mut self) { +- if std::thread::panicking() { +- return; + #[cfg(feature = "vss")] + if !std::thread::panicking() { -+ if let Some((Some(before), had_pending)) = self.sync_before.take() { -+ if !had_pending -+ && sync_snapshot(&mut self.guard, self.wrapper.online) -+ .is_some_and(|after| after == before) -+ { ++ if let Some(true) = self.sync_before.take() { ++ if snapshot_is_clean(&self.guard) { + if let Some(policy) = &self.wrapper.snapshot_policy { + if policy.clear_unchanged_intent().is_ok() { + self.mutated = false; @@ -3528,12 +4289,38 @@ index 05dc358..11c3457 100644 + } + } + } -+ } + } +- let Some(client) = self.client.as_ref().filter(|client| client.auto_backup()) else { +- return; +- }; +- // The same wallet mutex covers all callers, including explicit mutable access by bindings. +- // Only a confirmed remote commit can acknowledge the prepared content token. +- let result = self +- .inner +- .backup_info() +- .map_err(crate::rgb_vss::Error::from) +- .and_then(|dirty| { +- if dirty { +- crate::rgb_vss::backup_wallet(&mut self.inner, client).map(|_| ()) +- } else { +- Ok(()) + #[cfg(feature = "vss")] + if self.mutated { + if let Some(policy) = &self.wrapper.snapshot_policy { ++ if !policy.enabled.load(std::sync::atomic::Ordering::SeqCst) { ++ return; + } +- }); +- client.record_auto_backup_result(result.err().map(|error| error.to_string())); ++ let Some(client) = self.wrapper.vss_client() else { ++ policy ++ .failed ++ .store(true, std::sync::atomic::Ordering::SeqCst); ++ tracing::error!("Automatic snapshot client unavailable; wallet operation result is preserved"); ++ return; ++ }; + if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { -+ policy.after_mutation(&self.guard) ++ policy.after_mutation(&mut self.guard, &client) + })) + .is_err() + { @@ -3546,20 +4333,29 @@ index 05dc358..11c3457 100644 + } + } + } -+ } + } } +-pub(crate) struct RgbLibWalletWrapper { +- pub(crate) wallet: Arc>, +- pub(crate) online: Online, +- #[cfg(feature = "vss")] +- vss_client: Mutex>>, +-} +- impl RgbLibWalletWrapper { pub(crate) fn new(wallet: Arc>, online: Online) -> Self { -- RgbLibWalletWrapper { wallet, online } -+ RgbLibWalletWrapper { -+ wallet, -+ online, -+ #[cfg(feature = "vss")] + RgbLibWalletWrapper { + wallet, + online, + #[cfg(feature = "vss")] + snapshot_policy: None, -+ } -+ } -+ ++ #[cfg(feature = "vss")] + vss_client: Mutex::new(None), + } + } + +- fn lock_wallet(&self) -> MutexGuard<'_, RgbLibWallet> { + pub(crate) fn get_rgb_wallet(&self) -> RgbWalletGuard<'_> { + let guard = self.lock_rgb_wallet(); + RgbWalletGuard { @@ -3579,9 +4375,8 @@ index 05dc358..11c3457 100644 + policy.before_mutation(); + } + guard - } - -- pub(crate) fn get_rgb_wallet(&self) -> MutexGuard<'_, RgbLibWallet> { ++ } ++ + fn get_rgb_wallet_for_sync(&self, skip_sync: bool) -> RgbWalletGuard<'_> { + if skip_sync { + return self.get_rgb_wallet(); @@ -3589,19 +4384,16 @@ index 05dc358..11c3457 100644 + let mut guard = self.get_rgb_wallet(); + #[cfg(feature = "vss")] + if let Some(policy) = &self.snapshot_policy { -+ let had_pending = policy.pending() -+ || !matches!( -+ std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| guard -+ .guard -+ .backup_info())), -+ Ok(Ok(false)) -+ ); -+ let before = sync_snapshot(&mut guard.guard, self.online); ++ // A fresh/never-backed-up wallet has no acknowledged content digest. Do not ++ // mistake its timestamp-only "clean" result for proof of an unchanged sync. ++ let can_clear = !policy.pending() ++ && policy.has_completed_backup().unwrap_or(false) ++ && snapshot_is_clean(&guard.guard); + // Persist an intent before sync so a process death cannot hide an + // incoming transaction merely because the SDK dirty flag omitted it. + policy.before_mutation(); + guard.mutated = true; -+ guard.sync_before = Some((before, had_pending)); ++ guard.sync_before = Some(can_clear); + } + guard + } @@ -3609,7 +4401,53 @@ index 05dc358..11c3457 100644 // In FFI builds a panic crossing `extern "C"` frames aborts via // `panic_cannot_unwind`, cascading to every subsequent wallet op, // so we recover from mutex poisoning instead of propagating. -@@ -731,7 +873,8 @@ impl RgbLibWalletWrapper { +@@ -744,18 +813,6 @@ impl RgbLibWalletWrapper { + self.wallet.lock().unwrap() + } + +- pub(crate) fn get_rgb_wallet(&self) -> RgbWalletGuard<'_> { +- #[cfg(feature = "vss")] +- { +- RgbWalletGuard { +- inner: self.lock_wallet(), +- client: self.vss_client(), +- } +- } +- #[cfg(not(feature = "vss"))] +- self.lock_wallet() +- } +- + #[cfg(feature = "vss")] + pub(crate) fn vss_client(&self) -> Option> { + self.vss_client.lock().unwrap().clone() +@@ -781,8 +838,15 @@ impl RgbLibWalletWrapper { + &self, + client: &crate::rgb_vss::VssBackupClient, + ) -> Result { +- let mut wallet = self.lock_wallet(); +- crate::rgb_vss::backup_wallet(&mut wallet, client) ++ let mut wallet = self.lock_rgb_wallet(); ++ match &self.snapshot_policy { ++ Some(policy) => policy.backup(&mut wallet, client).map_err(|error| { ++ crate::rgb_vss::Error::Internal { ++ details: error.to_string(), ++ } ++ }), ++ None => crate::rgb_vss::backup_wallet(&mut wallet, client), ++ } + } + + #[cfg(feature = "vss")] +@@ -790,7 +854,7 @@ impl RgbLibWalletWrapper { + &self, + client: &crate::rgb_vss::VssBackupClient, + ) -> Result { +- let backup_required = self.lock_wallet().backup_info()?; ++ let backup_required = self.lock_rgb_wallet().backup_info()?; + let server_version = std::thread::scope(|scope| { + scope + .spawn(|| client.handle().block_on(client.get_backup_version())) +@@ -810,7 +874,8 @@ impl RgbLibWalletWrapper { // Upstream API retained through the merge; not yet wired into utexo's flow. #[allow(dead_code)] pub(crate) fn abort_pending_vanilla_tx(&self, txid: String) -> Result<(), RgbLibError> { @@ -3619,16 +4457,16 @@ index 05dc358..11c3457 100644 } pub(crate) fn bitcoin_network(&self) -> BitcoinNetwork { -@@ -746,7 +889,7 @@ impl RgbLibWalletWrapper { +@@ -825,7 +890,7 @@ impl RgbLibWalletWrapper { transport_endpoints: Vec, min_confirmations: u8, ) -> Result { - self.get_rgb_wallet().blind_receive( + self.get_rgb_wallet_mutating().blind_receive( + Some(self.online), asset_id, assignment, - expiration_timestamp, -@@ -760,7 +903,8 @@ impl RgbLibWalletWrapper { +@@ -841,7 +906,8 @@ impl RgbLibWalletWrapper { fascia: Fascia, witness_ord: Option, ) -> Result<(), RgbLibError> { @@ -3637,23 +4475,8 @@ index 05dc358..11c3457 100644 + .consume_fascia(fascia, witness_ord) } - pub(crate) fn color_psbt_and_prepare_consume( -@@ -768,7 +912,7 @@ impl RgbLibWalletWrapper { - psbt_to_color: &mut BitcoinPsbt, - coloring_info: ColoringInfo, - ) -> Result { -- self.get_rgb_wallet() -+ self.get_rgb_wallet_mutating() - .color_psbt_and_prepare_consume(psbt_to_color, coloring_info, 0, None) - } - -@@ -776,12 +920,12 @@ impl RgbLibWalletWrapper { - &self, - batch_transfer_idx: i32, - ) -> Result<(), RgbLibError> { -- self.get_rgb_wallet() -+ self.get_rgb_wallet_mutating() - .consume_transfer_fascia(self.online, batch_transfer_idx) + #[allow(unused_variables)] +@@ -866,7 +932,7 @@ impl RgbLibWalletWrapper { } pub(crate) fn create_consigments(&self, psbt: String) -> Result<(), RgbLibError> { @@ -3662,7 +4485,7 @@ index 05dc358..11c3457 100644 } pub(crate) fn create_utxos( -@@ -792,7 +936,7 @@ impl RgbLibWalletWrapper { +@@ -877,7 +943,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { @@ -3671,7 +4494,7 @@ index 05dc358..11c3457 100644 self.online, up_to, Some(num), -@@ -810,7 +954,7 @@ impl RgbLibWalletWrapper { +@@ -895,7 +961,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { @@ -3680,7 +4503,7 @@ index 05dc358..11c3457 100644 self.online, up_to, Some(num), -@@ -826,7 +970,7 @@ impl RgbLibWalletWrapper { +@@ -911,7 +977,7 @@ impl RgbLibWalletWrapper { signed_psbt: String, _skip_sync: bool, ) -> Result { @@ -3689,7 +4512,7 @@ index 05dc358..11c3457 100644 .create_utxos_end(self.online, signed_psbt) } -@@ -836,7 +980,7 @@ impl RgbLibWalletWrapper { +@@ -921,7 +987,7 @@ impl RgbLibWalletWrapper { no_asset_only: bool, skip_sync: bool, ) -> Result { @@ -3698,7 +4521,7 @@ index 05dc358..11c3457 100644 self.online, batch_transfer_idx, no_asset_only, -@@ -845,11 +989,12 @@ impl RgbLibWalletWrapper { +@@ -930,11 +996,11 @@ impl RgbLibWalletWrapper { } pub(crate) fn get_address(&self) -> Result { @@ -3707,13 +4530,12 @@ index 05dc358..11c3457 100644 } pub(crate) fn rotate_address(&self) -> Result { -- self.get_rgb_wallet().rotate_address(KeychainKind::Internal) -+ self.get_rgb_wallet_mutating() -+ .rotate_address(KeychainKind::Internal) +- self.get_rgb_wallet().get_address() ++ self.get_rgb_wallet_mutating().get_address() } pub(crate) fn get_asset_balance( -@@ -870,7 +1015,8 @@ impl RgbLibWalletWrapper { +@@ -955,7 +1021,8 @@ impl RgbLibWalletWrapper { pub(crate) fn get_btc_balance(&self, skip_sync: bool) -> Result { let online = if skip_sync { None } else { Some(self.online) }; @@ -3723,7 +4545,7 @@ index 05dc358..11c3457 100644 } pub(crate) fn get_fee_estimation(&self, blocks: u16) -> Result { -@@ -903,7 +1049,7 @@ impl RgbLibWalletWrapper { +@@ -988,7 +1055,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, min_confirmations: u8, ) -> Result { @@ -3732,7 +4554,7 @@ index 05dc358..11c3457 100644 self.online, asset_id, amount, -@@ -920,7 +1066,7 @@ impl RgbLibWalletWrapper { +@@ -1005,7 +1072,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, min_confirmations: u8, ) -> Result { @@ -3741,7 +4563,7 @@ index 05dc358..11c3457 100644 self.online, asset_id, inflation_amounts, -@@ -937,7 +1083,7 @@ impl RgbLibWalletWrapper { +@@ -1022,7 +1089,7 @@ impl RgbLibWalletWrapper { amounts: Vec, file_path: Option, ) -> Result { @@ -3750,25 +4572,16 @@ index 05dc358..11c3457 100644 .issue_asset_cfa(name, details, precision, amounts, file_path) } -@@ -952,7 +1098,7 @@ impl RgbLibWalletWrapper { - reject_list_url: Option, - issuance_type: Option, - ) -> Result { +@@ -1042,7 +1109,7 @@ impl RgbLibWalletWrapper { + details: "IFA link rights are unavailable in the BFA RGB library".into(), + }); + } - self.get_rgb_wallet().issue_asset_ifa( + self.get_rgb_wallet_mutating().issue_asset_ifa( ticker, name, precision, -@@ -972,7 +1118,7 @@ impl RgbLibWalletWrapper { - fee_rate: u64, - min_confirmations: u8, - ) -> Result { -- self.get_rgb_wallet().link_ifa( -+ self.get_rgb_wallet_mutating().link_ifa( - self.online, - parent_contract_id, - child_contract_id, -@@ -989,7 +1135,7 @@ impl RgbLibWalletWrapper { +@@ -1059,7 +1126,7 @@ impl RgbLibWalletWrapper { precision: u8, amounts: Vec, ) -> Result { @@ -3777,7 +4590,7 @@ index 05dc358..11c3457 100644 .issue_asset_nia(ticker, name, precision, amounts) } -@@ -1002,7 +1148,7 @@ impl RgbLibWalletWrapper { +@@ -1072,7 +1139,7 @@ impl RgbLibWalletWrapper { media_file_path: Option, attachments_file_paths: Vec, ) -> Result { @@ -3786,7 +4599,7 @@ index 05dc358..11c3457 100644 ticker, name, details, -@@ -1028,7 +1174,8 @@ impl RgbLibWalletWrapper { +@@ -1098,7 +1165,8 @@ impl RgbLibWalletWrapper { skip_sync: bool, ) -> Result, RgbLibError> { let online = if skip_sync { None } else { Some(self.online) }; @@ -3796,7 +4609,7 @@ index 05dc358..11c3457 100644 } pub(crate) fn list_transfers( -@@ -1045,7 +1192,7 @@ impl RgbLibWalletWrapper { +@@ -1115,7 +1183,7 @@ impl RgbLibWalletWrapper { skip_sync: bool, ) -> Result, RgbLibError> { let online = if skip_sync { None } else { Some(self.online) }; @@ -3805,7 +4618,7 @@ index 05dc358..11c3457 100644 .list_unspents(online, settled_only, skip_sync) } -@@ -1056,7 +1203,7 @@ impl RgbLibWalletWrapper { +@@ -1126,7 +1194,7 @@ impl RgbLibWalletWrapper { vout: u32, blinding: u64, ) -> Result<(RgbTransfer, Vec, HashSet), RgbLibError> { @@ -3814,7 +4627,7 @@ index 05dc358..11c3457 100644 self.online, consignment_path, txid, -@@ -1069,7 +1216,7 @@ impl RgbLibWalletWrapper { +@@ -1139,7 +1207,7 @@ impl RgbLibWalletWrapper { &self, recipient_id: String, ) -> Result, RgbLibError> { @@ -3823,7 +4636,7 @@ index 05dc358..11c3457 100644 .provide_out_of_band_ack(self.online, recipient_id) } -@@ -1078,11 +1225,8 @@ impl RgbLibWalletWrapper { +@@ -1148,11 +1216,8 @@ impl RgbLibWalletWrapper { consignment_path: String, media_file_paths: Vec, ) -> Result, RgbLibError> { @@ -3837,7 +4650,7 @@ index 05dc358..11c3457 100644 } pub(crate) fn refresh( -@@ -1091,7 +1235,7 @@ impl RgbLibWalletWrapper { +@@ -1161,7 +1226,7 @@ impl RgbLibWalletWrapper { filter: Vec, skip_sync: bool, ) -> Result { @@ -3846,7 +4659,7 @@ index 05dc358..11c3457 100644 .refresh(self.online, asset_id, filter, skip_sync) } -@@ -1100,7 +1244,7 @@ impl RgbLibWalletWrapper { +@@ -1170,7 +1235,7 @@ impl RgbLibWalletWrapper { consignment: RgbTransfer, offchain_txid: String, ) -> Result<(), RgbLibError> { @@ -3855,61 +4668,51 @@ index 05dc358..11c3457 100644 .save_new_asset(self.online, consignment, offchain_txid) } -@@ -1108,7 +1252,8 @@ impl RgbLibWalletWrapper { - &self, - contract: RgbContract, - ) -> Result { -- self.get_rgb_wallet().import_asset_contract(contract) -+ self.get_rgb_wallet_mutating() -+ .import_asset_contract(contract) - } - - pub(crate) fn send( -@@ -1119,7 +1264,7 @@ impl RgbLibWalletWrapper { - min_confirmations: u8, +@@ -1183,7 +1248,7 @@ impl RgbLibWalletWrapper { expiration_timestamp: u64, + sender_endpoint: Option, ) -> Result { - self.get_rgb_wallet().send( + self.get_rgb_wallet_mutating().send( self.online, recipient_map, donation, -@@ -1141,7 +1286,7 @@ impl RgbLibWalletWrapper { - dry_run: bool, - lock_time: Option, - ) -> Result { +@@ -1211,7 +1276,7 @@ impl RgbLibWalletWrapper { + details: "Custom RGB funding locktime is unavailable in the BFA RGB library".into(), + }); + } - self.get_rgb_wallet().send_begin( + self.get_rgb_wallet_mutating().send_begin( self.online, recipient_map, donation, -@@ -1160,8 +1305,14 @@ impl RgbLibWalletWrapper { +@@ -1230,7 +1295,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { - self.get_rgb_wallet() -- .send_btc(self.online, address, amount, fee_rate, skip_sync, None) -+ self.get_rgb_wallet_mutating().send_btc( ++ self.get_rgb_wallet_mutating() + .send_btc(self.online, address, amount, fee_rate, skip_sync) + } + +@@ -1241,8 +1306,14 @@ impl RgbLibWalletWrapper { + fee_rate: u64, + dry_run: bool, + ) -> Result { +- self.get_rgb_wallet() +- .send_btc_begin(self.online, address, amount, fee_rate, false, dry_run) ++ self.get_rgb_wallet_mutating().send_btc_begin( + self.online, + address, + amount, + fee_rate, -+ skip_sync, -+ None, ++ false, ++ dry_run, + ) } - pub(crate) fn send_btc_begin( -@@ -1173,7 +1324,7 @@ impl RgbLibWalletWrapper { - ) -> Result { - // Funding-only path: pin a final (zero) locktime so LDK accepts the - // vanilla channel funding tx regardless of the node's chain-tip lag. -- self.get_rgb_wallet().send_btc_begin( -+ self.get_rgb_wallet_mutating().send_btc_begin( - self.online, - address, - amount, -@@ -1185,18 +1336,20 @@ impl RgbLibWalletWrapper { + pub(crate) fn send_btc_channel_funding_begin( +@@ -1261,18 +1332,20 @@ impl RgbLibWalletWrapper { } pub(crate) fn send_btc_end(&self, signed_psbt: String) -> Result { @@ -3933,7 +4736,7 @@ index 05dc358..11c3457 100644 .send_end_db_update_only(self.online, signed_psbt) } -@@ -1205,7 +1358,7 @@ impl RgbLibWalletWrapper { +@@ -1281,7 +1354,7 @@ impl RgbLibWalletWrapper { } pub(crate) fn sync(&self, options: SyncOptions) -> Result<(), RgbLibError> { @@ -3942,7 +4745,7 @@ index 05dc358..11c3457 100644 } pub(crate) fn update_witnesses( -@@ -1213,7 +1366,7 @@ impl RgbLibWalletWrapper { +@@ -1289,7 +1362,7 @@ impl RgbLibWalletWrapper { after_height: u32, force_witnesses: Vec, ) -> Result { @@ -3951,7 +4754,7 @@ index 05dc358..11c3457 100644 .update_witnesses(self.online, after_height, force_witnesses) } -@@ -1222,7 +1375,7 @@ impl RgbLibWalletWrapper { +@@ -1298,7 +1371,7 @@ impl RgbLibWalletWrapper { witness_id: RgbTxid, witness_ord: WitnessOrd, ) -> Result<(), RgbLibError> { @@ -3960,16 +4763,16 @@ index 05dc358..11c3457 100644 .upsert_witness(witness_id, witness_ord) } -@@ -1234,7 +1387,7 @@ impl RgbLibWalletWrapper { +@@ -1310,7 +1383,7 @@ impl RgbLibWalletWrapper { transport_endpoints: Vec, min_confirmations: u8, ) -> Result { - self.get_rgb_wallet().witness_receive( + self.get_rgb_wallet_mutating().witness_receive( + Some(self.online), asset_id, assignment, - expiration_timestamp, -@@ -1335,7 +1488,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1413,7 +1486,7 @@ impl WalletSource for RgbLibWalletWrapper { Box::pin(async move { let network = Network::from(self.bitcoin_network()); let unspents = self @@ -3978,7 +4781,7 @@ index 05dc358..11c3457 100644 .list_unspents_vanilla(self.online, 1, false) .map_err(|e| { tracing::error!(error = %e, "failed to list unspents for fee bumping"); -@@ -1378,7 +1531,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1456,7 +1529,7 @@ impl WalletSource for RgbLibWalletWrapper { fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()> { Box::pin(async move { @@ -3987,10 +4790,24 @@ index 05dc358..11c3457 100644 tracing::error!(error = %e, "failed to get change address for fee bumping"); })?; let parsed = Address::from_str(&address).map_err(|e| { -@@ -1643,6 +1796,165 @@ mod wallet_source_tests { +@@ -1725,6 +1798,239 @@ mod wallet_source_tests { RgbLibWalletWrapper::new(Arc::new(Mutex::new(wallet)), Online { id: 0 }) } ++ #[test] ++ fn ordinary_bitcoin_prepare_is_distinct_from_unsupported_channel_funding() { ++ let wrapper = signetcustom_wallet_source(); ++ // This fresh offline fixture reaches the actual RGB wallet's online check. ++ assert!(matches!( ++ wrapper.send_btc_begin("unused".into(), 1000, 1, false), ++ Err(RgbLibError::Offline) ++ )); ++ assert!(matches!( ++ wrapper.send_btc_channel_funding_begin("unused".into(), 1000, 1), ++ Err(RgbLibError::Internal { details }) if details.contains("channel funding locktime") ++ )); ++ } ++ + #[cfg(feature = "vss")] + #[test] + fn explicit_mutation_marks_paused_wallet_while_read_guards_remain_pure() { @@ -4028,6 +4845,36 @@ index 05dc358..11c3457 100644 + } + + #[cfg(feature = "vss")] ++ fn completed_snapshot_fixture(wrapper: &RgbLibWalletWrapper, storage: &std::path::Path) { ++ let mut wallet = wrapper.lock_rgb_wallet(); ++ let snapshot = wallet.prepare_snapshot().unwrap(); ++ assert!(wallet.acknowledge_snapshot(&snapshot.token).unwrap()); ++ let policy_path = std::fs::read_dir(storage) ++ .unwrap() ++ .map(|entry| entry.unwrap().path()) ++ .find(|path| { ++ path.file_name() ++ .unwrap() ++ .to_str() ++ .unwrap() ++ .starts_with(".utexo-vss-policy-v1-") ++ }) ++ .unwrap(); ++ let completion = crate::vss_snapshot_policy::Completion { ++ version: 1, ++ completed_at_ms: 1, ++ network: BitcoinNetwork::SignetCustom.to_string().to_lowercase(), ++ vss_url: "https://example.invalid/vss".into(), ++ node_id: "test-node".into(), ++ }; ++ std::fs::write( ++ policy_path.with_extension("completion.json"), ++ serde_json::to_vec(&completion).unwrap(), ++ ) ++ .unwrap(); ++ } ++ ++ #[cfg(feature = "vss")] + #[test] + fn unchanged_sync_intent_clears_only_its_own_marker() { + let tmp = tempfile::tempdir().unwrap(); @@ -4043,6 +4890,8 @@ index 05dc358..11c3457 100644 + ); + let mut wrapper = signetcustom_wallet_source(); + wrapper.snapshot_policy = Some(policy.clone()); ++ completed_snapshot_fixture(&wrapper, tmp.path()); ++ assert!(policy.has_completed_backup().unwrap()); + { + let _guard = wrapper.get_rgb_wallet_for_sync(false); + } @@ -4056,7 +4905,39 @@ index 05dc358..11c3457 100644 + let _guard = wrapper.get_rgb_wallet_for_sync(false); + } + assert!(policy.pending()); -+ assert!(policy.metadata().unwrap().contains("\"last_backup\":null")); ++ assert!(policy.has_completed_backup().unwrap()); ++ } ++ ++ #[cfg(feature = "vss")] ++ #[test] ++ fn sync_does_not_clear_address_index_mutation_or_unacknowledged_fresh_state() { ++ let tmp = tempfile::tempdir().unwrap(); ++ let policy = Arc::new( ++ crate::vss_snapshot_policy::SnapshotPolicy::load( ++ tmp.path(), ++ BitcoinNetwork::SignetCustom, ++ "https://example.invalid/vss".into(), ++ "test-node".into(), ++ false, ++ ) ++ .unwrap(), ++ ); ++ let mut wrapper = signetcustom_wallet_source(); ++ wrapper.snapshot_policy = Some(policy.clone()); ++ { ++ let _guard = wrapper.get_rgb_wallet_for_sync(false); ++ } ++ // A timestamp-only fresh wallet has no acknowledged content identity. ++ assert!(policy.pending()); ++ policy.clear_unchanged_intent().unwrap(); ++ completed_snapshot_fixture(&wrapper, tmp.path()); ++ { ++ let mut guard = wrapper.get_rgb_wallet_for_sync(false); ++ guard.get_address().unwrap(); ++ } ++ // Derivation state changes without changing balance/history/UTXO views. ++ assert!(policy.pending()); ++ assert!(wrapper.get_rgb_wallet().backup_info().unwrap()); + } + + #[cfg(feature = "vss")] @@ -4074,8 +4955,6 @@ index 05dc358..11c3457 100644 + "abort_pending_vanilla_tx", + "blind_receive", + "consume_fascia", -+ "color_psbt_and_prepare_consume", -+ "consume_transfer_fascia", + "create_consigments", + "create_utxos", + "create_utxos_begin", @@ -4087,7 +4966,6 @@ index 05dc358..11c3457 100644 + "inflate", + "issue_asset_cfa", + "issue_asset_ifa", -+ "link_ifa", + "issue_asset_nia", + "issue_asset_uda", + "accept_transfer_consignment", @@ -4095,7 +4973,6 @@ index 05dc358..11c3457 100644 + "provide_out_of_band_consignment", + "refresh", + "save_new_asset", -+ "import_asset_contract", + "send", + "send_begin", + "send_btc", @@ -4153,11 +5030,24 @@ index 05dc358..11c3457 100644 #[tokio::test] async fn wallet_source_does_not_panic_on_signetcustom() { let wrapper = signetcustom_wallet_source(); +diff --git a/src/rgb_import.rs b/src/rgb_import.rs +index 225897d..00ff24e 100644 +--- a/src/rgb_import.rs ++++ b/src/rgb_import.rs +@@ -205,7 +205,7 @@ pub(crate) async fn import_rgb_contract( + .to_str() + .ok_or_else(|| APIError::InvalidRgbContract("Invalid temporary contract path".into()))? + .to_owned(); +- let mut wallet = unlocked_state.rgb_wallet_wrapper.get_rgb_wallet(); ++ let mut wallet = unlocked_state.rgb_wallet_wrapper.get_rgb_wallet_mutating(); + let already_imported = wallet.is_asset_known(contract_id)?; + let imported_id = rgb_lib::wallet::RgbWalletOpsOffline::import_contract( + &mut *wallet, diff --git a/src/routes.rs b/src/routes.rs -index b6784be..a0b28a8 100644 +index 9f660f5..ec10901 100644 --- a/src/routes.rs +++ b/src/routes.rs -@@ -1757,6 +1757,9 @@ impl From for RgbLibWitnessData { +@@ -1760,6 +1760,9 @@ impl From for RgbLibWitnessData { impl AppState { fn check_changing_state(&self) -> Result<(), APIError> { @@ -4167,7 +5057,7 @@ index b6784be..a0b28a8 100644 if *self.get_changing_state() { return Err(APIError::ChangingState); } -@@ -2575,8 +2578,7 @@ pub(crate) async fn connect_peer( +@@ -2578,8 +2581,7 @@ pub(crate) async fn connect_peer( let (peer_pubkey, peer_addr) = parse_peer_info(payload.peer_pubkey_and_addr.to_string())?; if let Some(peer_addr) = peer_addr { @@ -4177,7 +5067,7 @@ index b6784be..a0b28a8 100644 let db = state.get_db(); db.persist_channel_peer(&peer_pubkey, &peer_addr)?; } else { -@@ -4125,7 +4127,7 @@ pub(crate) async fn lock( +@@ -4130,7 +4132,7 @@ pub(crate) async fn lock( }; tracing::debug!("Stopping node..."); @@ -4186,7 +5076,7 @@ index b6784be..a0b28a8 100644 tracing::debug!("Node stopped"); state.update_unlocked_app_state(None).await; -@@ -4675,7 +4677,7 @@ pub(crate) async fn open_channel( +@@ -4680,7 +4682,7 @@ pub(crate) async fn open_channel( } } if let Some(peer_addr) = peer_addr { @@ -4195,23 +5085,7 @@ index b6784be..a0b28a8 100644 .await?; db.persist_channel_peer(&peer_pubkey, &peer_addr)?; } else { -@@ -5789,7 +5791,14 @@ pub(crate) async fn vss_backup( - let version = tokio::task::spawn_blocking(move || { - let wallet = wrapper.get_rgb_wallet(); - let rt = vss_client.handle().clone(); -- rt.block_on(wallet.vss_backup(&vss_client)) -+ match &wrapper.snapshot_policy { -+ Some(policy) => policy -+ .backup(&wallet) -+ .map_err(|e| rgb_lib::Error::Internal { -+ details: e.to_string(), -+ }), -+ None => rt.block_on(wallet.vss_backup(&vss_client)), -+ } - }) - .await - .map_err(|e| APIError::Unexpected(format!("VSS backup task failed: {e}")))? -@@ -6097,11 +6106,13 @@ mod state_mocks { +@@ -6143,11 +6145,13 @@ mod state_mocks { lsp_base_url: None, lsp_bearer_token: None, vss_url: None, @@ -4226,7 +5100,7 @@ index b6784be..a0b28a8 100644 ldk_background_services: Arc::new(Mutex::new(None)), attached_external_signer: Arc::new(Mutex::new(None)), diff --git a/src/sdk/mod.rs b/src/sdk/mod.rs -index 77680dd..5e96268 100644 +index 6ec97cf..723e483 100644 --- a/src/sdk/mod.rs +++ b/src/sdk/mod.rs @@ -140,6 +140,9 @@ impl Drop for OpenChannelVirtualIntentGuard { @@ -4248,23 +5122,7 @@ index 77680dd..5e96268 100644 state: &Arc, ) -> Result>>, APIError> { check_changing_state(state)?; -@@ -2058,7 +2061,14 @@ pub(crate) async fn vss_backup(state: Arc) -> Result { - let version = tokio::task::spawn_blocking(move || { - let wallet = wrapper.get_rgb_wallet(); - let rt = vss_client.handle().clone(); -- rt.block_on(wallet.vss_backup(&vss_client)) -+ match &wrapper.snapshot_policy { -+ Some(policy) => policy -+ .backup(&wallet) -+ .map_err(|e| rgb_lib::Error::Internal { -+ details: e.to_string(), -+ }), -+ None => rt.block_on(wallet.vss_backup(&vss_client)), -+ } - }) - .await - .map_err(|e| APIError::Unexpected(format!("VSS backup task failed: {e}")))? -@@ -2070,6 +2080,97 @@ pub(crate) async fn vss_backup(state: Arc) -> Result { +@@ -2152,6 +2155,98 @@ pub(crate) async fn vss_backup(state: Arc) -> Result { .await } @@ -4287,14 +5145,12 @@ index 77680dd..5e96268 100644 + .vss_client() + .ok_or_else(|| APIError::FailedVssInit("VSS is not configured".to_string()))?; + let wrapper = unlocked_state.rgb_wallet_wrapper.clone(); -+ let info = tokio::task::spawn_blocking(move || { -+ let wallet = wrapper.get_rgb_wallet(); -+ let rt = vss_client.handle().clone(); -+ rt.block_on(wallet.vss_backup_info(&vss_client)) -+ }) -+ .await -+ .map_err(|_| APIError::FailedVssInit("VSS backup status task failed".to_string()))? -+ .map_err(|_| APIError::FailedVssInit("VSS backup status could not be read".to_string()))?; ++ let info = tokio::task::spawn_blocking(move || wrapper.vss_backup_info(&vss_client)) ++ .await ++ .map_err(|_| APIError::FailedVssInit("VSS backup status task failed".to_string()))? ++ .map_err(|_| { ++ APIError::FailedVssInit("VSS backup status could not be read".to_string()) ++ })?; + Ok(serde_json::json!({ + "backup_exists": info.backup_exists, + "version": info.server_version, @@ -4328,7 +5184,7 @@ index 77680dd..5e96268 100644 + let guard = check_unlocked(&state).await?; + let wrapper = guard.as_ref().unwrap().rgb_wallet_wrapper.clone(); + tokio::task::spawn_blocking(move || { -+ let wallet = wrapper.get_rgb_wallet(); ++ let mut wallet = wrapper.get_rgb_wallet(); + let policy = wrapper + .snapshot_policy + .as_ref() @@ -4336,7 +5192,10 @@ index 77680dd..5e96268 100644 + let previous = policy.enabled.load(std::sync::atomic::Ordering::SeqCst); + // Re-enabling checkpoints all pending state before the policy can be acknowledged on. + if enabled && !previous { -+ policy.backup(&wallet)?; ++ let client = wrapper ++ .vss_client() ++ .ok_or_else(|| APIError::FailedVssInit("VSS is not configured".into()))?; ++ policy.backup(&mut wallet, &client)?; + } + policy.commit_enabled(enabled)?; + Ok(serde_json::json!({"automatic_backup_enabled":enabled}).to_string()) @@ -4362,7 +5221,7 @@ index 77680dd..5e96268 100644 /// Clears the VSS single-writer fence so a fresh instance can take over a /// store whose previous owner did not release it, for example after a crash /// or an incomplete persistence flush during shutdown. -@@ -2307,8 +2408,7 @@ pub(crate) async fn connect_peer( +@@ -2389,8 +2484,7 @@ pub(crate) async fn connect_peer( let (peer_pubkey, peer_addr) = parse_peer_info(peer_pubkey_and_addr.to_string())?; if let Some(peer_addr) = peer_addr { @@ -4372,7 +5231,7 @@ index 77680dd..5e96268 100644 state .get_db() .persist_channel_peer(&peer_pubkey, &peer_addr)?; -@@ -3068,8 +3168,7 @@ pub(crate) async fn open_channel( +@@ -3157,8 +3251,7 @@ pub(crate) async fn open_channel( } } if let Some(peer_addr) = peer_addr { @@ -4382,7 +5241,7 @@ index 77680dd..5e96268 100644 state .get_db() .persist_channel_peer(&peer_pubkey, &peer_addr)?; -@@ -4829,11 +4928,13 @@ mod tests { +@@ -4939,11 +5032,13 @@ mod tests { lsp_bearer_token: None, database: RwLock::new(Arc::new(database)), vss_url: None, @@ -4422,7 +5281,7 @@ index 535a698..0d6ace8 100644 ldk_background_services: Arc::new(Mutex::new(None)), attached_external_signer: Arc::new(Mutex::new(None)), diff --git a/src/test_utils.rs b/src/test_utils.rs -index 1282ad3..257addf 100644 +index df8a444..dedc116 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -41,11 +41,13 @@ pub fn mock_locked_app_state() -> TestAppState { @@ -5631,7 +6490,7 @@ index 0000000..c9f968e + } +} diff --git a/src/uniffi_api/mod.rs b/src/uniffi_api/mod.rs -index 89eb544..7765237 100644 +index 2724b8d..6df2db9 100644 --- a/src/uniffi_api/mod.rs +++ b/src/uniffi_api/mod.rs @@ -1,7 +1,10 @@ @@ -5670,7 +6529,7 @@ index 89eb544..7765237 100644 Ok(()) } -@@ -119,7 +132,16 @@ fn handle_from_request(request: SdkInitRequest) -> Result +@@ -124,7 +137,16 @@ fn handle_from_request(request: SdkInitRequest) -> Result // surface (and mobile-side bindings) for remote-signer support is a separate, larger change. remote_signer_listen_addr: None, }; @@ -5688,7 +6547,7 @@ index 89eb544..7765237 100644 } fn send_rgb_from_state( -@@ -417,12 +439,9 @@ impl SdkNode { +@@ -422,12 +444,9 @@ impl SdkNode { Ok(Self { handle }) } @@ -5703,7 +6562,7 @@ index 89eb544..7765237 100644 } pub fn init(&self, password: String, mnemonic: Option) -> Result { -@@ -478,6 +497,45 @@ impl SdkNode { +@@ -483,6 +502,45 @@ impl SdkNode { Ok(version) } @@ -5749,7 +6608,7 @@ index 89eb544..7765237 100644 pub fn connectpeer(&self, peer_pubkey_and_addr: String) -> Result<(), RlnError> { let state = self.handle.app_state(); block_on_sdk(sdk::connect_peer(state, peer_pubkey_and_addr))?; -@@ -1751,8 +1809,23 @@ impl SdkNode { +@@ -1784,8 +1842,23 @@ impl SdkNode { ) -> Result<(), RlnError> { let state = self.handle.app_state(); let expected = key_source_from_uniffi_bootstrap(signer.bootstrap()?); @@ -5775,7 +6634,7 @@ index 89eb544..7765237 100644 } #[allow(clippy::too_many_arguments)] -@@ -1795,15 +1868,18 @@ pub fn sdk_initialize(request: SdkInitRequest) -> Result<(), RlnError> { +@@ -1828,15 +1901,18 @@ pub fn sdk_initialize(request: SdkInitRequest) -> Result<(), RlnError> { Ok(()) } @@ -5802,11 +6661,10 @@ index 89eb544..7765237 100644 } pub fn sdk_init_with_external_signer( -@@ -2053,3 +2129,112 @@ impl crate::signer::ExternalSignerTransport for UniffiExternalSignerTransport { - +@@ -2087,6 +2163,115 @@ impl crate::signer::ExternalSignerTransport for UniffiExternalSignerTransport { #[cfg(test)] mod tests; -+ + +// Maintained binding methods deliberately do not change the upstream UniFFI/remote protocol. +#[cfg(feature = "vls")] +fn validate_message_signer_wallet_identity( @@ -5915,6 +6773,10 @@ index 89eb544..7765237 100644 + let network = network_from_str(&network)?; + bip322::verify(&message, &address, &signature, network.into()).map_err(RlnError::InvalidRequest) +} ++ + pub fn sdk_sendrgbinvoice(request: SendRgbInvoiceRequest) -> Result { + let handle = NodeHandle::from_app_state(get_uniffi_app_state()?); + SdkNode { handle }.sendrgbinvoice(request) diff --git a/src/uniffi_api/native_signer.rs b/src/uniffi_api/native_signer.rs index 42f6c14..a587e0d 100644 --- a/src/uniffi_api/native_signer.rs @@ -6444,10 +7306,10 @@ index 27ea384..d4abc7f 100644 } diff --git a/src/uniffi_api/types.rs b/src/uniffi_api/types.rs -index fb7517b..eb597f4 100644 +index 1bf91a1..2251e5b 100644 --- a/src/uniffi_api/types.rs +++ b/src/uniffi_api/types.rs -@@ -861,6 +861,7 @@ pub struct SdkInitRequest { +@@ -863,6 +863,7 @@ pub struct SdkInitRequest { pub lsp_bearer_token: Option, pub vss_url: Option, pub vss_allow_http: bool, @@ -6457,19 +7319,19 @@ index fb7517b..eb597f4 100644 } diff --git a/src/uniffi_api/vss_restore.rs b/src/uniffi_api/vss_restore.rs new file mode 100644 -index 0000000..e500852 +index 0000000..913f303 --- /dev/null +++ b/src/uniffi_api/vss_restore.rs -@@ -0,0 +1,860 @@ +@@ -0,0 +1,1030 @@ +//! Maintained external-signer RGB restore admission. Uses public transport and +//! crypto APIs, with bounded extraction and a local, destination-bound receipt. ++use crate::rgb_vss::{VssBackupClient, VssBackupConfig}; +use crate::{error::APIError, signer::read_key_source_file, utils::AppState}; +use bitcoin::hashes::{ + hmac::{Hmac, HmacEngine}, + sha256, Hash, HashEngine, +}; +use rand::RngCore; -+use rgb_lib::wallet::vss::{VssBackupClient, VssBackupConfig}; +use serde::{Deserialize, Serialize}; +#[cfg(unix)] +use std::os::{ @@ -6491,6 +7353,8 @@ index 0000000..e500852 +const MAX_ENTRIES: usize = 25000; +const MAX_MANIFEST: u64 = 65536; +const MAX_RECEIPT: u64 = 4096; ++const DATABASE_NAME: &str = "rgb_lib_db"; ++const SQLITE_HEADER: &[u8; 16] = b"SQLite format 3\0"; + +fn error(code: &str) -> APIError { + APIError::FailedVssInit(code.to_owned()) @@ -6755,6 +7619,26 @@ index 0000000..e500852 + { + return Err(error("VSS_RESTORE_IDENTITY_MISMATCH")); + } ++ // New-library snapshots keep BDK, RGB stock and proxy operation keys together in SQLite. ++ // Admit its complete public configuration, rather than accepting a manifest-shaped stub. ++ let schemas = value["supported_schemas"] ++ .as_array() ++ .ok_or_else(|| error("VSS_RESTORE_INVALID_MANIFEST"))?; ++ let mut known = HashSet::new(); ++ if value["database_type"] != "Sqlite" ++ || !value["max_allocations_per_utxo"] ++ .as_u64() ++ .is_some_and(|n| n > 0 && n <= u32::MAX as u64) ++ || value["vanilla_keychain"] != 0 ++ || value["witness_version"] != "Taproot" ++ || schemas.is_empty() ++ || schemas.iter().any(|schema| match schema.as_str() { ++ Some(name @ ("Nia" | "Uda" | "Cfa" | "Ifa" | "Bfa")) => !known.insert(name), ++ _ => true, ++ }) ++ { ++ return Err(error("VSS_RESTORE_INVALID_MANIFEST")); ++ } + let identity = serde_json::json!([ + value["version"], + value["bitcoin_network"], @@ -6762,7 +7646,10 @@ index 0000000..e500852 + value["account_xpub_vanilla"], + value["account_xpub_colored"], + value["vanilla_keychain"], -+ value["witness_version"] ++ value["witness_version"], ++ value["database_type"], ++ value["max_allocations_per_utxo"], ++ value["supported_schemas"] + ]); + Ok(digest(identity.to_string().as_bytes())) +} @@ -6800,9 +7687,38 @@ index 0000000..e500852 + { + return Err(error("VSS_RESTORE_IDENTITY_MISMATCH")); + } ++ validate_local_database(&target.join(DATABASE_NAME))?; + Ok(receipt.fields) +} + ++fn validate_local_database(path: &Path) -> Result<(), APIError> { ++ let mut options = OpenOptions::new(); ++ options.read(true); ++ #[cfg(unix)] ++ options.custom_flags(libc::O_NOFOLLOW); ++ let mut file = options ++ .open(path) ++ .map_err(|_| error("VSS_RESTORE_INVALID_DATABASE"))?; ++ let meta = file ++ .metadata() ++ .map_err(|_| error("VSS_RESTORE_INVALID_DATABASE"))?; ++ #[cfg(unix)] ++ if meta.uid() != unsafe { libc::geteuid() } || meta.mode() & 0o777 != 0o600 || meta.nlink() != 1 ++ { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } ++ if !meta.is_file() || meta.len() < 100 || meta.len() > MAX_INFLATED { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } ++ let mut header = [0u8; 16]; ++ file.read_exact(&mut header) ++ .map_err(|_| error("VSS_RESTORE_INVALID_DATABASE"))?; ++ if &header != SQLITE_HEADER { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } ++ Ok(()) ++} ++ +fn archive_plan(bytes: &[u8], ctx: &Context) -> Result { + if bytes.len() > MAX_COMPRESSED { + return Err(error("VSS_RESTORE_ARCHIVE_LIMIT")); @@ -6815,6 +7731,7 @@ index 0000000..e500852 + let mut paths = HashSet::new(); + let mut total = 0u64; + let mut manifest = None; ++ let mut database = false; + for i in 0..archive.len() { + let entry = archive + .by_index(i) @@ -6851,6 +7768,18 @@ index 0000000..e500852 + { + return Err(error("VSS_RESTORE_INVALID_ARCHIVE_PATH")); + } ++ if clean.split('/').skip(1).any(|part| { ++ matches!( ++ part, ++ "rgb_lib_db-wal" ++ | "rgb_lib_db-shm" ++ | "rgb_lib_db-journal" ++ | "bdk_db" ++ | "bdk_db_watch_only" ++ ) ++ }) { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } + total = total + .checked_add(entry.size()) + .ok_or_else(|| error("VSS_RESTORE_ARCHIVE_LIMIT"))?; @@ -6869,8 +7798,33 @@ index 0000000..e500852 + .read_to_end(&mut data) + .map_err(|_| error("VSS_RESTORE_INVALID_ARCHIVE"))?; + manifest = Some(manifest_identity(&data, ctx)?); ++ } else if clean == format!("{}/{DATABASE_NAME}", ctx.fp) { ++ if entry.is_dir() || entry.size() < 100 { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } ++ let mut header = [0u8; 16]; ++ entry ++ .take(16) ++ .read_exact(&mut header) ++ .map_err(|_| error("VSS_RESTORE_INVALID_DATABASE"))?; ++ if &header != SQLITE_HEADER { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } ++ database = true; ++ } else if !entry.is_dir() && entry.size() >= 16 { ++ let mut header = [0u8; 16]; ++ entry ++ .take(16) ++ .read_exact(&mut header) ++ .map_err(|_| error("VSS_RESTORE_INVALID_ARCHIVE"))?; ++ if &header == SQLITE_HEADER { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } + } + } ++ if !database { ++ return Err(error("VSS_RESTORE_INVALID_DATABASE")); ++ } + manifest.ok_or_else(|| error("VSS_RESTORE_INVALID_MANIFEST")) +} +fn extract(bytes: &[u8], stage: &Path, ctx: &Context) -> Result<(), APIError> { @@ -6974,6 +7928,7 @@ index 0000000..e500852 + .tempdir_in(&ctx.base) + .map_err(|_| error("VSS_RESTORE_LOCAL_WRITE_FAILED"))?; + extract(&bytes, stage.path(), &ctx)?; ++ validate_local_database(&stage.path().join(&ctx.fp).join(DATABASE_NAME))?; + let after = client + .get_backup_version() + .await @@ -7093,15 +8048,35 @@ index 0000000..e500852 + } + fn manifest(ctx: &Context) -> Vec { + serde_json::to_vec(&serde_json::json!({"version":1,"bitcoin_network":"Signet","master_fingerprint":ctx.fp, -+ "account_xpub_vanilla":ctx.vanilla,"account_xpub_colored":ctx.colored,"vanilla_keychain":0,"witness_version":"Taproot"})).unwrap() ++ "account_xpub_vanilla":ctx.vanilla,"account_xpub_colored":ctx.colored,"vanilla_keychain":0,"witness_version":"Taproot", ++ "database_type":"Sqlite","max_allocations_per_utxo":5,"supported_schemas":["Nia","Uda","Cfa","Ifa","Bfa"]})).unwrap() ++ } ++ fn database_fixture() -> Vec { ++ // Archive admission checks only format/containment; actual SQLite integrity and wallet ++ // consistency remain native-open requirements, exercised by real restore integration. ++ let mut data = vec![0u8; 100]; ++ data[..16].copy_from_slice(SQLITE_HEADER); ++ data + } + fn archive(ctx: &Context, extra: &[(&str, &[u8])]) -> Vec { ++ archive_with_database(ctx, extra, Some(&database_fixture())) ++ } ++ fn archive_with_database( ++ ctx: &Context, ++ extra: &[(&str, &[u8])], ++ database: Option<&[u8]>, ++ ) -> Vec { + let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new())); + let options = SimpleFileOptions::default(); + zip.add_directory(format!("{}/", ctx.fp), options).unwrap(); + zip.start_file(format!("{}/wallet_manifest.json", ctx.fp), options) + .unwrap(); + zip.write_all(&manifest(ctx)).unwrap(); ++ if let Some(database) = database { ++ zip.start_file(format!("{}/{DATABASE_NAME}", ctx.fp), options) ++ .unwrap(); ++ zip.write_all(database).unwrap(); ++ } + for (path, bytes) in extra { + zip.start_file(*path, options).unwrap(); + zip.write_all(bytes).unwrap(); @@ -7119,6 +8094,7 @@ index 0000000..e500852 + #[cfg(unix)] + fs::set_permissions(&target, fs::Permissions::from_mode(0o700)).unwrap(); + private_file(&target.join("wallet_manifest.json"), &manifest(ctx)); ++ private_file(&target.join(DATABASE_NAME), &database_fixture()); + let fields = Fields { + schema_version: 1, + network: ctx.network.clone(), @@ -7157,6 +8133,59 @@ index 0000000..e500852 + assert!(archive_plan(&bytes, &other).is_err()); + } + #[test] ++ fn archive_requires_standalone_new_library_database() { ++ let temp = tempfile::tempdir().unwrap(); ++ let ctx = context(temp.path()); ++ assert!(archive_plan(&archive_with_database(&ctx, &[], None), &ctx).is_err()); ++ assert!(archive_plan( ++ &archive_with_database(&ctx, &[], Some(b"not a database")), ++ &ctx ++ ) ++ .is_err()); ++ let wrong_header = vec![0u8; 100]; ++ assert!( ++ archive_plan(&archive_with_database(&ctx, &[], Some(&wrong_header)), &ctx).is_err() ++ ); ++ for name in [ ++ "rgb_lib_db-wal", ++ "rgb_lib_db-shm", ++ "rgb_lib_db-journal", ++ "bdk_db", ++ "bdk_db_watch_only", ++ ] { ++ let path = format!("{}/{name}", ctx.fp); ++ assert!(archive_plan(&archive(&ctx, &[(path.as_str(), b"sidecar")]), &ctx).is_err()); ++ } ++ let path = format!("{}/another.db", ctx.fp); ++ assert!(archive_plan( ++ &archive(&ctx, &[(path.as_str(), &database_fixture())]), ++ &ctx ++ ) ++ .is_err()); ++ assert!(fs::read_dir(temp.path()).unwrap().next().is_none()); ++ } ++ #[test] ++ fn manifest_rejects_incomplete_or_unsupported_new_wallet_configuration() { ++ let temp = tempfile::tempdir().unwrap(); ++ let ctx = context(temp.path()); ++ for (field, value) in [ ++ ("database_type", serde_json::json!("Other")), ++ ("max_allocations_per_utxo", serde_json::json!(0)), ++ ("supported_schemas", serde_json::json!([])), ++ ("supported_schemas", serde_json::json!(["Bfa", "Bfa"])), ++ ("supported_schemas", serde_json::json!(["Unknown"])), ++ ("vanilla_keychain", serde_json::json!(1)), ++ ("witness_version", serde_json::json!("SegwitV0")), ++ ] { ++ let mut data: serde_json::Value = serde_json::from_slice(&manifest(&ctx)).unwrap(); ++ data[field] = value; ++ assert!(manifest_identity(&serde_json::to_vec(&data).unwrap(), &ctx).is_err()); ++ } ++ let mut missing: serde_json::Value = serde_json::from_slice(&manifest(&ctx)).unwrap(); ++ missing.as_object_mut().unwrap().remove("database_type"); ++ assert!(manifest_identity(&serde_json::to_vec(&missing).unwrap(), &ctx).is_err()); ++ } ++ #[test] + fn archive_rejects_traversal_absolute_unknown_roots_duplicates_and_reserved_keys() { + let temp = tempfile::tempdir().unwrap(); + let ctx = context(temp.path()); @@ -7198,6 +8227,9 @@ index 0000000..e500852 + zip.start_file(format!("{}/wallet_manifest.json", ctx.fp), options) + .unwrap(); + zip.write_all(&manifest(&ctx)).unwrap(); ++ zip.start_file(format!("{}/{DATABASE_NAME}", ctx.fp), options) ++ .unwrap(); ++ zip.write_all(&database_fixture()).unwrap(); + if symlink { + zip.add_symlink(format!("{}/link", ctx.fp), "../../outside", options) + .unwrap(); @@ -7523,16 +8555,19 @@ index 454f4ba..be65f0c 100644 assert!(validate_vss_url("ftp://example.com/vss", true).is_err()); diff --git a/src/vss_snapshot_policy.rs b/src/vss_snapshot_policy.rs new file mode 100644 -index 0000000..77b5c8a +index 0000000..bff8d11 --- /dev/null +++ b/src/vss_snapshot_policy.rs -@@ -0,0 +1,655 @@ +@@ -0,0 +1,699 @@ +//! Maintained snapshot policy and confirmed completion receipts. The public SDK -+//! client remains available with its built-in scheduler disabled; our serialized ++//! transport remains available with its built-in scheduler disabled; our serialized +//! mutation guard owns blocking automatic snapshots, so policy changes are barriers. -+use crate::error::APIError; ++use crate::{error::APIError, rgb_vss::VssBackupClient}; +use bitcoin::hashes::{sha256, Hash}; -+use rgb_lib::{wallet::Wallet, BitcoinNetwork}; ++use rgb_lib::{ ++ wallet::{RgbWalletOpsOffline, Wallet}, ++ BitcoinNetwork, ++}; +use serde::{Deserialize, Serialize}; +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt; @@ -7785,60 +8820,94 @@ index 0000000..77b5c8a + self.enabled.store(enabled, Ordering::SeqCst); + Ok(()) + } -+ pub(crate) fn backup(&self, wallet: &Wallet) -> Result { -+ let client = wallet.vss_client().ok_or_else(error)?; -+ let handle = client.handle().clone(); -+ write_atomic(&self.path.with_extension("pending"), b"1")?; -+ // Completion time is captured after the acknowledged upload, before any later status read. -+ let _runtime_context = handle.enter(); -+ // The client owns a live multi-thread runtime. Poll on this calling thread -+ // without nesting Tokio block_on, and let its workers drive network IO. -+ let before = futures::executor::block_on(client.get_backup_version()) -+ .map_err(|_| error())? -+ .unwrap_or(0); -+ let version = futures::executor::block_on(wallet.vss_backup(&client)).map_err(|_| { ++ /// The caller holds the RGB wallet mutex throughout preparation, upload and acknowledgement. ++ /// Taking an explicit client prevents this path from reacquiring the wrapper's wallet lock. ++ pub(crate) fn backup( ++ &self, ++ wallet: &mut Wallet, ++ client: &VssBackupClient, ++ ) -> Result { ++ self.unsaved.store(true, Ordering::SeqCst); ++ let result = (|| { ++ write_atomic(&self.path.with_extension("pending"), b"1")?; ++ if !client.encryption_enabled() { ++ return Err(error()); ++ } ++ let snapshot = wallet.prepare_snapshot().map_err(|_| error())?; ++ let data = snapshot.data; ++ // Drive HTTP on the client's dedicated runtime without nesting Tokio block_on on ++ // a host runtime thread. Keep local mutation admission closed until readback/ack. ++ let upload = std::thread::scope(|scope| { ++ scope ++ .spawn(|| { ++ client.handle().block_on(async { ++ let before = client ++ .get_backup_version() ++ .await ++ .map_err(|_| error())? ++ .unwrap_or(0); ++ let version = client.upload_backup(data).await.map_err(|_| error())?; ++ // This records actual acknowledged upload completion, not a later status read. ++ let completed_at_ms = now_ms()?; ++ let canonical = client ++ .get_backup_version() ++ .await ++ .map_err(|_| error())? ++ .ok_or_else(error)?; ++ Ok::<_, APIError>(( ++ version, ++ completed_at_ms, ++ confirmed_manifest_successor(before, canonical)?, ++ )) ++ }) ++ }) ++ .join() ++ }) ++ .map_err(|_| error())??; ++ let (version, completed_at_ms, canonical) = upload; ++ // A successful upload of stale content must never clear local dirty state. ++ if !wallet ++ .acknowledge_snapshot(&snapshot.token) ++ .map_err(|_| error())? ++ { ++ return Err(error()); ++ } ++ let receipt = Completion { ++ version: canonical, ++ completed_at_ms, ++ network: self.network.clone(), ++ vss_url: self.url.clone(), ++ node_id: self.node_id.clone(), ++ }; ++ write_atomic( ++ &self.completion_path(), ++ &serde_json::to_vec(&receipt).map_err(|_| error())?, ++ )?; ++ fs::remove_file(self.path.with_extension("pending")).map_err(|_| error())?; ++ sync_parent(&self.path)?; ++ self.unsaved.store(false, Ordering::SeqCst); ++ self.failed.store(false, Ordering::SeqCst); ++ Ok(version) ++ })(); ++ if result.is_err() { + self.failed.store(true, Ordering::SeqCst); -+ error() -+ })?; -+ let completed_at_ms = now_ms()?; -+ let canonical = futures::executor::block_on(client.get_backup_version()) -+ .map_err(|_| error())? -+ .ok_or_else(error)?; -+ let canonical = confirmed_manifest_successor(before, canonical)?; -+ let receipt = Completion { -+ version: canonical, -+ completed_at_ms, -+ network: self.network.clone(), -+ vss_url: self.url.clone(), -+ node_id: self.node_id.clone(), -+ }; -+ write_atomic( -+ &self.completion_path(), -+ &serde_json::to_vec(&receipt).map_err(|_| error())?, -+ )?; -+ fs::remove_file(self.path.with_extension("pending")).map_err(|_| error())?; -+ fs::File::open(self.path.parent().ok_or_else(error)?) -+ .and_then(|f| f.sync_all()) -+ .map_err(|_| error())?; -+ self.unsaved.store(false, Ordering::SeqCst); -+ self.failed.store(false, Ordering::SeqCst); -+ Ok(version) ++ } ++ result + } -+ pub(crate) fn after_mutation(&self, wallet: &Wallet) { ++ pub(crate) fn after_mutation(&self, wallet: &mut Wallet, client: &VssBackupClient) { + if self.unknown.load(Ordering::SeqCst) + || !self.enabled.load(Ordering::SeqCst) + || std::thread::panicking() + { + return; + } -+ // Explicit mutation admission also covers RGB stock/witness changes -+ // that the pinned SDK does not include in its database dirty flag. -+ if self.backup(wallet).is_err() { ++ // Explicit mutation admission also covers stock, consignment and proxy-key changes. ++ if self.backup(wallet, client).is_err() { + self.failed.store(true, Ordering::SeqCst); + tracing::warn!("Automatic VSS snapshot failed; wallet operation result is preserved"); + } + } -+ pub(crate) fn metadata(&self) -> Result { ++ fn completion(&self) -> Result, APIError> { + if self.unknown.load(Ordering::SeqCst) { + return Err(error()); + } @@ -7857,6 +8926,13 @@ index 0000000..77b5c8a + Some(r) + } + }; ++ Ok(receipt) ++ } ++ pub(crate) fn has_completed_backup(&self) -> Result { ++ Ok(self.completion()?.is_some()) ++ } ++ pub(crate) fn metadata(&self) -> Result { ++ let receipt = self.completion()?; + Ok(serde_json::json!({"schema_version":1,"automatic_backup_enabled":self.enabled.load(Ordering::SeqCst),"last_backup":receipt}).to_string()) + } +} diff --git a/scripts/burn-types.ts b/scripts/burn-types.ts index ba7441c..ec30d1a 100644 --- a/scripts/burn-types.ts +++ b/scripts/burn-types.ts @@ -20,3 +20,11 @@ node.burn({ ...request, min_confirmations: '1' }) node.burnAsset({ ...request, burn_recipient: new Uint8Array(32) }) // @ts-expect-error Decimal strings must be integer text. node.burn({ ...request, amount: '1.5' }) + +const sent = node.sendRgbInvoice({ invoice: 'rgb:invoice', asset_id: 'rgb:test', amount: '18446744073709551615', donation: false, fee_rate: '1', min_confirmations: 6 }) +const rgbTxid: string = sent.txid +void rgbTxid +// @ts-expect-error Confirmation policy is a numeric u8, never a decimal string. +node.sendRgbInvoice({ invoice: 'rgb:invoice', asset_id: 'rgb:test', amount: 1, donation: false, fee_rate: 1, min_confirmations: '6' }) +// @ts-expect-error Callers cannot override the invoice encryption policy. +node.sendRgbInvoice({ invoice: 'rgb:invoice', asset_id: 'rgb:test', amount: 1, donation: false, fee_rate: 1, min_confirmations: 6, encryption: 'Optional' }) diff --git a/scripts/contract.test.js b/scripts/contract.test.js index 5004a4b..857e808 100644 --- a/scripts/contract.test.js +++ b/scripts/contract.test.js @@ -66,7 +66,7 @@ function facade (overrides = {}) { const root = path.resolve(__dirname, '..') const identity = installer.identity(installer.readConfig()) const info = { abi_version: 1, rln_version: '0.17.0-beta.3', rln_commit: identity.commit, - lightning_commit: identity.lightningCommit, adapter_sha256: identity.patchSha256, + lightning_commit: identity.lightningCommit, rgb_lib_commit: identity.rgbLibCommit, adapter_sha256: identity.patchSha256, wrapper_sha256: identity.wrapperSha256, lock_sha256: identity.lockSha256, target: identity.target, capabilities: [], ...overrides } const native = { getRuntimeInfo: () => JSON.stringify(info) } @@ -290,3 +290,30 @@ test('automatic backup policy is boolean-only and completion metadata preserves failure = true assert.throws(() => node.setVssAutomaticBackupEnabled(false), /native policy failed/) }) + + +test('invoice-first send forwards full invoice, exact amounts and native failures once', () => { + const calls = [] + const error = new Error('Rln(InvalidRecipient): invoice expired') + let fail = false + const native = { sendRgbInvoice: (...args) => { + calls.push(args.at(-1)) + if (fail) throw error + return '{"txid":"abc","batch_transfer_idx":2147483647}' + } } + const node = new (facade().SdkNode)(native) + const request = { invoice: 'rgb:contract/recipient?pk=abc&enc=required&expiry=1893456000', + asset_id: 'rgb:contract', amount: '18446744073709551615', donation: false, + fee_rate: 2, min_confirmations: 6 } + assert.deepEqual(node.sendRgbInvoice(request), { txid: 'abc', batch_transfer_idx: 2147483647 }) + assert.equal(boundary.parse(calls[0]).invoice, request.invoice) + assert.match(calls[0], /"amount":18446744073709551615/) + for (const invalid of [{ ...request, amount: 9007199254740992 }, { ...request, public_key: 'override' }, + { ...request, donation: 'false' }, { ...request, witness_data: { amount_sat: -1 } }]) { + assert.throws(() => node.sendRgbInvoice(invalid)) + } + assert.equal(calls.length, 1) + fail = true + assert.throws(() => node.sendRgbInvoice(request), error) + assert.equal(calls.length, 2) +}) diff --git a/scripts/install-overlay-addon.js b/scripts/install-overlay-addon.js index 3599417..57069af 100644 --- a/scripts/install-overlay-addon.js +++ b/scripts/install-overlay-addon.js @@ -59,6 +59,8 @@ function identity (config) { ref: config.ref, commit: config.commit, lightningCommit: config.lightningCommit, + rgbLibRepository: config.rgbLibRepository, + rgbLibCommit: config.rgbLibCommit, patchSha256: config.patchSha256, rustToolchain: config.rustToolchain, macosDeploymentTarget: '13.0', diff --git a/scripts/install-overlay-addon.test.js b/scripts/install-overlay-addon.test.js index c6e14b3..cc0d834 100644 --- a/scripts/install-overlay-addon.test.js +++ b/scripts/install-overlay-addon.test.js @@ -66,10 +66,10 @@ test('manifest rejects wrong artifact, source, wrapper, lock and target identiti assert.equal(manifestMatches(config, null, 'a'.repeat(64)), false) }) -test('wrapper lock uses the released RGB-lib and path transaction sync', () => { +test('wrapper lock uses the pinned BFA library and path transaction sync', () => { const lock = fs.readFileSync(path.join(__dirname, '..', 'Cargo.lock'), 'utf8') - assert.match(lock, /rgb-lib\.git\?tag=v0\.3\.0-beta\.50-bfa#1b951af0c8233308c919975552487be19ad2a930/) - assert.doesNotMatch(lock, /95332c41|94b6221|dcorral/) + assert.match(lock, /rgb-lib-bfa\.git\?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655/) + assert.doesNotMatch(lock, /rgb-lib\.git\?|rgb-(?:consensus|ops|schemas)-s-bfa|95332c41|94b6221|dcorral/) const sync = lock.split('[[package]]').find(block => block.includes('name = "lightning-transaction-sync"')) assert.ok(sync) assert.ok(!sync.includes('source =')) diff --git a/scripts/invoice-boundary.test.js b/scripts/invoice-boundary.test.js new file mode 100644 index 0000000..f5fda78 --- /dev/null +++ b/scripts/invoice-boundary.test.js @@ -0,0 +1,59 @@ +'use strict' + +const assert = require('node:assert/strict') +const test = require('node:test') +const { sendRgbInvoiceRequest, parse } = require('../json-boundary') + +const request = Object.freeze({ + invoice: 'rgb:contract/recipient?pk=' + 'ab'.repeat(32) + '&enc=required&expiry=1893456000', + asset_id: 'rgb:contract', + amount: '9007199254740993', + donation: false, + fee_rate: 2, + min_confirmations: 6 +}) + +test('invoice-first sends preserve exact invoice and u64 numeric tokens', () => { + const raw = sendRgbInvoiceRequest(request) + assert.match(raw, /"amount":9007199254740993/) + assert.equal(parse(raw).invoice, request.invoice) + assert.equal(parse(raw).amount, request.amount) + assert.equal(parse(raw).donation, false) + assert.equal(parse(raw).witness_data, null) + const max = sendRgbInvoiceRequest({ ...request, amount: '18446744073709551615', fee_rate: '1', + witness_data: Object.freeze({ amount_sat: '9007199254740993', blinding: '18446744073709551615' }) }) + assert.equal(parse(max).witness_data.blinding, '18446744073709551615') + assert.equal(parse(max).witness_data.amount_sat, '9007199254740993') + assert.equal(parse(max).fee_rate, 1) +}) + +test('lossy amounts and caller-supplied proxy policy never reach serialization', () => { + for (const field of ['amount', 'fee_rate']) { + for (const value of [0, -1, 1.5, NaN, Infinity, 9007199254740992, '0', '01', '1e3', '-1', '1.0', + '18446744073709551616', null, undefined, {}, true, 1n]) { + assert.throws(() => sendRgbInvoiceRequest({ ...request, [field]: value })) + } + } + for (const field of ['public_key', 'encryption', 'sender_proxy_url', 'expiration_timestamp', 'skip_sync', 'toJSON']) { + assert.throws(() => sendRgbInvoiceRequest({ ...request, [field]: 'override' }), /Unknown/) + } + for (const value of [null, undefined, [], 'invoice']) assert.throws(() => sendRgbInvoiceRequest(value)) +}) + +test('invoice bounds, witness integers and confirmation policy are strict', () => { + for (const invoice of ['', 'rgb:invoice\0ignored', 'rgb:invoice\n', ' rgb:invoice', 'a'.repeat(16385), null, {}, 1]) { + assert.throws(() => sendRgbInvoiceRequest({ ...request, invoice })) + } + for (const confirmations of [-1, 256, 1.5, '6', null]) { + assert.throws(() => sendRgbInvoiceRequest({ ...request, min_confirmations: confirmations })) + } + for (const donation of [undefined, null, 'false', 0, 1, {}]) { + assert.throws(() => sendRgbInvoiceRequest({ ...request, donation })) + } + for (const witness of [[], 1, {}, { amount_sat: 0 }, { amount_sat: 600, blinding: -1 }, + { amount_sat: 600, public_key: 'override' }, { amount_sat: '18446744073709551616' }]) { + assert.throws(() => sendRgbInvoiceRequest({ ...request, witness_data: witness })) + } + assert.deepEqual(parse(sendRgbInvoiceRequest({ ...request, witness_data: { amount_sat: 600 } })).witness_data, + { amount_sat: 600, blinding: null }) +}) diff --git a/scripts/native-admission.test.js b/scripts/native-admission.test.js index adc60c5..44eef54 100644 --- a/scripts/native-admission.test.js +++ b/scripts/native-admission.test.js @@ -22,7 +22,7 @@ function setup (t) { fs.writeFileSync(sidecar, JSON.stringify({ ...expected, addonSha256: sha256(artifact) })) let loaded = 0 let initialized = 0 - const info = { abi_version: 1, rln_version: config.rlnVersion, rln_commit: expected.commit, lightning_commit: expected.lightningCommit, + const info = { abi_version: 1, rln_version: config.rlnVersion, rln_commit: expected.commit, lightning_commit: expected.lightningCommit, rgb_lib_commit: expected.rgbLibCommit, adapter_sha256: expected.patchSha256, wrapper_sha256: expected.wrapperSha256, lock_sha256: expected.lockSha256, target: expected.target, capabilities: [] } const localRequire = createRequire(path.resolve(__dirname, '../index.js')) diff --git a/scripts/release-contract.js b/scripts/release-contract.js index 24f6f63..7a0871a 100644 --- a/scripts/release-contract.js +++ b/scripts/release-contract.js @@ -6,12 +6,14 @@ const path = require('node:path') const { execFileSync } = require('node:child_process') const RELEASE = Object.freeze({ - rlnVersion: '0.17.0-beta.3', - ref: 'ecc69f20308231400561f6f060fea88acfa40cc4', - commit: 'ecc69f20308231400561f6f060fea88acfa40cc4', - lightningCommit: '79b9eb5166546c62692d3a672986f385c7fe047d', - rustToolchain: '1.94.0', - cffiLockSha256: '5fcdc9ad9179d3230e48973cdfa514a863d13a837752027febf8ea04713bbeb5' + 'rlnVersion': '0.17.0-beta.3', + 'ref': 'cd3be114a6d0e41af8ce3f29719d210e105234dc', + 'commit': 'cd3be114a6d0e41af8ce3f29719d210e105234dc', + 'lightningCommit': '510189d5700a6d1406229ce7523e181c3838715e', + 'rgbLibRepository': 'https://github.com/UTEXO-Protocol/rgb-lib-bfa.git', + 'rgbLibCommit': '962730a211f0f7569d7ad8ae777052f433d87655', + 'rustToolchain': '1.94.0', + 'cffiLockSha256': '460728009d9c1db3abf9a2f2ad4d76f777589b55eff96ff7be1f3c4d301dab30' }) // Exact reviewed C adapter plus the shared native lifecycle repair. This remains // an upstream release with an explicitly hashed runtime overlay, not plain RLN. @@ -29,6 +31,7 @@ const ALLOWED_FILES = Object.freeze([ 'src/ldk.rs', 'src/lib.rs', 'src/rgb.rs', + 'src/rgb_import.rs', 'src/vss_snapshot_policy.rs', 'src/uniffi_api/types.rs', 'src/ldk_chain_backend/block_sync.rs', @@ -52,15 +55,11 @@ const ALLOWED_FILES = Object.freeze([ ]) const GIT_SOURCES = Object.freeze({ 'musig2': 'git+https://github.com/arik-so/rust-musig2?rev=6f95a05718cbb44d8fe3fa6021aea8117aa38d50#6f95a05718cbb44d8fe3fa6021aea8117aa38d50', - 'rgb-consensus': 'git+https://github.com/UTEXO-Protocol/rgb-consensus-s-bfa?rev=2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757#2faf6118b2f5fe30dd90dd5244d5a7dc32bcd757', - 'rgb-invoicing': 'git+https://github.com/UTEXO-Protocol/rgb-ops-s-bfa?rev=773766b5ca7a9e705ec68c2f9ed0917f505cd6e3#773766b5ca7a9e705ec68c2f9ed0917f505cd6e3', - 'rgb-lib': 'git+https://github.com/UTEXO-Protocol/rgb-lib.git?tag=v0.3.0-beta.50-bfa#1b951af0c8233308c919975552487be19ad2a930', - 'rgb-lib-migration': 'git+https://github.com/UTEXO-Protocol/rgb-lib.git?tag=v0.3.0-beta.50-bfa#1b951af0c8233308c919975552487be19ad2a930', - 'rgb-ops': 'git+https://github.com/UTEXO-Protocol/rgb-ops-s-bfa?rev=773766b5ca7a9e705ec68c2f9ed0917f505cd6e3#773766b5ca7a9e705ec68c2f9ed0917f505cd6e3', - 'rgb-schemas': 'git+https://github.com/UTEXO-Protocol/rgb-schemas-s-bfa?rev=238dfe7a89db998107fd87d919bf2757a2746d7f#238dfe7a89db998107fd87d919bf2757a2746d7f', - 'signer-external': 'git+https://github.com/UTEXO-Protocol/rln-external-signer.git?branch=main#0fb005ec4b927ddbe13e1646d247b5bb11e8ffed', - 'vls-core': 'git+https://github.com/UTEXO-Protocol/vls-core.git?branch=feat/rgb-compatibility#45c72edfd58620849eb486925439a76526b415ae', - 'vls-protocol-signer': 'git+https://github.com/UTEXO-Protocol/vls-protocol-signer.git?branch=feat/rgb-compatibility#dddd336fcdf84c4febdd7af8363bda8d1721d893', + 'rgb-lib': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655', + 'rgb-lib-migration': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655', + 'signer-external': 'git+https://github.com/UTEXO-Protocol/rln-external-signer.git?rev=0fb005ec4b927ddbe13e1646d247b5bb11e8ffed#0fb005ec4b927ddbe13e1646d247b5bb11e8ffed', + 'vls-core': 'git+https://github.com/UTEXO-Protocol/vls-core.git?rev=45c72edfd58620849eb486925439a76526b415ae#45c72edfd58620849eb486925439a76526b415ae', + 'vls-protocol-signer': 'git+https://github.com/UTEXO-Protocol/vls-protocol-signer.git?rev=dddd336fcdf84c4febdd7af8363bda8d1721d893#dddd336fcdf84c4febdd7af8363bda8d1721d893', 'vss-client-ng': 'git+https://github.com/lightningdevkit/vss-client?rev=ad63805047561dcf3117cd992ad6d8bc3881adef#ad63805047561dcf3117cd992ad6d8bc3881adef' }) diff --git a/scripts/source-credential.js b/scripts/source-credential.js index f56c31a..e565e24 100644 --- a/scripts/source-credential.js +++ b/scripts/source-credential.js @@ -3,9 +3,7 @@ const fs = require('node:fs') const repositories = new Set([ - 'UTEXO-Protocol/rgb-consensus-s-bfa', - 'UTEXO-Protocol/rgb-ops-s-bfa', - 'UTEXO-Protocol/rgb-schemas-s-bfa' + 'UTEXO-Protocol/rgb-lib-bfa' ]) function credential (operation, input, token) { diff --git a/scripts/source-credential.test.js b/scripts/source-credential.test.js index 456a3d8..53f6a67 100644 --- a/scripts/source-credential.test.js +++ b/scripts/source-credential.test.js @@ -5,12 +5,12 @@ const test = require('node:test') const { credential } = require('./source-credential') test('source credentials are limited to exact private repositories and never stored', () => { - const input = 'protocol=https\nhost=github.com\npath=UTEXO-Protocol/rgb-consensus-s-bfa.git\n' + const input = 'protocol=https\nhost=github.com\npath=UTEXO-Protocol/rgb-lib-bfa.git\n' assert.match(credential('get', input, 'fixture-only'), /password=fixture-only/) for (const operation of ['store', 'erase']) assert.equal(credential(operation, input, 'fixture-only'), '') assert.equal(credential('get', input, undefined), '') for (const modified of [input.replace('https', 'http'), input.replace('github.com', 'github.com.attacker'), - input.replace('rgb-consensus-s-bfa', 'other-repo'), input.replace('UTEXO-Protocol/', '')]) { + input.replace('rgb-lib-bfa', 'other-repo'), input.replace('rgb-lib-bfa', 'rgb-consensus-s-bfa'), input.replace('UTEXO-Protocol/', '')]) { assert.equal(credential('get', modified, 'fixture-only'), '') } assert.throws(() => credential('get', input, 'bad\ntoken'), /Invalid/) diff --git a/scripts/with-source-access.js b/scripts/with-source-access.js index 1fe599a..744a62a 100644 --- a/scripts/with-source-access.js +++ b/scripts/with-source-access.js @@ -4,7 +4,7 @@ const { spawnSync } = require('node:child_process') const path = require('node:path') if (!process.env.ORG_READ_TOKEN) { - throw new Error('Configure ORG_READ_TOKEN with read-only access to the three pinned BFA source repositories; public package consumers must use qualified prebuilds.') + throw new Error('Configure ORG_READ_TOKEN with read-only access to the pinned rgb-lib-bfa source repository; public package consumers must use qualified prebuilds.') } const [command, ...args] = process.argv.slice(2) if (!command) throw new Error('A build command is required') diff --git a/src/lib.rs b/src/lib.rs index 09ca5ef..6c8caf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,8 @@ use rlncffi::{ rln_sdk_node_unlock_with_native_external_signer, rln_sdk_node_vss_backup, rln_sdk_node_vss_backup_info, rln_sdk_node_vss_backup_metadata, rln_sdk_node_vss_clear_fence, rln_sdk_node_vss_restore_info, rln_send_btc, rln_send_onion_message, rln_send_payment, - rln_send_rgb, rln_sign_message, rln_sign_message_bip322, rln_sync, rln_taker, - rln_verify_message, rln_verify_message_bip322, rln_verify_message_bip322_offline, + rln_send_rgb, rln_send_rgb_invoice, rln_sign_message, rln_sign_message_bip322, rln_sync, + rln_taker, rln_verify_message, rln_verify_message_bip322, rln_verify_message_bip322_offline, COpaqueStruct, CResultString, CResultValue, }; @@ -665,6 +665,10 @@ impl SdkNode { pub fn send_rgb(&self, request_json: String) -> Result { fwd_json_req!(self, rln_send_rgb, request_json) } + #[napi] + pub fn send_rgb_invoice(&self, request_json: String) -> Result { + fwd_json_req!(self, rln_send_rgb_invoice, request_json) + } /// Preserve the released per-batch refresh status and failure response. #[napi] From 0000868bc7376e1738ba298d1352136c5f960790 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 20 Oct 2026 17:57:47 +0530 Subject: [PATCH 2/7] Test native lifecycle and VSS with fresh address semantics --- scripts/test-bip322-host.js | 28 +++++++++++++++++++++++----- scripts/test-vss-live-host.js | 20 ++++++++++++++++---- scripts/test-vss-policy-host.js | 3 ++- test-worker-teardown.js | 3 ++- test.js | 10 +++++++--- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/scripts/test-bip322-host.js b/scripts/test-bip322-host.js index 893be1f..b8a852b 100644 --- a/scripts/test-bip322-host.js +++ b/scripts/test-bip322-host.js @@ -1,5 +1,6 @@ /* global Bare */ 'use strict' +// rgb-lib-bfa requires fresh addresses; address reuse is explicitly unsupported. const bare = typeof Bare !== 'undefined' const fs = require(bare ? 'bare-fs' : 'node:fs') const os = require(bare ? 'bare-os' : 'node:os') @@ -34,7 +35,7 @@ try { // Public fixture seed. Never fund this wallet. signer = api.NativeExternalSigner.createWithStorage('01'.repeat(32), 'regtest', path.join(root, 'signer')) node = api.SdkNode.create({ storage_dir_path: path.join(root, 'node'), network: 'regtest', daemon_listening_port: 0, - ldk_peer_listening_port: 0, max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, reuse_addresses: true }) + ldk_peer_listening_port: 0, max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, reuse_addresses: false }) rejects(() => node.getMessageSigningAddress()) rejects(() => node.signMessageBip322('locked', 'bcrt1ptest')) if (cycle === 0) node.initWithNativeExternalSigner(signer) @@ -42,10 +43,27 @@ try { const address = node.getMessageSigningAddress() if (saved) assert(saved === address, 'Canonical address changed on signer reopen') saved = address - const receive = node.address().address - assert(receive === address, 'First vanilla wallet receive address does not match the canonical signing address') - assert(node.getMessageSigningAddress() === address) - assert(node.address().address === receive, 'Signing getter rotated receive address') + // The same deterministic wallet without signing-getter calls provides a + // control address sequence. Fresh-address requests advance; signing must not. + let control, controlSigner + try { + controlSigner = api.NativeExternalSigner.createWithStorage('01'.repeat(32), 'regtest', path.join(root, 'control-signer')) + control = api.SdkNode.create({ storage_dir_path: path.join(root, 'control-node'), network: 'regtest', + daemon_listening_port: 0, ldk_peer_listening_port: 0, max_media_upload_size_mb: 5, + enable_virtual_channels_v0: false, reuse_addresses: false }) + if (cycle === 0) control.initWithNativeExternalSigner(controlSigner) + control.unlockWithNativeExternalSigner(controlSigner, request) + const receive = node.address().address + assert(receive === control.address().address, 'Signing getter advanced the fresh receive sequence') + if (cycle === 0) assert(receive === address, 'First vanilla receive key differs from the canonical signing key') + assert(node.getMessageSigningAddress() === address) + const nextReceive = node.address().address + assert(nextReceive !== receive, 'Fresh receive requests reused an address') + assert(nextReceive === control.address().address, 'Signing getter rotated the receive sequence') + } finally { + if (control) control.shutdown() + if (controlSigner) { controlSigner.shutdown(); controlSigner.destroy() } + } const transactions = JSON.stringify(node.listTransactions(true)) for (const message of ['', ' exact bytes\n', 'Unicode: é e\u0301 😄', 'NUL\0inside', 'a'.repeat(4096)]) { const signature = node.signMessageBip322(message, address) diff --git a/scripts/test-vss-live-host.js b/scripts/test-vss-live-host.js index e7771b1..662169d 100644 --- a/scripts/test-vss-live-host.js +++ b/scripts/test-vss-live-host.js @@ -1,5 +1,6 @@ /* global Bare */ 'use strict' +// rgb-lib-bfa requires fresh addresses; address reuse is explicitly unsupported. // Opt-in disposable-wallet qualification. Never runs during package installation. const bare = typeof Bare !== 'undefined' const fs = require(bare ? 'bare-fs' : 'node:fs') @@ -28,7 +29,7 @@ const versions = [] const infoRows = [] const config = (directory, allowEmpty) => ({ storage_dir_path: path.join(root, directory), network, daemon_listening_port: 0, ldk_peer_listening_port: 0, max_media_upload_size_mb: 5, - enable_virtual_channels_v0: false, reuse_addresses: true, vss_url: endpoint, + enable_virtual_channels_v0: false, reuse_addresses: false, vss_url: endpoint, vss_allow_http: false, vss_allow_empty_restore: allowEmpty }) const unlock = { ldk_chain_sync: { mode: 'TransactionSync', config: { indexer_url: indexer } }, indexer_url: indexer, proxy_endpoint: proxy, eth_rpc_url: eth, announce_addresses: [] } @@ -66,6 +67,7 @@ try { open('original', true, false) const initial = status() assert(initial.backup_exists, 'Initial configured wallet did not create a remote snapshot') + const signingAddress = node.getMessageSigningAddress() const address = node.address().address const checkpoint = node.vssBackup() assert(checkpoint.version !== undefined, 'Checkpoint returned no version') @@ -76,18 +78,28 @@ try { close() open('original', false, false) - assert(node.address().address === address, 'Graceful reopen changed the wallet address') + assert(node.getMessageSigningAddress() === signingAddress, 'Graceful reopen changed the canonical signing key') + const reopenedAddress = node.address().address + assert(reopenedAddress !== address, 'Graceful reopen reused an already revealed receive address') assert(status().backup_exists, 'Graceful reopen lost remote backup information') + versions.push(node.vssBackup().version) + // Keep the remote checkpoint fixed, then observe the next local receive key. + // A fresh-directory restore must recover that exact derivation cursor. + node.setVssAutomaticBackupEnabled(false) + const checkpointVersion = status().version + const restoredNextAddress = node.address().address + assert(status().version === checkpointVersion && status().backup_required, 'Paused local derivation unexpectedly replaced the remote checkpoint') close() open('restored', true, false, true) - assert(node.address().address === address, 'Fresh-directory restore did not recover wallet address state') + assert(node.getMessageSigningAddress() === signingAddress, 'Fresh-directory restore changed the canonical signing key') + assert(node.address().address === restoredNextAddress, 'Fresh-directory restore did not recover the fresh-address derivation cursor') assert(status().backup_exists, 'Restored wallet did not read its remote snapshot') versions.push(node.vssBackup().version) close() console.log(JSON.stringify({ vssLiveQualification: 'pass', runtime: bare ? 'bare' : 'node', network, initialBackup: true, explicitCheckpoint: true, gracefulReopen: true, - freshDirectoryAddressRestore: true, versions, infoRows, funded: false, bridgeAccessed: false, + freshDirectoryAddressRestore: true, freshAddressCursorRestore: true, versions, infoRows, funded: false, bridgeAccessed: false, remoteErasureClaimed: false })) } finally { close() diff --git a/scripts/test-vss-policy-host.js b/scripts/test-vss-policy-host.js index 58bd2e4..683a3f9 100644 --- a/scripts/test-vss-policy-host.js +++ b/scripts/test-vss-policy-host.js @@ -1,5 +1,6 @@ /* global Bare */ 'use strict' +// rgb-lib-bfa requires fresh addresses; address reuse is explicitly unsupported. const bare = typeof Bare !== 'undefined' const fs = require(bare ? 'bare-fs' : 'node:fs') const os = require(bare ? 'bare-os' : 'node:os') @@ -26,7 +27,7 @@ let fundedQualified = false let syntheticClockReceiptQualified = false const config = { storage_dir_path: path.join(root, 'wallet'), network, daemon_listening_port: 0, ldk_peer_listening_port: 0, max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, - reuse_addresses: true, vss_url: endpoint, vss_allow_http: false, vss_allow_empty_restore: false } + reuse_addresses: false, vss_url: endpoint, vss_allow_http: false, vss_allow_empty_restore: false } const unlock = { ldk_chain_sync: { mode: 'TransactionSync', config: { indexer_url: indexer } }, indexer_url: indexer, proxy_endpoint: proxy, eth_rpc_url: eth, announce_addresses: [] } function open (initialize, override) { diff --git a/test-worker-teardown.js b/test-worker-teardown.js index 4226c0c..a56b1bd 100644 --- a/test-worker-teardown.js +++ b/test-worker-teardown.js @@ -1,4 +1,5 @@ 'use strict' +// rgb-lib-bfa requires fresh addresses; address reuse is explicitly unsupported. const assert = require('node:assert/strict') const fs = require('node:fs') @@ -22,7 +23,7 @@ if (!isMainThread) { network: 'regtest', max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, - reuse_addresses: true + reuse_addresses: false }) global.node.initWithNativeExternalSigner(global.signer) global.node.attachNativeExternalSigner(global.signer) diff --git a/test.js b/test.js index c8144ad..c23904c 100644 --- a/test.js +++ b/test.js @@ -1,4 +1,5 @@ 'use strict' +// rgb-lib-bfa requires fresh addresses; address reuse is explicitly unsupported. const assert = require('node:assert/strict') const fs = require('node:fs') @@ -29,6 +30,9 @@ try { signer = NativeExternalSigner.createWithStorage('01'.repeat(32), 'regtest', signerDir) bootstrap = signer.bootstrap() assert.match(bootstrap.node_id, /^(02|03)[a-f0-9]{64}$/) + assert.throws(() => SdkNode.create({ storage_dir_path: path.join(root, 'unsupported-reuse'), + network: 'regtest', daemon_listening_port: 0, ldk_peer_listening_port: 0, + max_media_upload_size_mb: 5, reuse_addresses: true }), /Address reuse is not supported by rgb-lib-bfa/) node = SdkNode.create({ storage_dir_path: nodeDir, daemon_listening_port: 0, @@ -36,7 +40,7 @@ try { network: 'regtest', max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, - reuse_addresses: true + reuse_addresses: false }) assert.throws(() => node.apayNewWithAddress('02'.repeat(33), 'canary', 'example.com'), /NotInitialized/) assert.equal(typeof node.burn, 'function') @@ -83,7 +87,7 @@ try { const expectedBootstrap = retainedSigner.bootstrap() retainedNode = SdkNode.create({ storage_dir_path: path.join(retainedRoot, 'node'), daemon_listening_port: 0, ldk_peer_listening_port: 0, network: 'regtest', - max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, reuse_addresses: true }) + max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, reuse_addresses: false }) retainedNode.initWithNativeExternalSigner(retainedSigner) retainedNode.attachNativeExternalSigner(retainedSigner) retainedSigner.destroy() @@ -105,7 +109,7 @@ console.log('Early host-handle release preserves the attached signer until node const mainnetRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rln-mainnet-policy-')) const mainnet = SdkNode.create({ storage_dir_path: mainnetRoot, daemon_listening_port: 0, ldk_peer_listening_port: 0, network: 'mainnet', - max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, reuse_addresses: true }) + max_media_upload_size_mb: 5, enable_virtual_channels_v0: false, reuse_addresses: false }) try { assert.throws(() => mainnet.lnInvoice({ amt_msat: 1000, expiry_sec: 3600 }), /LightningUnsupportedOnMainnet/) assert.throws(() => mainnet.listChannels(), /LightningUnsupportedOnMainnet/) From b17a0dc63c40dfa6152144d6eb51c2dff5bf02f0 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 20 Oct 2026 18:34:33 +0530 Subject: [PATCH 3/7] Clarify locally funded Lightning channel migration blocker --- BFA-MIGRATION.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BFA-MIGRATION.md b/BFA-MIGRATION.md index 318fcb9..4b3c99e 100644 --- a/BFA-MIGRATION.md +++ b/BFA-MIGRATION.md @@ -20,7 +20,9 @@ Use canonical decimal strings when values may exceed JavaScript's safe integer r Iris remains on-chain only. Signer shutdown, retained-clone revocation, owned task/listener cleanup, native external signing, BIP-322, recovery access and the durable default-on VSS policy are preserved in the maintained adapter. VSS manifest v2 uses immutable generation keys and confirms the canonical remote commit before acknowledging a content-bound snapshot; interrupted/uncertain outcomes retain pending state. Fresh migration test stores are required; this branch does not claim old manifest format migration. -The new library does not provide old IFA linking, deferred consume for channel sweeps or custom channel funding locktime. RLN rejects these specific unsupported boundaries before signing or unsafe mutation. Compilation is not generic Lightning/channel qualification. The external-signer VSS public-bootstrap limitation is unchanged and must not be worked around by supplying a seed to RGB. +The new library does not provide old IFA linking, deferred consume for channel sweeps or explicit final-zero-locktime channel funding construction. **This candidate cannot open ordinary locally funded BTC or RGB Lightning channels.** The colored open preflight and funding event pass `Some(0)` to the guarded RGB builder; the vanilla funding event calls the guarded Bitcoin funding builder. Colored virtual funding is also blocked. These are terminal funding failures before signing, not merely a restriction on optional user-selected locktimes. Peer-funded incoming channels, existing-channel payments and uncolored synthetic virtual channels are separate paths and remain unqualified. + +Ordinary on-chain BTC and complete-invoice RGB transfers use separate supported builders. Restoring channel funding requires a library-owned funding API that sets final zero locktime before persisting txid-bound reservations and RGB consignments, including matching dry-run/end/abort behavior. Removing the guard or editing the returned PSBT is unsafe. This migration leaves that work explicit rather than claiming generic Lightning parity. The external-signer VSS public-bootstrap limitation is unchanged and must not be worked around by supplying a seed to RGB. The supplied Signet public contract is **Bridged USDT / BUSDT / 6**, asset `rgb:OG9No4hZ-UrxoWmR-09ohtOk-ppxBN7h-g~KkQro-ncKc9A4`, SHA-256 `dfb49261fed5ad3dd237dd3a03913793b76fe70e486372bbeb0769b82158a895`. Native import validates it and creates no balance. Funded regtest tests must use a fresh local BFA contract with its own identity. From 82c628300329fdd48ca4f84af0215b032b946cf1 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 20 Oct 2026 18:58:23 +0530 Subject: [PATCH 4/7] Pin contract error and shared-cache build repairs --- package.json | 6 +++--- patches/release-adapter-v0.17.0-beta.3.patch | 8 ++++---- scripts/release-contract.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 39b00a1..6e593d9 100644 --- a/package.json +++ b/package.json @@ -60,11 +60,11 @@ "author": "UTEXO", "utexoNativeOverlay": { "repository": "https://github.com/UTEXO-Protocol/rgb-lightning-node.git", - "ref": "cd3be114a6d0e41af8ce3f29719d210e105234dc", - "commit": "cd3be114a6d0e41af8ce3f29719d210e105234dc", + "ref": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", + "commit": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", "lightningCommit": "510189d5700a6d1406229ce7523e181c3838715e", "patch": "patches/release-adapter-v0.17.0-beta.3.patch", - "patchSha256": "3662f1e7d97f5bb540b1e6cb0bce98a2e0b243868417e481bcd0c27e15819702", + "patchSha256": "cf92a4618b765e7889001c9e630e6e64861ef9bdd295d57551461d3bc3281d4c", "cffiLockSha256": "460728009d9c1db3abf9a2f2ad4d76f777589b55eff96ff7be1f3c4d301dab30", "rustToolchain": "1.94.0", "rlnVersion": "0.17.0-beta.3", diff --git a/patches/release-adapter-v0.17.0-beta.3.patch b/patches/release-adapter-v0.17.0-beta.3.patch index 5bc97cc..0ae2ad6 100644 --- a/patches/release-adapter-v0.17.0-beta.3.patch +++ b/patches/release-adapter-v0.17.0-beta.3.patch @@ -1709,7 +1709,7 @@ index 5b606f1..9c9c59e 100644 } } diff --git a/bindings/c-ffi/src/lib.rs b/bindings/c-ffi/src/lib.rs -index e1dded1..42e6332 100644 +index e1dded1..ecc18b1 100644 --- a/bindings/c-ffi/src/lib.rs +++ b/bindings/c-ffi/src/lib.rs @@ -89,6 +89,33 @@ pub extern "C" fn rln_free_string(s: *mut c_char) { @@ -1722,7 +1722,7 @@ index e1dded1..42e6332 100644 + ffi_call!("rln_binding_build_info", Ok::(serde_json::json!({ + "abi_version": 1, + "rln_version": "0.17.0-beta.3", -+ "rln_commit": "cd3be114a6d0e41af8ce3f29719d210e105234dc", ++ "rln_commit": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", + "lightning_commit": "510189d5700a6d1406229ce7523e181c3838715e", + "rgb_lib_commit": "962730a211f0f7569d7ad8ae777052f433d87655", + "adapter_sha256": env!("RLN_ADAPTER_SHA256"), @@ -5031,10 +5031,10 @@ index 83202d4..be14b41 100644 async fn wallet_source_does_not_panic_on_signetcustom() { let wrapper = signetcustom_wallet_source(); diff --git a/src/rgb_import.rs b/src/rgb_import.rs -index 225897d..00ff24e 100644 +index a665290..135045c 100644 --- a/src/rgb_import.rs +++ b/src/rgb_import.rs -@@ -205,7 +205,7 @@ pub(crate) async fn import_rgb_contract( +@@ -209,7 +209,7 @@ pub(crate) async fn import_rgb_contract( .to_str() .ok_or_else(|| APIError::InvalidRgbContract("Invalid temporary contract path".into()))? .to_owned(); diff --git a/scripts/release-contract.js b/scripts/release-contract.js index 7a0871a..085a0c2 100644 --- a/scripts/release-contract.js +++ b/scripts/release-contract.js @@ -7,8 +7,8 @@ const { execFileSync } = require('node:child_process') const RELEASE = Object.freeze({ 'rlnVersion': '0.17.0-beta.3', - 'ref': 'cd3be114a6d0e41af8ce3f29719d210e105234dc', - 'commit': 'cd3be114a6d0e41af8ce3f29719d210e105234dc', + 'ref': 'c29d1b2dfe43ba95c25ced8984f09e42c789d6af', + 'commit': 'c29d1b2dfe43ba95c25ced8984f09e42c789d6af', 'lightningCommit': '510189d5700a6d1406229ce7523e181c3838715e', 'rgbLibRepository': 'https://github.com/UTEXO-Protocol/rgb-lib-bfa.git', 'rgbLibCommit': '962730a211f0f7569d7ad8ae777052f433d87655', From f73d9c23c7b71b4f4e8e74d6f0610c4992c664ee Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 20 Oct 2026 20:44:59 +0530 Subject: [PATCH 5/7] Preserve content-aware VSS refresh snapshots --- package.json | 2 +- patches/release-adapter-v0.17.0-beta.3.patch | 166 +++++++++++++++++-- 2 files changed, 150 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 6e593d9..072ad1e 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "commit": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", "lightningCommit": "510189d5700a6d1406229ce7523e181c3838715e", "patch": "patches/release-adapter-v0.17.0-beta.3.patch", - "patchSha256": "cf92a4618b765e7889001c9e630e6e64861ef9bdd295d57551461d3bc3281d4c", + "patchSha256": "26834a8aca4676380abb41c432f7ebbc33153a58b669213cf1f32472d38db275", "cffiLockSha256": "460728009d9c1db3abf9a2f2ad4d76f777589b55eff96ff7be1f3c4d301dab30", "rustToolchain": "1.94.0", "rlnVersion": "0.17.0-beta.3", diff --git a/patches/release-adapter-v0.17.0-beta.3.patch b/patches/release-adapter-v0.17.0-beta.3.patch index 0ae2ad6..cf53812 100644 --- a/patches/release-adapter-v0.17.0-beta.3.patch +++ b/patches/release-adapter-v0.17.0-beta.3.patch @@ -4213,7 +4213,7 @@ index d504a55..9294786 100644 /// left in `uniffi_api::handle_from_request`) must be able to configure the remote-signer daemon /// address; without this field, `NodeHandle`-embedding callers could configure external-signer diff --git a/src/rgb.rs b/src/rgb.rs -index 83202d4..be14b41 100644 +index 83202d4..8afd8bf 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -285,7 +285,7 @@ impl CommonState { @@ -4650,16 +4650,19 @@ index 83202d4..be14b41 100644 } pub(crate) fn refresh( -@@ -1161,7 +1226,7 @@ impl RgbLibWalletWrapper { +@@ -1161,7 +1226,10 @@ impl RgbLibWalletWrapper { filter: Vec, skip_sync: bool, ) -> Result { - self.get_rgb_wallet() -+ self.get_rgb_wallet_mutating() ++ // Even skip_sync refresh can receive proxy consignments/keys or settle a transfer. ++ // Admit that mutation before dispatch, but suppress an upload only when the full ++ // acknowledged snapshot identity proves that all wallet content stayed unchanged. ++ self.get_rgb_wallet_for_sync(false) .refresh(self.online, asset_id, filter, skip_sync) } -@@ -1170,7 +1235,7 @@ impl RgbLibWalletWrapper { +@@ -1170,7 +1238,7 @@ impl RgbLibWalletWrapper { consignment: RgbTransfer, offchain_txid: String, ) -> Result<(), RgbLibError> { @@ -4668,7 +4671,7 @@ index 83202d4..be14b41 100644 .save_new_asset(self.online, consignment, offchain_txid) } -@@ -1183,7 +1248,7 @@ impl RgbLibWalletWrapper { +@@ -1183,7 +1251,7 @@ impl RgbLibWalletWrapper { expiration_timestamp: u64, sender_endpoint: Option, ) -> Result { @@ -4677,7 +4680,7 @@ index 83202d4..be14b41 100644 self.online, recipient_map, donation, -@@ -1211,7 +1276,7 @@ impl RgbLibWalletWrapper { +@@ -1211,7 +1279,7 @@ impl RgbLibWalletWrapper { details: "Custom RGB funding locktime is unavailable in the BFA RGB library".into(), }); } @@ -4686,7 +4689,7 @@ index 83202d4..be14b41 100644 self.online, recipient_map, donation, -@@ -1230,7 +1295,7 @@ impl RgbLibWalletWrapper { +@@ -1230,7 +1298,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { @@ -4695,7 +4698,7 @@ index 83202d4..be14b41 100644 .send_btc(self.online, address, amount, fee_rate, skip_sync) } -@@ -1241,8 +1306,14 @@ impl RgbLibWalletWrapper { +@@ -1241,8 +1309,14 @@ impl RgbLibWalletWrapper { fee_rate: u64, dry_run: bool, ) -> Result { @@ -4712,7 +4715,7 @@ index 83202d4..be14b41 100644 } pub(crate) fn send_btc_channel_funding_begin( -@@ -1261,18 +1332,20 @@ impl RgbLibWalletWrapper { +@@ -1261,18 +1335,20 @@ impl RgbLibWalletWrapper { } pub(crate) fn send_btc_end(&self, signed_psbt: String) -> Result { @@ -4736,7 +4739,7 @@ index 83202d4..be14b41 100644 .send_end_db_update_only(self.online, signed_psbt) } -@@ -1281,7 +1354,7 @@ impl RgbLibWalletWrapper { +@@ -1281,7 +1357,7 @@ impl RgbLibWalletWrapper { } pub(crate) fn sync(&self, options: SyncOptions) -> Result<(), RgbLibError> { @@ -4745,7 +4748,7 @@ index 83202d4..be14b41 100644 } pub(crate) fn update_witnesses( -@@ -1289,7 +1362,7 @@ impl RgbLibWalletWrapper { +@@ -1289,7 +1365,7 @@ impl RgbLibWalletWrapper { after_height: u32, force_witnesses: Vec, ) -> Result { @@ -4754,7 +4757,7 @@ index 83202d4..be14b41 100644 .update_witnesses(self.online, after_height, force_witnesses) } -@@ -1298,7 +1371,7 @@ impl RgbLibWalletWrapper { +@@ -1298,7 +1374,7 @@ impl RgbLibWalletWrapper { witness_id: RgbTxid, witness_ord: WitnessOrd, ) -> Result<(), RgbLibError> { @@ -4763,7 +4766,7 @@ index 83202d4..be14b41 100644 .upsert_witness(witness_id, witness_ord) } -@@ -1310,7 +1383,7 @@ impl RgbLibWalletWrapper { +@@ -1310,7 +1386,7 @@ impl RgbLibWalletWrapper { transport_endpoints: Vec, min_confirmations: u8, ) -> Result { @@ -4772,7 +4775,7 @@ index 83202d4..be14b41 100644 Some(self.online), asset_id, assignment, -@@ -1413,7 +1486,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1413,7 +1489,7 @@ impl WalletSource for RgbLibWalletWrapper { Box::pin(async move { let network = Network::from(self.bitcoin_network()); let unspents = self @@ -4781,7 +4784,7 @@ index 83202d4..be14b41 100644 .list_unspents_vanilla(self.online, 1, false) .map_err(|e| { tracing::error!(error = %e, "failed to list unspents for fee bumping"); -@@ -1456,7 +1529,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1456,7 +1532,7 @@ impl WalletSource for RgbLibWalletWrapper { fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()> { Box::pin(async move { @@ -4790,7 +4793,7 @@ index 83202d4..be14b41 100644 tracing::error!(error = %e, "failed to get change address for fee bumping"); })?; let parsed = Address::from_str(&address).map_err(|e| { -@@ -1725,6 +1798,239 @@ mod wallet_source_tests { +@@ -1725,6 +1801,368 @@ mod wallet_source_tests { RgbLibWalletWrapper::new(Arc::new(Mutex::new(wallet)), Online { id: 0 }) } @@ -4942,6 +4945,128 @@ index 83202d4..be14b41 100644 + + #[cfg(feature = "vss")] + #[test] ++ fn refresh_error_without_content_change_preserves_clean_acknowledged_snapshot() { ++ for skip_sync in [false, true] { ++ let tmp = tempfile::tempdir().unwrap(); ++ let policy = Arc::new( ++ crate::vss_snapshot_policy::SnapshotPolicy::load( ++ tmp.path(), ++ BitcoinNetwork::SignetCustom, ++ "https://example.invalid/vss".into(), ++ "test-node".into(), ++ true, ++ ) ++ .unwrap(), ++ ); ++ let mut wrapper = signetcustom_wallet_source(); ++ wrapper.snapshot_policy = Some(policy.clone()); ++ completed_snapshot_fixture(&wrapper, tmp.path()); ++ // Exercise the actual named refresh adapter. Its offline fixture error does ++ // not change content, so neither a pending marker nor an attempted automatic ++ // upload may remain. No client is attached: an attempted upload would fail. ++ assert!(matches!( ++ wrapper.refresh(None, vec![], skip_sync), ++ Err(RgbLibError::Offline) ++ )); ++ assert!(!policy.pending()); ++ assert!(!policy.failed.load(std::sync::atomic::Ordering::SeqCst)); ++ assert!(!wrapper.get_rgb_wallet().backup_info().unwrap()); ++ } ++ } ++ ++ #[cfg(feature = "vss")] ++ #[test] ++ fn refresh_preserves_fresh_missing_receipt_prior_pending_and_failed_states() { ++ for state in ["fresh", "missing_receipt", "pending", "failed"] { ++ for skip_sync in [false, true] { ++ let tmp = tempfile::tempdir().unwrap(); ++ let policy = Arc::new( ++ crate::vss_snapshot_policy::SnapshotPolicy::load( ++ tmp.path(), ++ BitcoinNetwork::SignetCustom, ++ "https://example.invalid/vss".into(), ++ "test-node".into(), ++ false, ++ ) ++ .unwrap(), ++ ); ++ let mut wrapper = signetcustom_wallet_source(); ++ wrapper.snapshot_policy = Some(policy.clone()); ++ if state != "fresh" { ++ completed_snapshot_fixture(&wrapper, tmp.path()); ++ } ++ if state == "missing_receipt" { ++ let receipt = std::fs::read_dir(tmp.path()) ++ .unwrap() ++ .map(|entry| entry.unwrap().path()) ++ .find(|path| path.to_string_lossy().ends_with(".completion.json")) ++ .unwrap(); ++ std::fs::remove_file(receipt).unwrap(); ++ } ++ if state == "pending" || state == "failed" { ++ policy.before_mutation(); ++ } ++ if state == "failed" { ++ policy ++ .failed ++ .store(true, std::sync::atomic::Ordering::SeqCst); ++ } ++ assert!(matches!( ++ wrapper.refresh(None, vec![], skip_sync), ++ Err(RgbLibError::Offline) ++ )); ++ assert!(policy.pending(), "lost {state}, skip_sync={skip_sync}"); ++ assert_eq!( ++ policy.failed.load(std::sync::atomic::Ordering::SeqCst), ++ state == "failed" ++ ); ++ } ++ } ++ } ++ ++ #[cfg(feature = "vss")] ++ #[test] ++ fn content_aware_refresh_guard_retains_secret_file_and_derivation_mutations() { ++ for mutation in ["secret_file", "derivation"] { ++ let tmp = tempfile::tempdir().unwrap(); ++ let policy = Arc::new( ++ crate::vss_snapshot_policy::SnapshotPolicy::load( ++ tmp.path(), ++ BitcoinNetwork::SignetCustom, ++ "https://example.invalid/vss".into(), ++ "test-node".into(), ++ false, ++ ) ++ .unwrap(), ++ ); ++ let mut wrapper = signetcustom_wallet_source(); ++ wrapper.snapshot_policy = Some(policy.clone()); ++ completed_snapshot_fixture(&wrapper, tmp.path()); ++ let before = wrapper.get_btc_balance(true).unwrap(); ++ { ++ let mut guard = wrapper.get_rgb_wallet_for_sync(false); ++ if mutation == "secret_file" { ++ // Stand-in for a newly persisted operation key. Its bytes must ++ // participate without any balance/history/derivation change. ++ std::fs::write( ++ guard ++ .get_wallet_dir() ++ .join("proxy-operation-secret.fixture"), ++ [0x55; 32], ++ ) ++ .unwrap(); ++ } else { ++ guard.get_address().unwrap(); ++ } ++ } ++ assert_eq!(wrapper.get_btc_balance(true).unwrap(), before); ++ assert!(policy.pending(), "lost {mutation}"); ++ assert!(wrapper.get_rgb_wallet().backup_info().unwrap()); ++ } ++ } ++ ++ #[cfg(feature = "vss")] ++ #[test] + fn every_adapter_state_mutator_uses_explicit_snapshot_admission() { + let source = include_str!("rgb.rs"); + let adapter = source @@ -4971,7 +5096,6 @@ index 83202d4..be14b41 100644 + "accept_transfer_consignment", + "provide_out_of_band_ack", + "provide_out_of_band_consignment", -+ "refresh", + "save_new_asset", + "send", + "send_begin", @@ -4996,6 +5120,14 @@ index 83202d4..be14b41 100644 + "{method} lacks snapshot admission" + ); + } ++ let refresh = adapter ++ .split("fn refresh(") ++ .nth(1) ++ .unwrap() ++ .split("pub(crate) fn ") ++ .next() ++ .unwrap(); ++ assert!(refresh.contains("get_rgb_wallet_for_sync(false)")); + for method in [ + "get_asset_balance", + "get_asset_metadata", From 3339951bf8e8cdc2de00b9499c78e6bd744c93cc Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 20 Oct 2026 23:05:18 +0530 Subject: [PATCH 6/7] fix: preserve VSS restore after public manifest updates --- package.json | 2 +- patches/release-adapter-v0.17.0-beta.3.patch | 202 ++++++++++++++++++- 2 files changed, 195 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 072ad1e..f846713 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "commit": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", "lightningCommit": "510189d5700a6d1406229ce7523e181c3838715e", "patch": "patches/release-adapter-v0.17.0-beta.3.patch", - "patchSha256": "26834a8aca4676380abb41c432f7ebbc33153a58b669213cf1f32472d38db275", + "patchSha256": "ae0b1f29d2f81b8b5d219b9e749f97238ce1cc86777cb07a9c01d5e7fdfd88f9", "cffiLockSha256": "460728009d9c1db3abf9a2f2ad4d76f777589b55eff96ff7be1f3c4d301dab30", "rustToolchain": "1.94.0", "rlnVersion": "0.17.0-beta.3", diff --git a/patches/release-adapter-v0.17.0-beta.3.patch b/patches/release-adapter-v0.17.0-beta.3.patch index cf53812..60e2cb5 100644 --- a/patches/release-adapter-v0.17.0-beta.3.patch +++ b/patches/release-adapter-v0.17.0-beta.3.patch @@ -7451,10 +7451,10 @@ index 1bf91a1..2251e5b 100644 } diff --git a/src/uniffi_api/vss_restore.rs b/src/uniffi_api/vss_restore.rs new file mode 100644 -index 0000000..913f303 +index 0000000..1d36b2c --- /dev/null +++ b/src/uniffi_api/vss_restore.rs -@@ -0,0 +1,1030 @@ +@@ -0,0 +1,1216 @@ +//! Maintained external-signer RGB restore admission. Uses public transport and +//! crypto APIs, with bounded extraction and a local, destination-bound receipt. +use crate::rgb_vss::{VssBackupClient, VssBackupConfig}; @@ -7608,11 +7608,20 @@ index 0000000..913f303 + engine.input(&bytes); + Ok(Hmac::::from_engine(engine).to_string()) +} -+fn private_read(path: &Path, maximum: u64) -> Result, APIError> { ++#[derive(Clone, Copy)] ++enum LocalReadPolicy { ++ Private, ++ PublicManifest, ++} ++fn bounded_local_read( ++ path: &Path, ++ maximum: u64, ++ policy: LocalReadPolicy, ++) -> Result, APIError> { + let mut options = OpenOptions::new(); + options.read(true); + #[cfg(unix)] -+ options.custom_flags(libc::O_NOFOLLOW); ++ options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK); + let file = options + .open(path) + .map_err(|_| error("VSS_RESTORE_INVALID_RECEIPT"))?; @@ -7620,10 +7629,24 @@ index 0000000..913f303 + .metadata() + .map_err(|_| error("VSS_RESTORE_INVALID_RECEIPT"))?; + #[cfg(unix)] -+ if meta.uid() != unsafe { libc::geteuid() } || meta.mode() & 0o777 != 0o600 || meta.nlink() != 1 + { -+ return Err(error("VSS_RESTORE_INVALID_RECEIPT")); ++ let permissions = meta.mode() & 0o7777; ++ let permitted = match policy { ++ LocalReadPolicy::Private => permissions == 0o600, ++ // rgb-lib atomically rewrites its explicitly non-secret manifest on ++ // every Wallet::new. Under the usual umask 022 it becomes 0644. ++ // Receipt/key files remain strictly private; the manifest still has ++ // to be owned, single-linked, non-writable by others and identity-bound. ++ LocalReadPolicy::PublicManifest => { ++ permissions & 0o400 != 0 && permissions & !0o644 == 0 ++ } ++ }; ++ if meta.uid() != unsafe { libc::geteuid() } || !permitted || meta.nlink() != 1 { ++ return Err(error("VSS_RESTORE_INVALID_RECEIPT")); ++ } + } ++ #[cfg(not(unix))] ++ let _ = policy; + if !meta.is_file() || meta.len() > maximum { + return Err(error("VSS_RESTORE_INVALID_RECEIPT")); + } @@ -7636,6 +7659,12 @@ index 0000000..913f303 + } + Ok(bytes) +} ++fn private_read(path: &Path, maximum: u64) -> Result, APIError> { ++ bounded_local_read(path, maximum, LocalReadPolicy::Private) ++} ++fn public_manifest_read(path: &Path) -> Result, APIError> { ++ bounded_local_read(path, MAX_MANIFEST, LocalReadPolicy::PublicManifest) ++} +fn read_key(ctx: &Context) -> Result, APIError> { + let key = private_read(&ctx.base.join(KEY), 32)?; + if key.len() != 32 { @@ -7813,7 +7842,7 @@ index 0000000..913f303 + { + return Err(error("VSS_RESTORE_INVALID_RECEIPT")); + } -+ let manifest = private_read(&target.join("wallet_manifest.json"), MAX_MANIFEST)?; ++ let manifest = public_manifest_read(&target.join("wallet_manifest.json"))?; + if manifest.len() as u64 > MAX_MANIFEST + || manifest_identity(&manifest, ctx)? != f.manifest_identity_sha256 + { @@ -8227,6 +8256,10 @@ index 0000000..913f303 + fs::set_permissions(&target, fs::Permissions::from_mode(0o700)).unwrap(); + private_file(&target.join("wallet_manifest.json"), &manifest(ctx)); + private_file(&target.join(DATABASE_NAME), &database_fixture()); ++ install_receipt_for_existing_wallet(ctx) ++ } ++ fn install_receipt_for_existing_wallet(ctx: &Context) -> Fields { ++ let target = ctx.base.join(&ctx.fp); + let fields = Fields { + schema_version: 1, + network: ctx.network.clone(), @@ -8235,7 +8268,11 @@ index 0000000..913f303 + version: 7, + endpoint_sha256: ctx.endpoint_hash.clone(), + target_sha256: ctx.target_hash.clone(), -+ manifest_identity_sha256: manifest_identity(&manifest(ctx), ctx).unwrap(), ++ manifest_identity_sha256: manifest_identity( ++ &fs::read(target.join("wallet_manifest.json")).unwrap(), ++ ctx, ++ ) ++ .unwrap(), + }; + let key = receipt_key(ctx, &open_directory(&ctx.base).unwrap()).unwrap(); + private_file( @@ -8484,6 +8521,155 @@ index 0000000..913f303 + std::os::unix::fs::symlink(&saved, &key).unwrap(); + assert!(load_receipt(&ctx).is_err()); + } ++ ++ #[cfg(unix)] ++ #[test] ++ fn real_watch_only_wallet_rewrite_under_umask_022_retains_restore_provenance() { ++ // Change umask only in a dedicated child process running this single test. ++ // Other tests and the invoking process never inherit a changed umask. ++ const CHILD: &str = "UTEXO_TEST_PUBLIC_MANIFEST_UMASK_022_CHILD"; ++ if std::env::var_os(CHILD).is_none() { ++ let output = std::process::Command::new("/bin/sh") ++ .args(["-c", "umask 022; exec \"$@\"", "vss-manifest-test"]) ++ .arg(std::env::current_exe().unwrap()) ++ .args([ ++ "--exact", ++ "uniffi_api::vss_restore::tests::real_watch_only_wallet_rewrite_under_umask_022_retains_restore_provenance", ++ "--nocapture", ++ "--test-threads=1", ++ ]) ++ .env(CHILD, "1") ++ .output() ++ .unwrap(); ++ assert!( ++ output.status.success() ++ && String::from_utf8_lossy(&output.stdout).contains("running 1 test") ++ && String::from_utf8_lossy(&output.stdout).contains("1 passed"), ++ "isolated wallet-reopen regression failed: {}{}", ++ String::from_utf8_lossy(&output.stdout), ++ String::from_utf8_lossy(&output.stderr) ++ ); ++ return; ++ } ++ use rgb_lib::{ ++ keys::{generate_keys, WitnessVersion}, ++ wallet::{DatabaseType, RgbWalletOpsOffline, SinglesigKeys, Wallet, WalletData}, ++ AssetSchema, BitcoinNetwork, ++ }; ++ let temp = tempfile::tempdir().unwrap(); ++ let restored = tempfile::tempdir().unwrap(); ++ let generated = generate_keys(BitcoinNetwork::Signet, WitnessVersion::Taproot); ++ let keys = SinglesigKeys { ++ account_xpub_vanilla: generated.account_xpub_vanilla, ++ account_xpub_colored: generated.account_xpub_colored, ++ vanilla_keychain: None, ++ master_fingerprint: generated.master_fingerprint, ++ mnemonic: None, ++ witness_version: WitnessVersion::Taproot, ++ }; ++ let mut data = WalletData { ++ data_dir: temp.path().display().to_string(), ++ bitcoin_network: BitcoinNetwork::Signet, ++ database_type: DatabaseType::Sqlite, ++ max_allocations_per_utxo: 5, ++ supported_schemas: vec![AssetSchema::Bfa], ++ }; ++ let mut ctx = context(restored.path()); ++ ctx.fp = keys.master_fingerprint.clone(); ++ ctx.vanilla = keys.account_xpub_vanilla.clone(); ++ ctx.colored = keys.account_xpub_colored.clone(); ++ ctx.target_hash = digest(ctx.base.join(&ctx.fp).as_os_str().as_encoded_bytes()); ++ let mut source_wallet = Wallet::new(data.clone(), keys.clone()).unwrap(); ++ let snapshot = source_wallet.prepare_snapshot().unwrap(); ++ drop(source_wallet); ++ // Use the genuine consistent DB snapshot and private extraction path, ++ // rather than treating a fresh public-permission DB as restored state. ++ archive_plan(&snapshot.data, &ctx).unwrap(); ++ extract(&snapshot.data, restored.path(), &ctx).unwrap(); ++ data.data_dir = restored.path().display().to_string(); ++ let target = ctx.base.join(&ctx.fp); ++ let manifest = target.join("wallet_manifest.json"); ++ assert_eq!(fs::metadata(&manifest).unwrap().mode() & 0o777, 0o600); ++ assert_eq!( ++ fs::metadata(target.join(DATABASE_NAME)).unwrap().mode() & 0o777, ++ 0o600 ++ ); ++ install_receipt_for_existing_wallet(&ctx); ++ let key_before = fs::read(ctx.base.join(KEY)).unwrap(); ++ let receipt_before = fs::read(target.join(RECEIPT)).unwrap(); ++ assert_eq!(load_receipt(&ctx).unwrap().version, 7); ++ ++ // Exercise the real library's unconditional atomic manifest rewrite. ++ drop(Wallet::new(data, keys).unwrap()); ++ assert_eq!(fs::metadata(&manifest).unwrap().mode() & 0o777, 0o644); ++ assert_eq!(load_receipt(&ctx).unwrap().version, 7); ++ assert_eq!(fs::read(ctx.base.join(KEY)).unwrap(), key_before); ++ assert_eq!(fs::read(target.join(RECEIPT)).unwrap(), receipt_before); ++ } ++ ++ #[cfg(unix)] ++ #[test] ++ fn public_manifest_permissions_never_relax_private_receipt_or_key_permissions() { ++ let temp = tempfile::tempdir().unwrap(); ++ let ctx = context(temp.path()); ++ install_receipt(&ctx); ++ let target = ctx.base.join(&ctx.fp); ++ let manifest = target.join("wallet_manifest.json"); ++ for mode in [0o400, 0o440, 0o444, 0o600, 0o604, 0o640, 0o644] { ++ fs::set_permissions(&manifest, fs::Permissions::from_mode(mode)).unwrap(); ++ assert_eq!(load_receipt(&ctx).unwrap().version, 7); ++ } ++ for mode in [ ++ 0o602, 0o620, 0o622, 0o660, 0o666, 0o700, 0o755, 0o1644, 0o4600, ++ ] { ++ fs::set_permissions(&manifest, fs::Permissions::from_mode(mode)).unwrap(); ++ assert!( ++ load_receipt(&ctx).is_err(), ++ "accepted manifest mode {mode:o}" ++ ); ++ } ++ fs::set_permissions(&manifest, fs::Permissions::from_mode(0o644)).unwrap(); ++ for secret in [ctx.base.join(KEY), target.join(RECEIPT)] { ++ fs::set_permissions(&secret, fs::Permissions::from_mode(0o644)).unwrap(); ++ assert!(load_receipt(&ctx).is_err()); ++ fs::set_permissions(&secret, fs::Permissions::from_mode(0o600)).unwrap(); ++ assert_eq!(load_receipt(&ctx).unwrap().version, 7); ++ } ++ } ++ ++ #[cfg(unix)] ++ #[test] ++ fn public_manifest_rejects_links_special_files_oversize_and_identity_changes() { ++ let temp = tempfile::tempdir().unwrap(); ++ let ctx = context(temp.path()); ++ install_receipt(&ctx); ++ let target = ctx.base.join(&ctx.fp); ++ let path = target.join("wallet_manifest.json"); ++ let saved = ctx.base.join("saved-manifest"); ++ fs::hard_link(&path, &saved).unwrap(); ++ assert!(load_receipt(&ctx).is_err()); ++ fs::remove_file(&saved).unwrap(); ++ fs::rename(&path, &saved).unwrap(); ++ std::os::unix::fs::symlink(&saved, &path).unwrap(); ++ assert!(load_receipt(&ctx).is_err()); ++ fs::remove_file(&path).unwrap(); ++ let fifo = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).unwrap(); ++ assert_eq!(unsafe { libc::mkfifo(fifo.as_ptr(), 0o600) }, 0); ++ assert!(public_manifest_read(&path).is_err()); ++ fs::remove_file(&path).unwrap(); ++ private_file(&path, &vec![b' '; MAX_MANIFEST as usize + 1]); ++ assert!(load_receipt(&ctx).is_err()); ++ let mut changed: serde_json::Value = ++ serde_json::from_slice(&fs::read(&saved).unwrap()).unwrap(); ++ changed["account_xpub_colored"] = serde_json::json!("other-public-identity"); ++ fs::write(&path, serde_json::to_vec(&changed).unwrap()).unwrap(); ++ fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap(); ++ assert!(load_receipt(&ctx).is_err()); ++ fs::remove_file(&path).unwrap(); ++ fs::rename(&saved, &path).unwrap(); ++ fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap(); ++ assert_eq!(load_receipt(&ctx).unwrap().version, 7); ++ } +} diff --git a/src/utils.rs b/src/utils.rs index 454f4ba..be65f0c 100644 From ce1032cc0ece3171e1e6cc17870733c2751a6045 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Wed, 21 Oct 2026 00:38:28 +0530 Subject: [PATCH 7/7] Budget and time Darwin x64 native matrix builds --- .github/workflows/runtime-matrix.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/runtime-matrix.yml b/.github/workflows/runtime-matrix.yml index 09ca10e..18f0e62 100644 --- a/.github/workflows/runtime-matrix.yml +++ b/.github/workflows/runtime-matrix.yml @@ -37,7 +37,7 @@ jobs: - runner: ubuntu-24.04-arm target: linux-arm64-gnu runs-on: ${{ matrix.runner }} - timeout-minutes: 90 + timeout-minutes: ${{ matrix.target == 'darwin-x64' && 150 || 90 }} env: CARGO_BUILD_JOBS: 2 CARGO_PROFILE_RELEASE_DEBUG: 0 @@ -54,7 +54,13 @@ jobs: with: toolchain: '1.94.0' rustflags: '' - - run: node scripts/with-source-access.js bash scripts/runtime-matrix.sh + - run: | + if [[ "$EXPECTED_TARGET" == "darwin-x64" ]]; then + mkdir -p "$RUNNER_TEMP/node-runtime-timing" + exec /usr/bin/time -l -o "$RUNNER_TEMP/node-runtime-timing/darwin-x64.txt" \ + node scripts/with-source-access.js bash scripts/runtime-matrix.sh + fi + exec node scripts/with-source-access.js bash scripts/runtime-matrix.sh if: runner.os == 'macOS' env: ORG_READ_TOKEN: ${{ secrets.ORG_READ_TOKEN }} @@ -78,6 +84,12 @@ jobs: index-*.provenance.json *.tgz if-no-files-found: warn + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() && matrix.target == 'darwin-x64' + with: + name: ci-timing-darwin-x64-${{ github.sha }} + path: ${{ runner.temp }}/node-runtime-timing/darwin-x64.txt + if-no-files-found: warn musl-runtime: runs-on: ubuntu-22.04