From 98341ed4672688d8987a79437ecbbd7dce759d98 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 20 Oct 2026 17:44:47 +0530 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 From 5f9b0e9a59e59d265f900a8e5f79a2644058a1ca Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Fri, 23 Oct 2026 12:09:14 +0530 Subject: [PATCH 08/11] Pin bfa.1 gateway fixes and verify compiled native source identities --- BFA-MIGRATION.md | 16 ++- Cargo.lock | 4 +- package-lock.json | 4 +- package.json | 14 +-- patches/release-adapter-v0.17.0-beta.3.patch | 118 +++++++++---------- scripts/compiled-runtime-identity.test.js | 87 ++++++++++++++ scripts/install-overlay-addon.test.js | 2 +- scripts/release-contract.js | 37 ++++-- 8 files changed, 202 insertions(+), 80 deletions(-) create mode 100644 scripts/compiled-runtime-identity.test.js diff --git a/BFA-MIGRATION.md b/BFA-MIGRATION.md index 4b3c99e..cba8fe7 100644 --- a/BFA-MIGRATION.md +++ b/BFA-MIGRATION.md @@ -1,6 +1,20 @@ # 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. +Candidate version: `0.2.0-bfa.1`. 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. + + +## Strict gateway and proxy compatibility + +Candidate `0.2.0-bfa.1` sends an empty JSON-RPC parameter array for EVM methods taking no arguments. Public-network startup requires an explicitly configured protocol-1.0 RGB proxy; the obsolete protocol-0.2 public fallback is removed. The local regtest proxy default remains supported and is validated by the same preflight. + +| Source | Exact revision | +| --- | --- | +| RGB library | `89c37086a68ef792da75c62bce59b58b1d7952a0` | +| LDK | `bbe68189872c26088e351d034b05c58769cb996a` | +| RLN | `d10d679f0dd73246f6b475a44bdf13edd5f9b2a4` | +| Maintained adapter SHA-256 | `db4511e71a1c770def1aeeb5f1ab5e72e07fdd228627d6d744dce56b4b3bdcfe` | + +This graph requires fresh native builds and separately frozen candidate archives. Previous `0.2.0-bfa.0` artifacts and qualification records retain their original identities and do not qualify this candidate. The candidate adds `SdkNode.sendRgbInvoice` for protocol-1.0 proxy transfers. Pass the complete invoice without reconstructing its recipient keys or encryption fields: diff --git a/Cargo.lock b/Cargo.lock index a49d6db..f0d3c36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4886,7 +4886,7 @@ dependencies = [ [[package]] name = "rgb-lib" version = "0.3.0-beta.7" -source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655" +source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" dependencies = [ "amplify", "base64 0.23.1", @@ -4932,7 +4932,7 @@ dependencies = [ [[package]] name = "rgb-lib-migration" version = "0.3.0-beta.5" -source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655" +source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" dependencies = [ "sea-orm-migration 2.0.2", "tokio", diff --git a/package-lock.json b/package-lock.json index 03e48c8..3aac189 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@utexo/rgb-lightning-node-nodejs", - "version": "0.2.0-bfa.0", + "version": "0.2.0-bfa.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@utexo/rgb-lightning-node-nodejs", - "version": "0.2.0-bfa.0", + "version": "0.2.0-bfa.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index f846713..f8a2c7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@utexo/rgb-lightning-node-nodejs", - "version": "0.2.0-bfa.0", + "version": "0.2.0-bfa.1", "description": "Node.js (napi-rs) bindings for rgb-lightning-node", "keywords": [ "rgb", @@ -60,16 +60,16 @@ "author": "UTEXO", "utexoNativeOverlay": { "repository": "https://github.com/UTEXO-Protocol/rgb-lightning-node.git", - "ref": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", - "commit": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", - "lightningCommit": "510189d5700a6d1406229ce7523e181c3838715e", + "ref": "d10d679f0dd73246f6b475a44bdf13edd5f9b2a4", + "commit": "d10d679f0dd73246f6b475a44bdf13edd5f9b2a4", + "lightningCommit": "bbe68189872c26088e351d034b05c58769cb996a", "patch": "patches/release-adapter-v0.17.0-beta.3.patch", - "patchSha256": "ae0b1f29d2f81b8b5d219b9e749f97238ce1cc86777cb07a9c01d5e7fdfd88f9", - "cffiLockSha256": "460728009d9c1db3abf9a2f2ad4d76f777589b55eff96ff7be1f3c4d301dab30", + "patchSha256": "db4511e71a1c770def1aeeb5f1ab5e72e07fdd228627d6d744dce56b4b3bdcfe", + "cffiLockSha256": "3039a36910386f9d6f350ed33791cd6461174cd340247998b80726a6581c3763", "rustToolchain": "1.94.0", "rlnVersion": "0.17.0-beta.3", "rgbLibRepository": "https://github.com/UTEXO-Protocol/rgb-lib-bfa.git", - "rgbLibCommit": "962730a211f0f7569d7ad8ae777052f433d87655" + "rgbLibCommit": "89c37086a68ef792da75c62bce59b58b1d7952a0" }, "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 60e2cb5..6114055 100644 --- a/patches/release-adapter-v0.17.0-beta.3.patch +++ b/patches/release-adapter-v0.17.0-beta.3.patch @@ -1,5 +1,5 @@ diff --git a/bindings/c-ffi/Cargo.lock b/bindings/c-ffi/Cargo.lock -index be5eede..5fda28c 100644 +index be5eede..980a432 100644 --- a/bindings/c-ffi/Cargo.lock +++ b/bindings/c-ffi/Cargo.lock @@ -95,8 +95,6 @@ dependencies = [ @@ -418,7 +418,7 @@ index be5eede..5fda28c 100644 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" ++source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" dependencies = [ "amplify", - "base64 0.22.1", @@ -460,7 +460,7 @@ index be5eede..5fda28c 100644 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" ++source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" dependencies = [ "sea-orm-migration 2.0.2", "tokio", @@ -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..ecc18b1 100644 +index e1dded1..92908fd 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,9 +1722,9 @@ index e1dded1..ecc18b1 100644 + ffi_call!("rln_binding_build_info", Ok::(serde_json::json!({ + "abi_version": 1, + "rln_version": "0.17.0-beta.3", -+ "rln_commit": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af", -+ "lightning_commit": "510189d5700a6d1406229ce7523e181c3838715e", -+ "rgb_lib_commit": "962730a211f0f7569d7ad8ae777052f433d87655", ++ "rln_commit": "d10d679f0dd73246f6b475a44bdf13edd5f9b2a4", ++ "lightning_commit": "bbe68189872c26088e351d034b05c58769cb996a", ++ "rgb_lib_commit": "89c37086a68ef792da75c62bce59b58b1d7952a0", + "adapter_sha256": env!("RLN_ADAPTER_SHA256"), + "wrapper_sha256": env!("RLN_WRAPPER_SHA256"), + "lock_sha256": env!("RLN_LOCK_SHA256"), @@ -2130,7 +2130,7 @@ index a58949a..8c72dfa 100644 boolean reuse_addresses = false; }; diff --git a/src/ldk.rs b/src/ldk.rs -index 86035df..78b61f7 100644 +index 3bc967e..3bc9c7b 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -108,18 +108,17 @@ use std::collections::HashSet; @@ -2164,8 +2164,8 @@ index 86035df..78b61f7 100644 + 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, -@@ -337,14 +336,290 @@ pub(crate) fn node_override_matches( + }; +@@ -336,14 +335,290 @@ pub(crate) fn node_override_matches( .is_some_and(|id| *id == our_node_id) } @@ -2457,7 +2457,7 @@ index 86035df..78b61f7 100644 #[derive(Clone, Debug)] pub(crate) struct PaymentInfo { pub(crate) preimage: Option, -@@ -1099,12 +1374,14 @@ impl LightningState { +@@ -1098,12 +1373,14 @@ impl LightningState { /// `FutureSpawner` backed by `tokio::spawn`, used to drive async monitor /// persistence completions for `MonitorUpdatingPersisterAsync`. #[cfg(feature = "vss")] @@ -2474,7 +2474,7 @@ index 86035df..78b61f7 100644 } } -@@ -3621,18 +3898,26 @@ async fn handle_ldk_events( +@@ -3620,18 +3897,26 @@ async fn handle_ldk_events( .await } Event::ConnectionNeeded { node_id, addresses } => { @@ -2510,7 +2510,7 @@ index 86035df..78b61f7 100644 } Event::SplicePending { .. } => { // We don't use the splice feature, so this event should never be seen. -@@ -4817,6 +5102,19 @@ impl NodeStartup { +@@ -4848,6 +5133,19 @@ impl NodeStartup { Some(mnemonic_str.clone()), ) }; @@ -2530,7 +2530,7 @@ index 86035df..78b61f7 100644 let data_dir = static_state .storage_dir_path .clone() -@@ -4866,7 +5164,22 @@ impl NodeStartup { +@@ -4897,7 +5195,22 @@ impl NodeStartup { )), _ => None, }; @@ -2554,7 +2554,7 @@ index 86035df..78b61f7 100644 // 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 || { let mut rgb_wallet = RgbLibWallet::new( -@@ -4951,16 +5264,14 @@ impl NodeStartup { +@@ -4977,16 +5290,14 @@ impl NodeStartup { &master_fingerprint, )?; @@ -2579,7 +2579,7 @@ index 86035df..78b61f7 100644 #[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 { +@@ -4995,15 +5306,20 @@ impl NodeStartup { .configure_vss_backup( crate::rgb_vss::VssBackupConfig::new(url, store_id, key) .with_encryption(true) @@ -2603,7 +2603,7 @@ index 86035df..78b61f7 100644 wrapper .vss_backup(&client) .map_err(|error| APIError::FailedVssInit(error.to_string()))?; -@@ -4990,18 +5306,6 @@ impl NodeStartup { +@@ -5016,18 +5332,6 @@ impl NodeStartup { })??; } @@ -2622,7 +2622,7 @@ index 86035df..78b61f7 100644 Ok(Arc::new(CommonState { config: Arc::clone(&static_state.config), signer, -@@ -5380,6 +5684,44 @@ async fn prepare_node( +@@ -5397,6 +5701,44 @@ async fn prepare_node( async fn start_lightning( #[allow(unused_mut)] mut setup: NodeStartup, @@ -2667,7 +2667,7 @@ index 86035df..78b61f7 100644 ) -> Result<(Option, Arc), APIError> { // Defense in depth: no chain backend, manager or worker precedes this check. setup.app_state.check_lightning_supported()?; -@@ -5387,6 +5729,29 @@ async fn start_lightning( +@@ -5404,6 +5746,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(); @@ -2697,7 +2697,7 @@ index 86035df..78b61f7 100644 let kv_store = Arc::clone(&setup.kv_store); let external_signer_mode = setup.external_signer_mode; let internal_mnemonic = setup.internal_mnemonic.clone(); -@@ -5403,6 +5768,19 @@ async fn start_lightning( +@@ -5420,6 +5785,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; @@ -2717,7 +2717,7 @@ index 86035df..78b61f7 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()) -@@ -5444,6 +5822,9 @@ async fn start_lightning( +@@ -5461,6 +5839,9 @@ async fn start_lightning( handle.clone(), Arc::clone(&logger), static_state.config.chain.fee_refresh_interval_secs, @@ -2727,7 +2727,7 @@ index 86035df..78b61f7 100644 ) .await { -@@ -5452,7 +5833,11 @@ async fn start_lightning( +@@ -5469,7 +5850,11 @@ async fn start_lightning( }; // Check that the bitcoind we've connected to is running the network we expect @@ -2740,7 +2740,7 @@ index 86035df..78b61f7 100644 if bitcoind_chain != match bitcoin_network { BitcoinNetwork::Mainnet => "main", -@@ -5468,7 +5853,11 @@ async fn start_lightning( +@@ -5485,7 +5870,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 @@ -2753,7 +2753,7 @@ index 86035df..78b61f7 100644 let initial_best_block = polled_chain_tip.to_best_block(); ChainSetup { -@@ -5501,16 +5890,14 @@ async fn start_lightning( +@@ -5518,16 +5907,14 @@ async fn start_lightning( Arc::clone(&logger), static_state.config.chain.indexer_timeout_secs, static_state.config.chain.fee_refresh_interval_secs, @@ -2774,7 +2774,7 @@ index 86035df..78b61f7 100644 ); let initial_best_block = indexer_client .get_best_block() -@@ -5546,7 +5933,7 @@ async fn start_lightning( +@@ -5563,7 +5950,7 @@ async fn start_lightning( let (chain_monitor, mut channelmonitors): (Arc, _) = { let persister = MonitorUpdatingPersisterAsync::new( Arc::clone(&monitor_kv_store), @@ -2783,7 +2783,7 @@ index 86035df..78b61f7 100644 Arc::clone(&logger), 1000, Arc::clone(&keys_manager), -@@ -5952,7 +6339,7 @@ async fn start_lightning( +@@ -5969,7 +6356,7 @@ async fn start_lightning( }); // The UTXO verifier can only attach to a P2P sync, and only after PeerManager @@ -2792,7 +2792,7 @@ index 86035df..78b61f7 100644 let (p2p_gossip_sync_for_verifier, route_handler): ( Option>, Arc, -@@ -6067,61 +6454,110 @@ async fn start_lightning( +@@ -6084,61 +6471,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({ @@ -2932,7 +2932,7 @@ index 86035df..78b61f7 100644 match backend { #[cfg(feature = "block-sync")] ChainBackend::BlockSync { client, .. } => { -@@ -6131,7 +6567,7 @@ async fn start_lightning( +@@ -6148,7 +6584,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; @@ -2941,7 +2941,7 @@ index 86035df..78b61f7 100644 let chain_poller = poll::ChainPoller::new(client.as_ref(), network); let chain_listener = ( chain_monitor_listener, -@@ -6140,13 +6576,21 @@ async fn start_lightning( +@@ -6157,13 +6593,21 @@ async fn start_lightning( let mut spv_client = SpvClient::new(chain_tip, chain_poller, &mut cache, &chain_listener); loop { @@ -2967,7 +2967,7 @@ index 86035df..78b61f7 100644 } }); } -@@ -6157,19 +6601,19 @@ async fn start_lightning( +@@ -6174,19 +6618,19 @@ async fn start_lightning( chain_monitor.clone(), output_sweeper.clone(), ]; @@ -2994,7 +2994,7 @@ index 86035df..78b61f7 100644 } }); } -@@ -6370,6 +6814,7 @@ async fn start_lightning( +@@ -6387,6 +6831,7 @@ async fn start_lightning( let unlocked_state = Arc::new(LightningState { common: Arc::clone(&common), @@ -3002,7 +3002,7 @@ index 86035df..78b61f7 100644 channel_manager: Arc::clone(&channel_manager), gossip_source: Arc::clone(&gossip_source), inbound_payments, -@@ -6397,15 +6842,19 @@ async fn start_lightning( +@@ -6414,15 +6859,19 @@ async fn start_lightning( asset_link_handler.set_authorizer(Arc::new(NodeAssetLinkAuthorizer)); @@ -3030,7 +3030,7 @@ index 86035df..78b61f7 100644 } let recent_payments_payment_ids = channel_manager -@@ -6472,17 +6921,22 @@ async fn start_lightning( +@@ -6489,17 +6938,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(); @@ -3057,7 +3057,7 @@ index 86035df..78b61f7 100644 return; } let db = RlnDatabase::new((*connect_db).clone()); -@@ -6494,14 +6948,18 @@ async fn start_lightning( +@@ -6511,14 +6965,18 @@ async fn start_lightning( .map(|chan| chan.counterparty.node_id) .filter(|id| connect_pm.peer_by_node_id(id).is_none()) { @@ -3080,7 +3080,7 @@ index 86035df..78b61f7 100644 } } } -@@ -6533,16 +6991,16 @@ async fn start_lightning( +@@ -6550,16 +7008,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); @@ -3101,7 +3101,7 @@ index 86035df..78b61f7 100644 return; } if link_event && link.is_connected() { -@@ -6564,10 +7022,12 @@ async fn start_lightning( +@@ -6581,10 +7039,12 @@ async fn start_lightning( interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { tokio::select! { @@ -3115,7 +3115,7 @@ index 86035df..78b61f7 100644 return; } su_chain_monitor.signer_unblocked(None); -@@ -6580,45 +7040,17 @@ async fn start_lightning( +@@ -6597,45 +7057,17 @@ async fn start_lightning( }); } @@ -3167,7 +3167,7 @@ index 86035df..78b61f7 100644 } sweep_handler.sweep_stale_state(); } -@@ -6628,15 +7060,24 @@ async fn start_lightning( +@@ -6645,15 +7077,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; @@ -3196,7 +3196,7 @@ index 86035df..78b61f7 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. -@@ -6665,7 +7106,8 @@ async fn start_lightning( +@@ -6682,7 +7123,8 @@ async fn start_lightning( Ok(( Some(LdkBackgroundServices { stop_processing, @@ -3206,7 +3206,7 @@ index 86035df..78b61f7 100644 peer_manager: peer_manager.clone(), bp_exit, background_processor: Some(background_processor), -@@ -6694,6 +7136,8 @@ pub(crate) fn attach_external_signer_transport( +@@ -6711,6 +7153,8 @@ pub(crate) fn attach_external_signer_transport( Ok(ExternalSignerAttachment { bootstrap, transport, @@ -3215,7 +3215,7 @@ index 86035df..78b61f7 100644 }) } -@@ -6708,15 +7152,11 @@ impl AppState { +@@ -6725,15 +7169,11 @@ impl AppState { ldk_background_services .stop_processing .store(true, Ordering::Release); @@ -3234,7 +3234,7 @@ index 86035df..78b61f7 100644 Some(ldk_background_services) } } -@@ -6837,13 +7277,19 @@ async fn release_vss_fence(kv_store: Arc, teardown: VssTeardown) +@@ -6854,13 +7294,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. @@ -3259,7 +3259,7 @@ index 86035df..78b61f7 100644 } } -@@ -6909,7 +7355,16 @@ mod vss_teardown_tests { +@@ -6926,7 +7372,16 @@ mod vss_teardown_tests { } /// Stop the resources owned by the unlocked session, including wallet-only persistence. @@ -3277,7 +3277,7 @@ index 86035df..78b61f7 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 -@@ -6931,19 +7386,33 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6948,19 +7403,33 @@ pub(crate) async fn stop_node(app_state: Arc) { ) }); @@ -3313,7 +3313,7 @@ index 86035df..78b61f7 100644 } } } -@@ -6952,9 +7421,13 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6969,9 +7438,13 @@ pub(crate) async fn stop_node(app_state: Arc) { .as_mut() .and_then(|services| services.background_processor.take()) { @@ -3328,7 +3328,7 @@ index 86035df..78b61f7 100644 #[cfg(feature = "vss")] if let Some((common, monitor_store)) = common_and_monitor { let deadline = Instant::now() + VSS_TEARDOWN_TIMEOUT; -@@ -6969,34 +7442,95 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6986,34 +7459,95 @@ pub(crate) async fn stop_node(app_state: Arc) { .is_err() { worker.abort(); @@ -4020,10 +4020,10 @@ index 003572d..7def4e9 100644 // workaround for https://github.com/tokio-rs/tracing/issues/1372 diff --git a/src/mainnet_startup_tests.rs b/src/mainnet_startup_tests.rs -index 0c75b17..ab46500 100644 +index cf0e30a..5fc6db4 100644 --- a/src/mainnet_startup_tests.rs +++ b/src/mainnet_startup_tests.rs -@@ -623,6 +623,7 @@ async fn mainnet_shutdown_waits_for_inflight_unlock_publication() { +@@ -693,6 +693,7 @@ async fn mainnet_shutdown_waits_for_inflight_unlock_publication() { tokio::time::timeout(Duration::from_secs(15), shutdown) .await .unwrap() @@ -4031,7 +4031,7 @@ index 0c75b17..ab46500 100644 .unwrap(); assert!(!*fixture.state.changing_state.lock().unwrap()); assert!(fixture.state.unlocked_app_state.lock().await.is_none()); -@@ -703,7 +704,7 @@ async fn mainnet_attached_and_native_signer_unlock_keep_strict_policy_without_ch +@@ -773,7 +774,7 @@ async fn mainnet_attached_and_native_signer_unlock_keep_strict_policy_without_ch ) .unwrap(); fixture.assert_no_lightning().await; @@ -8672,7 +8672,7 @@ index 0000000..1d36b2c + } +} diff --git a/src/utils.rs b/src/utils.rs -index 454f4ba..be65f0c 100644 +index 069153f..132ca59 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,7 +30,7 @@ use std::{ @@ -8684,7 +8684,7 @@ index 454f4ba..be65f0c 100644 time::{Duration, SystemTime}, }; use tokio::sync::{Mutex as TokioMutex, MutexGuard as TokioMutexGuard}; -@@ -85,6 +85,7 @@ pub(crate) fn fatal_exit_code() -> i32 { +@@ -84,6 +84,7 @@ pub(crate) fn fatal_exit_code() -> i32 { pub(crate) struct AppState { pub(crate) static_state: Arc, pub(crate) cancel_token: CancellationToken, @@ -8692,7 +8692,7 @@ index 454f4ba..be65f0c 100644 pub(crate) unlocked_app_state: Arc>>>, pub(crate) ldk_background_services: Arc>>, #[allow(dead_code)] -@@ -170,6 +171,7 @@ pub(crate) struct StaticState { +@@ -169,6 +170,7 @@ pub(crate) struct StaticState { /// `vss` feature flag; only the consumer in `start_node` is feature-gated. #[cfg_attr(not(feature = "vss"), allow(dead_code))] pub(crate) vss_url: Option, @@ -8700,7 +8700,7 @@ index 454f4ba..be65f0c 100644 /// When true, a failed VSS restore on a fresh device logs a warning and /// continues with empty local state instead of aborting unlock. #[cfg_attr(not(feature = "vss"), allow(dead_code))] -@@ -216,6 +218,7 @@ pub(crate) struct CommonState { +@@ -215,6 +217,7 @@ pub(crate) struct CommonState { /// A complete Lightning runtime, present only on supported non-mainnet networks. pub(crate) struct LightningState { @@ -8708,7 +8708,7 @@ index 454f4ba..be65f0c 100644 pub(crate) common: Arc, pub(crate) channel_manager: Arc, pub(crate) gossip_source: Arc, -@@ -560,37 +563,34 @@ pub(crate) fn check_port_is_available(port: u16) -> Result<(), AppError> { +@@ -559,37 +562,34 @@ pub(crate) fn check_port_is_available(port: u16) -> Result<(), AppError> { /// accepted; pass `allow_http = true` to allow `http://` on any host (for /// private networks with out-of-band trust). pub(crate) fn validate_vss_url(url: &str, allow_http: bool) -> Result<(), AppError> { @@ -8773,7 +8773,7 @@ index 454f4ba..be65f0c 100644 } pub(crate) fn get_db_path(storage_dir_path: &Path) -> PathBuf { -@@ -612,14 +612,15 @@ pub(crate) fn encrypt_and_save_mnemonic( +@@ -611,14 +611,15 @@ pub(crate) fn encrypt_and_save_mnemonic( pub(crate) async fn connect_peer_if_necessary( pubkey: PublicKey, address: SocketAddr, @@ -8791,7 +8791,7 @@ index 454f4ba..be65f0c 100644 tracing::info!("connected to peer (pubkey: {pubkey}, addr: {address})"); Ok(()) } -@@ -646,16 +647,37 @@ pub(crate) async fn do_connect_peer( +@@ -645,16 +646,37 @@ pub(crate) async fn do_connect_peer( pubkey: PublicKey, address: SocketAddr, peer_manager: Arc, @@ -8830,7 +8830,7 @@ index 454f4ba..be65f0c 100644 return Ok(()); } } -@@ -807,6 +829,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr +@@ -806,6 +828,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr lsp_base_url: args.lsp_base_url.clone(), lsp_bearer_token: args.lsp_bearer_token.clone(), vss_url: args.vss_url.clone(), @@ -8838,7 +8838,7 @@ index 454f4ba..be65f0c 100644 vss_allow_empty_restore: args.vss_allow_empty_restore, reuse_addresses: args.reuse_addresses, remote_signer_listen_addr: args.remote_signer_listen_addr, -@@ -818,6 +841,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr +@@ -817,6 +840,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr let app_state = Arc::new(AppState { static_state, cancel_token, @@ -8846,7 +8846,7 @@ index 454f4ba..be65f0c 100644 unlocked_app_state: Arc::new(TokioMutex::new(None)), ldk_background_services: Arc::new(Mutex::new(None)), attached_external_signer: Arc::new(Mutex::new(None)), -@@ -1070,6 +1094,24 @@ mod utils_tests { +@@ -1069,6 +1093,24 @@ mod utils_tests { assert!(validate_vss_url("http://example.com/vss", true).is_ok()); } diff --git a/scripts/compiled-runtime-identity.test.js b/scripts/compiled-runtime-identity.test.js new file mode 100644 index 0000000..1d7d5f5 --- /dev/null +++ b/scripts/compiled-runtime-identity.test.js @@ -0,0 +1,87 @@ +'use strict' + +const assert = require('node:assert/strict') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const test = require('node:test') +const vm = require('node:vm') +const contract = require('./release-contract') + +function fixture (context) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'utexo-compiled-runtime-')) + context.after(() => fs.rmSync(root, { recursive: true, force: true })) + const owner = require('../package.json') + const patch = fs.readFileSync(path.join(__dirname, '..', owner.utexoNativeOverlay.patch), 'utf8') + const section = patch.split('diff --git a/bindings/c-ffi/src/lib.rs b/bindings/c-ffi/src/lib.rs\n')[1].split('\ndiff --git ')[0] + const source = section.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++')).map(line => line.slice(1)).join('\n') + const file = path.join(root, 'bindings/c-ffi/src/lib.rs') + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, source) + return { root, file, source, patch } +} + +test('reviewed Rust runtime declarations match the complete release graph', context => { + const { root } = fixture(context) + contract.verifyCompiledRuntimeIdentity(root) +}) + +test('stale compiled graph literals are refused independently of metadata', context => { + const { root, file, source } = fixture(context) + for (const [field, value, stale] of [ + ['abi_version', 1, 0], + ['rln_version', contract.RELEASE.rlnVersion, '0.16.0'], + ['rln_commit', contract.RELEASE.commit, 'c29d1b2dfe43ba95c25ced8984f09e42c789d6af'], + ['lightning_commit', contract.RELEASE.lightningCommit, '510189d5700a6d1406229ce7523e181c3838715e'], + ['rgb_lib_commit', contract.RELEASE.rgbLibCommit, '962730a211f0f7569d7ad8ae777052f433d87655'] + ]) { + const before = `"${field}": ${JSON.stringify(value)}` + assert.ok(source.includes(before), field) + fs.writeFileSync(file, source.replace(before, `"${field}": ${JSON.stringify(stale)}`)) + assert.throws(() => contract.verifyCompiledRuntimeIdentity(root), new RegExp('identity mismatch: ' + field)) + } +}) + +test('missing or ambiguous native runtime declarations fail closed', context => { + const { root, file, source } = fixture(context) + for (const candidate of [ + source.replace('rln_binding_build_info()', 'another_build_info()'), + source + '\n' + source, + source.replace('"abi_version": 1,', '"abi_version": 1, "abi_version": 1,'), + source.replace('"abi_version": 1,', '"abi_version": 1, "abi_version": another_version(),'), + source.replace('"abi_version": 1,', '"abi_version": 1 + 1,') + ]) { + fs.writeFileSync(file, candidate) + assert.throws(() => contract.verifyCompiledRuntimeIdentity(root), /compiled C-FFI runtime identity|C-FFI runtime identity mismatch/) + } +}) + +test('normal preparation and final admission reject a checksum-valid stale native declaration', context => { + const { root, file, source, patch } = fixture(context) + const before = `"rln_commit": "${contract.RELEASE.commit}"` + const stale = '"rln_commit": "c29d1b2dfe43ba95c25ced8984f09e42c789d6af"' + const stalePatch = patch.replace(before, stale) + assert.notEqual(stalePatch, patch) + fs.writeFileSync(file, source.replace(before, stale)) + const patchPath = path.join(root, 'reviewed-stale.patch') + fs.writeFileSync(patchPath, stalePatch) + const config = { ...contract.RELEASE, patchPath, patchSha256: contract.sha256(patchPath) } + // Git identity and frozen-diff checks succeed; the actual Rust declaration is + // still stale. This isolates the additional normal source-admission boundary. + const sandbox = { + module: { exports: {} }, + require: name => name === 'node:child_process' + ? { execFileSync: (command, args) => { + assert.equal(command, 'git') + if (args.includes('rev-parse')) return args[1] === path.join(root, 'rust-lightning') ? contract.RELEASE.lightningCommit : contract.RELEASE.commit + if (args.includes('diff')) return stalePatch + if (args.includes('ls-files') || args.includes('status')) return '' + throw new Error('Unexpected Git operation: ' + args.join(' ')) + } } + : require(name) + } + vm.runInNewContext(fs.readFileSync(path.join(__dirname, 'release-contract.js'), 'utf8'), sandbox) + for (const check of ['prepareSource', 'verifyPreparedSource']) { + assert.throws(() => sandbox.module.exports[check](root, config), /Compiled C-FFI runtime identity mismatch: rln_commit/) + } +}) diff --git a/scripts/install-overlay-addon.test.js b/scripts/install-overlay-addon.test.js index cc0d834..232df2b 100644 --- a/scripts/install-overlay-addon.test.js +++ b/scripts/install-overlay-addon.test.js @@ -68,7 +68,7 @@ test('manifest rejects wrong artifact, source, wrapper, lock and target identiti 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-bfa\.git\?rev=962730a211f0f7569d7ad8ae777052f433d87655#962730a211f0f7569d7ad8ae777052f433d87655/) + assert.match(lock, /rgb-lib-bfa\.git\?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0/) 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) diff --git a/scripts/release-contract.js b/scripts/release-contract.js index 085a0c2..7e12cbb 100644 --- a/scripts/release-contract.js +++ b/scripts/release-contract.js @@ -7,13 +7,13 @@ const { execFileSync } = require('node:child_process') const RELEASE = Object.freeze({ 'rlnVersion': '0.17.0-beta.3', - 'ref': 'c29d1b2dfe43ba95c25ced8984f09e42c789d6af', - 'commit': 'c29d1b2dfe43ba95c25ced8984f09e42c789d6af', - 'lightningCommit': '510189d5700a6d1406229ce7523e181c3838715e', + 'ref': 'd10d679f0dd73246f6b475a44bdf13edd5f9b2a4', + 'commit': 'd10d679f0dd73246f6b475a44bdf13edd5f9b2a4', + 'lightningCommit': 'bbe68189872c26088e351d034b05c58769cb996a', 'rgbLibRepository': 'https://github.com/UTEXO-Protocol/rgb-lib-bfa.git', - 'rgbLibCommit': '962730a211f0f7569d7ad8ae777052f433d87655', + 'rgbLibCommit': '89c37086a68ef792da75c62bce59b58b1d7952a0', 'rustToolchain': '1.94.0', - 'cffiLockSha256': '460728009d9c1db3abf9a2f2ad4d76f777589b55eff96ff7be1f3c4d301dab30' + 'cffiLockSha256': '3039a36910386f9d6f350ed33791cd6461174cd340247998b80726a6581c3763' }) // Exact reviewed C adapter plus the shared native lifecycle repair. This remains // an upstream release with an explicitly hashed runtime overlay, not plain RLN. @@ -55,8 +55,8 @@ const ALLOWED_FILES = Object.freeze([ ]) const GIT_SOURCES = Object.freeze({ 'musig2': 'git+https://github.com/arik-so/rust-musig2?rev=6f95a05718cbb44d8fe3fa6021aea8117aa38d50#6f95a05718cbb44d8fe3fa6021aea8117aa38d50', - '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', + 'rgb-lib': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0', + 'rgb-lib-migration': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0', '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', @@ -83,6 +83,26 @@ function git (root, args) { return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }) } +function verifyCompiledRuntimeIdentity (root) { + const source = fs.readFileSync(path.join(root, 'bindings/c-ffi/src/lib.rs'), 'utf8') + const declarations = [...source.matchAll(/pub extern "C" fn rln_binding_build_info\(\) -> CResultString \{([\s\S]*?)\n\}/g)] + if (declarations.length !== 1) throw new Error('Missing or ambiguous compiled C-FFI runtime identity') + const expected = { + abi_version: 1, + rln_version: RELEASE.rlnVersion, + rln_commit: RELEASE.commit, + lightning_commit: RELEASE.lightningCommit, + rgb_lib_commit: RELEASE.rgbLibCommit + } + for (const [key, value] of Object.entries(expected)) { + const fields = [...declarations[0][1].matchAll(new RegExp('"' + key + '"\\s*:', 'g'))] + const values = [...declarations[0][1].matchAll(new RegExp('"' + key + '"\\s*:\\s*("[^"\\\\]*"|[0-9]+)\\s*(?=,|\\})', 'g'))] + if (fields.length !== 1 || values.length !== 1 || JSON.parse(values[0][1]) !== value) { + throw new Error(`Compiled C-FFI runtime identity mismatch: ${key}`) + } + } +} + function prepareSource (root, config, applyIfClean = true) { validateAdapter(config) if (git(root, ['rev-parse', 'HEAD']).trim() !== RELEASE.commit) throw new Error('Unexpected RLN revision') @@ -97,6 +117,7 @@ function prepareSource (root, config, applyIfClean = true) { if (!diff && applyIfClean) git(root, ['apply', '--index', config.patchPath]) else if (diff !== patch) throw new Error('Native source has changes beyond the exact approved adapter') if (git(root, ['diff', '--binary', 'HEAD']) !== patch) throw new Error('Applied native adapter is not exact') + verifyCompiledRuntimeIdentity(root) if (sha256(path.join(root, 'bindings/c-ffi/Cargo.lock')) !== RELEASE.cffiLockSha256) { throw new Error('Unexpected C-FFI lock graph') } @@ -135,4 +156,4 @@ function wrapperSha256 (root, files) { return hash.digest('hex') } -module.exports = { RELEASE, ALLOWED_FILES, GIT_SOURCES, sha256, validateAdapter, prepareSource, verifyPreparedSource, wrapperSha256, verifyGraph, verifyCargoGraph } +module.exports = { RELEASE, ALLOWED_FILES, GIT_SOURCES, sha256, validateAdapter, verifyCompiledRuntimeIdentity, prepareSource, verifyPreparedSource, wrapperSha256, verifyGraph, verifyCargoGraph } From 705e44eb382e9c4efd32d092f953317a3620d483 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Mon, 26 Oct 2026 16:14:08 +0530 Subject: [PATCH 09/11] Expose recoverable RGB output operations with exact native evidence --- BFA-MIGRATION.md | 32 +- index.d.ts | 41 +- index.js | 26 +- json-boundary.js | 35 +- package.json | 6 +- patches/release-adapter-v0.17.0-beta.3.patch | 734 +++++++++++++++---- scripts/contract.test.js | 65 +- scripts/release-contract.js | 5 +- src/lib.rs | 56 +- 9 files changed, 819 insertions(+), 181 deletions(-) diff --git a/BFA-MIGRATION.md b/BFA-MIGRATION.md index cba8fe7..a0c3b9d 100644 --- a/BFA-MIGRATION.md +++ b/BFA-MIGRATION.md @@ -11,8 +11,8 @@ Candidate `0.2.0-bfa.1` sends an empty JSON-RPC parameter array for EVM methods | --- | --- | | RGB library | `89c37086a68ef792da75c62bce59b58b1d7952a0` | | LDK | `bbe68189872c26088e351d034b05c58769cb996a` | -| RLN | `d10d679f0dd73246f6b475a44bdf13edd5f9b2a4` | -| Maintained adapter SHA-256 | `db4511e71a1c770def1aeeb5f1ab5e72e07fdd228627d6d744dce56b4b3bdcfe` | +| RLN | `7cced3aa3c2c904a88d8f8a7832981d25a5ea871` | +| Maintained adapter SHA-256 | `b70bb70610ff1b47eb55ea2bdf67a65af85e89fbabc006f2345302cbe74f1082` | This graph requires fresh native builds and separately frozen candidate archives. Previous `0.2.0-bfa.0` artifacts and qualification records retain their original identities and do not qualify this candidate. @@ -41,3 +41,31 @@ Ordinary on-chain BTC and complete-invoice RGB transfers use separate supported 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. + + +## Recoverable RGB output setup + +The candidate adds `rgb-output-operations-v1`. Check the compiled capability and the complete API surface before using it; method names alone do not establish native support. + +Persist a random `operation_id` containing 32–64 lowercase hexadecimal characters before calling `prepareCreateUtxos({ operation_id, up_to, num, size, fee_rate, skip_sync })`. Preparation reserves native inputs and returns the exact transaction ID, input outpoints, actual RGB output count and Bitcoin fee for review. The library may prepare fewer outputs than requested when funds do not cover the requested count and fee; always review the returned count and fee. No transaction is signed or broadcast during preparation. + +`commitPreparedCreateUtxos({ operation_id })` commits only the persisted transaction. Retrying the same ID retains the same transaction and input ownership. `getCreateUtxosOperation({ operation_id })` and `listCreateUtxosOperations()` recover operation evidence after process loss; `listPendingVanillaTransactions()` reports native reservations. Never infer an operation's identity by comparing transaction histories. + +| Status | Meaning and allowed action | +| --- | --- | +| `preparing` | Preparation was interrupted before a complete review record. Native recovery releases only an unambiguously owned, provably unbroadcast reservation. | +| `prepared` | Exact transaction ready for review. `can_commit` permits the reviewed transaction; only `can_cancel: true` proves safe cancellation. A restored snapshot may predate a broadcast, so restored preparations cannot be discarded merely because the snapshot says `prepared`. | +| `submitting` | The exact signed transaction is persisted and broadcast may have occurred. Retry commits only that same transaction; cancellation is refused. | +| `broadcast` | Native broadcast bookkeeping completed. Observe the exact transaction for confirmation. | +| `confirmed` | The exact transaction was observed confirmed. Reconcile again after a chain reorganization. | +| `cancelled` / `failed` | A terminal, provably unbroadcast operation. Its ID cannot be reused for different intent; prepare a fresh operation if needed. | + +`cancelCreateUtxosPlan({ operation_id })` is permitted only when native `can_cancel` evidence proves it safe. Never derive this permission from `status` alone. A timeout, missing mempool transaction or elapsed time never authorizes clearing uncertain inputs. Existing usable RGB outputs and independently spendable native inputs do not require a blanket ban on new output setup. Native reservation and allocation checks remain authoritative. + +The legacy one-shot `createUtxos()` remains available for compatibility. Applications needing review, interruption recovery and exact idempotent retries should use the prepared operation API. Host JavaScript tests are distinct from funded native and Android application qualification; retain the exact source and artifact receipts for each tested build. + +## Output-operation qualification scope + +This source candidate passes 21 durable-output Rust cases, including two real funded isolated regtest fixtures, 13 wallet-source/snapshot-barrier cases and 53 VSS cases. Snapshot capture and every output-operation journal/database transition share an ordered barrier, so an already-running automatic backup cannot capture a reservation without its prepared transaction. Existing automatic-backup ownership and completion semantics remain in place. Status-only reads of an unchanged acknowledged wallet do not invent a new backup receipt. + +Bare passes 112 host installer/contract tests; Node passes 47 host contract tests. Both pass their TypeScript checks. These counts do not qualify a packaged native platform. Only an Android ARM64 candidate artifact is requested now; all other platform binaries, full distribution assembly and corresponding runtime qualification remain deferred. The normal all-target publication gates remain unchanged. No package has been published. diff --git a/index.d.ts b/index.d.ts index afefdd9..64da016 100644 --- a/index.d.ts +++ b/index.d.ts @@ -160,6 +160,30 @@ export interface CreateUtxosRequest { skip_sync: boolean } +/** Caller identity is persisted before constructing/reserving an unsigned transaction. */ +export interface PrepareCreateUtxosRequest extends Omit { + operation_id: string + fee_rate: number | string +} + +export interface CreateUtxosOperationRequest { operation_id: string } +export type CreateUtxosOperationStatus = 'preparing' | 'prepared' | 'submitting' | 'broadcast' | 'confirmed' | 'cancelled' | 'failed' +export interface CreateUtxosOperation { + operation_id: string + txid: string | null + status: CreateUtxosOperationStatus + /** False after restore unless native evidence proves cancellation cannot discard a broadcast. */ + can_cancel: boolean + /** True only when the exact persisted transaction may be committed. */ + can_commit: boolean + inputs: Array<{ txid: string; vout: number }> + num_outputs: number + size_sat: number + fee_sat: number | string + fee_rate: number | string +} +export interface PendingVanillaTransaction { txid: string; type: 'CreateUtxos' | 'SendBtc' | 'Drain' } + export interface SendBtcResponse { txid: string } @@ -288,14 +312,15 @@ export class SdkNode { commitPreparedBtcSend(request: JsonRequest): never /** @deprecated Unsupported by released RLN; always throws UnsupportedCapabilityError. */ cancelBtcSendPlan(request: { plan_id: string }): never - /** @deprecated Unsupported by released RLN; always throws UnsupportedCapabilityError. */ - prepareCreateUtxos(request: CreateUtxosRequest): never - /** @deprecated Unsupported by released RLN; always throws UnsupportedCapabilityError. */ - commitPreparedCreateUtxos(request: JsonRequest): never - /** @deprecated Unsupported by released RLN; always throws UnsupportedCapabilityError. */ - cancelCreateUtxosPlan(request: { plan_id: string }): never - /** @deprecated Unsupported by released RLN; always throws UnsupportedCapabilityError. */ - listPendingVanillaTransactions(): never + /** Idempotent unsigned review with exact native txid, fee and owned input reservations. */ + prepareCreateUtxos(request: PrepareCreateUtxosRequest): CreateUtxosOperation + /** Signs/broadcasts the prepared transaction once; uncertain retries retain the exact txid. */ + commitPreparedCreateUtxos(request: CreateUtxosOperationRequest): CreateUtxosOperation + /** Only a provably unbroadcast operation can release its owned reservations. */ + cancelCreateUtxosPlan(request: CreateUtxosOperationRequest): CreateUtxosOperation + getCreateUtxosOperation(request: CreateUtxosOperationRequest): CreateUtxosOperation | null + listCreateUtxosOperations(): CreateUtxosOperation[] + listPendingVanillaTransactions(): PendingVanillaTransaction[] /** @deprecated Unsupported by released RLN; always throws UnsupportedCapabilityError. */ listAddressReceipts(address: string): never createUtxos(request: JsonRequest): JsonValue diff --git a/index.js b/index.js index 50f020b..d880038 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, sendRgbInvoiceRequest, unsupported, UnsupportedCapabilityError } = require('./json-boundary') +const { parse, stringify, bip322Request, paymentRequest, burnRequest, sendRgbInvoiceRequest, createUtxosOperationRequest, unsupported, UnsupportedCapabilityError } = require('./json-boundary') // ─── 1. Native addon resolution ────────────────────────────────────────── @@ -198,13 +198,29 @@ class SdkNode { cancelBtcSendPlan (request) { unsupported('cancelBtcSendPlan') } - prepareCreateUtxos (request) { unsupported('prepareCreateUtxos') } + prepareCreateUtxos (request) { + return parse(this._inner.prepareCreateUtxos(createUtxosOperationRequest(request, true))) + } + + commitPreparedCreateUtxos (request) { + return parse(this._inner.commitPreparedCreateUtxos(createUtxosOperationRequest(request))) + } + + cancelCreateUtxosPlan (request) { + return parse(this._inner.cancelCreateUtxosPlan(createUtxosOperationRequest(request))) + } - commitPreparedCreateUtxos (request) { unsupported('commitPreparedCreateUtxos') } + getCreateUtxosOperation (request) { + return parse(this._inner.getCreateUtxosOperation(createUtxosOperationRequest(request))) + } - cancelCreateUtxosPlan (request) { unsupported('cancelCreateUtxosPlan') } + listCreateUtxosOperations () { + return parse(this._inner.listCreateUtxosOperations()) + } - listPendingVanillaTransactions () { unsupported('listPendingVanillaTransactions') } + listPendingVanillaTransactions () { + return parse(this._inner.listPendingVanillaTransactions()) + } listAddressReceipts (address) { unsupported('listAddressReceipts') } diff --git a/json-boundary.js b/json-boundary.js index 8e9e795..a6bc9d0 100644 --- a/json-boundary.js +++ b/json-boundary.js @@ -162,4 +162,37 @@ function sendRgbInvoiceRequest (request) { }) } -module.exports = { bip322Request, parse, stringify, paymentRequest, burnRequest, sendRgbInvoiceRequest, unsupported, UnsupportedCapabilityError } +// An operation ID is durable caller intent, never a txid inferred from history. +function createUtxosOperationRequest (request, prepare = false) { + if (!request || typeof request !== 'object' || Array.isArray(request)) { + throw new TypeError('RGB output operation request must be an object') + } + const allowed = prepare + ? ['operation_id', 'up_to', 'num', 'size', 'fee_rate', 'skip_sync'] + : ['operation_id'] + if (Object.keys(request).some(key => !allowed.includes(key))) { + throw new TypeError('Unknown RGB output operation request field') + } + if (typeof request.operation_id !== 'string' || !/^[a-f0-9]{32,64}$/.test(request.operation_id)) { + throw new TypeError('RGB output operation_id must contain 32 to 64 lowercase hex characters') + } + if (!prepare) return stringify({ operation_id: request.operation_id }) + if (typeof request.up_to !== 'boolean' || typeof request.skip_sync !== 'boolean') { + throw new TypeError('RGB output up_to and skip_sync must be booleans') + } + for (const [field, max] of [['num', 255], ['size', 4294967295]]) { + const value = request[field] + if (value != null && (!Number.isInteger(value) || value < 1 || value > max)) { + throw new RangeError(`RGB output ${field} must be a positive ${field === 'num' ? '8' : '32'}-bit integer`) + } + } + const fee = request.fee_rate + checkedNumber(fee) + if (!((typeof fee === 'number' && Number.isSafeInteger(fee) && fee > 0) || + (typeof fee === 'string' && /^[1-9][0-9]*$/.test(fee))) || BigInt(fee) > 18446744073709551615n) { + throw new RangeError('RGB output fee_rate must be an exact positive 64-bit integer') + } + return stringifyLossless({ ...request, fee_rate: new LosslessNumber(String(fee)) }) +} + +module.exports = { createUtxosOperationRequest, bip322Request, parse, stringify, paymentRequest, burnRequest, sendRgbInvoiceRequest, unsupported, UnsupportedCapabilityError } diff --git a/package.json b/package.json index f8a2c7d..ea0fcc9 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": "d10d679f0dd73246f6b475a44bdf13edd5f9b2a4", - "commit": "d10d679f0dd73246f6b475a44bdf13edd5f9b2a4", + "ref": "7cced3aa3c2c904a88d8f8a7832981d25a5ea871", + "commit": "7cced3aa3c2c904a88d8f8a7832981d25a5ea871", "lightningCommit": "bbe68189872c26088e351d034b05c58769cb996a", "patch": "patches/release-adapter-v0.17.0-beta.3.patch", - "patchSha256": "db4511e71a1c770def1aeeb5f1ab5e72e07fdd228627d6d744dce56b4b3bdcfe", + "patchSha256": "b70bb70610ff1b47eb55ea2bdf67a65af85e89fbabc006f2345302cbe74f1082", "cffiLockSha256": "3039a36910386f9d6f350ed33791cd6461174cd340247998b80726a6581c3763", "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 6114055..168835f 100644 --- a/patches/release-adapter-v0.17.0-beta.3.patch +++ b/patches/release-adapter-v0.17.0-beta.3.patch @@ -919,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..69493f2 100644 +index 19a5c4f..7caaf28 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, @@ -934,16 +934,16 @@ index c8bb08b..69493f2 100644 struct CResultString rln_btc_balance(const struct COpaqueStruct *node, bool skip_sync); struct CResultString rln_burn(const struct COpaqueStruct *node, const char *request_json); -@@ -102,6 +107,8 @@ struct CResultString rln_get_consignment_path(const struct COpaqueStruct *node, - const char *asset_id, - const char *txid); +@@ -111,6 +116,8 @@ struct CResultString rln_get_consignment_path(const struct COpaqueStruct *node, + struct CResultString rln_get_create_utxos_operation(const struct COpaqueStruct *node, + const char *request_json); +struct CResultString rln_get_message_signing_address(const struct COpaqueStruct *node); + struct CResultString rln_get_payment(const struct COpaqueStruct *node, const char *payment_hash_hex, const char *payment_type); -@@ -167,6 +174,22 @@ struct CResult rln_native_external_signer_new(const char *seed_hex, +@@ -180,6 +187,22 @@ struct CResult rln_native_external_signer_new(const char *seed_hex, const char *network, bool permissive_policy); @@ -966,7 +966,7 @@ index c8bb08b..69493f2 100644 struct CResultString rln_network_info(const struct COpaqueStruct *node); struct CResultString rln_node_info(const struct COpaqueStruct *node); -@@ -195,6 +218,14 @@ struct CResultString rln_sdk_initialize(const char *request_json); +@@ -211,6 +234,14 @@ struct CResultString rln_sdk_initialize(const char *request_json); struct CResultString rln_sdk_node_apay_new(const struct COpaqueStruct *node, const char *host_node_id); @@ -981,7 +981,7 @@ index c8bb08b..69493f2 100644 struct CResultString rln_sdk_node_attach_native_external_signer(const struct COpaqueStruct *node, const struct COpaqueStruct *signer); -@@ -212,6 +243,14 @@ struct CResultString rln_sdk_node_init_with_native_external_signer(const struct +@@ -228,6 +259,14 @@ struct CResultString rln_sdk_node_init_with_native_external_signer(const struct struct CResult rln_sdk_node_new(const char *request_json); @@ -996,7 +996,7 @@ index c8bb08b..69493f2 100644 struct CResultString rln_sdk_node_shutdown(const struct COpaqueStruct *node); struct CResultString rln_sdk_node_unlock(const struct COpaqueStruct *node, -@@ -225,28 +264,28 @@ struct CResultString rln_sdk_node_unlock_with_native_external_signer(const struc +@@ -241,28 +280,28 @@ struct CResultString rln_sdk_node_unlock_with_native_external_signer(const struc const char *request_json); /** @@ -1039,7 +1039,7 @@ index c8bb08b..69493f2 100644 struct CResultString rln_sdk_shutdown(void); struct CResultString rln_send_btc(const struct COpaqueStruct *node, const char *request_json); -@@ -258,8 +297,14 @@ struct CResultString rln_send_payment(const struct COpaqueStruct *node, const ch +@@ -274,8 +313,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); @@ -1054,7 +1054,7 @@ index c8bb08b..69493f2 100644 struct CResultString rln_sync(const struct COpaqueStruct *node); struct CResultString rln_taker(const struct COpaqueStruct *node, const char *request_json); -@@ -271,3 +316,8 @@ struct CResultString rln_uniffi_is_initialized(void); +@@ -287,3 +332,8 @@ struct CResultString rln_uniffi_is_initialized(void); struct CResultString rln_verify_message(const struct COpaqueStruct *node, const char *message, const char *signature); @@ -1064,7 +1064,7 @@ index c8bb08b..69493f2 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..655ea84 100644 +index 764cc94..b84559f 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 @@ -1077,16 +1077,16 @@ index 498a7e1..655ea84 100644 CResultString rln_btc_balance(const COpaqueStruct *node, bool skip_sync); CResultString rln_burn(const COpaqueStruct *node, const char *request_json); -@@ -90,6 +93,8 @@ CResultString rln_get_consignment_path(const COpaqueStruct *node, - const char *asset_id, - const char *txid); +@@ -96,6 +99,8 @@ CResultString rln_get_consignment_path(const COpaqueStruct *node, + + CResultString rln_get_create_utxos_operation(const COpaqueStruct *node, const char *request_json); +CResultString rln_get_message_signing_address(const COpaqueStruct *node); + CResultString rln_get_payment(const COpaqueStruct *node, const char *payment_hash_hex, const char *payment_type); -@@ -147,6 +152,18 @@ CResult rln_native_external_signer_new(const char *seed_hex, +@@ -157,6 +162,18 @@ CResult rln_native_external_signer_new(const char *seed_hex, const char *network, bool permissive_policy); @@ -1105,7 +1105,7 @@ index 498a7e1..655ea84 100644 CResultString rln_network_info(const COpaqueStruct *node); CResultString rln_node_info(const COpaqueStruct *node); -@@ -170,6 +187,12 @@ CResultString rln_sdk_initialize(const char *request_json); +@@ -182,6 +199,12 @@ CResultString rln_sdk_initialize(const char *request_json); /// unused_hashes, refill_batch_size, first_hash_index). Upstream PR #51. CResultString rln_sdk_node_apay_new(const COpaqueStruct *node, const char *host_node_id); @@ -1118,7 +1118,7 @@ index 498a7e1..655ea84 100644 CResultString rln_sdk_node_attach_native_external_signer(const COpaqueStruct *node, const COpaqueStruct *signer); -@@ -187,6 +210,12 @@ CResultString rln_sdk_node_init_with_native_external_signer(const COpaqueStruct +@@ -199,6 +222,12 @@ CResultString rln_sdk_node_init_with_native_external_signer(const COpaqueStruct CResult rln_sdk_node_new(const char *request_json); @@ -1131,7 +1131,7 @@ index 498a7e1..655ea84 100644 CResultString rln_sdk_node_shutdown(const COpaqueStruct *node); CResultString rln_sdk_node_unlock(const COpaqueStruct *node, const char *request_json); -@@ -198,24 +227,20 @@ CResultString rln_sdk_node_unlock_with_native_external_signer(const COpaqueStruc +@@ -210,24 +239,20 @@ CResultString rln_sdk_node_unlock_with_native_external_signer(const COpaqueStruc const COpaqueStruct *signer, const char *request_json); @@ -1166,7 +1166,7 @@ index 498a7e1..655ea84 100644 CResultString rln_sdk_shutdown(); CResultString rln_send_btc(const COpaqueStruct *node, const char *request_json); -@@ -226,8 +251,12 @@ CResultString rln_send_payment(const COpaqueStruct *node, const char *request_js +@@ -238,8 +263,12 @@ CResultString rln_send_payment(const COpaqueStruct *node, const char *request_js CResultString rln_send_rgb(const COpaqueStruct *node, const char *request_json); @@ -1179,7 +1179,7 @@ index 498a7e1..655ea84 100644 CResultString rln_sync(const COpaqueStruct *node); CResultString rln_taker(const COpaqueStruct *node, const char *request_json); -@@ -240,4 +269,8 @@ CResultString rln_verify_message(const COpaqueStruct *node, +@@ -252,4 +281,8 @@ CResultString rln_verify_message(const COpaqueStruct *node, const char *message, const char *signature); @@ -1189,7 +1189,7 @@ index 498a7e1..655ea84 100644 + } // extern "C" diff --git a/bindings/c-ffi/src/api.rs b/bindings/c-ffi/src/api.rs -index 2b9e873..69683c0 100644 +index 453f97f..6648a9d 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( @@ -1258,7 +1258,7 @@ index 2b9e873..69683c0 100644 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 Result { @@ -1267,7 +1267,7 @@ index 2b9e873..69683c0 100644 ok_void() } -@@ -783,6 +817,34 @@ pub(crate) fn native_external_signer_bootstrap(signer: &COpaqueStruct) -> Result +@@ -827,6 +861,34 @@ pub(crate) fn native_external_signer_bootstrap(signer: &COpaqueStruct) -> Result json(JsonSdkExternalSignerBootstrap::from(bootstrap)) } @@ -1302,7 +1302,7 @@ index 2b9e873..69683c0 100644 pub(crate) fn sdk_node_init_with_native_external_signer( node: &COpaqueStruct, signer: &COpaqueStruct, -@@ -840,3 +902,62 @@ pub(crate) fn sdk_node_unlock_with_attached_external_signer( +@@ -884,3 +946,62 @@ pub(crate) fn sdk_node_unlock_with_attached_external_signer( node.unlock_with_attached_external_signer_request(r.into())?; ok_void() } @@ -1366,21 +1366,21 @@ index 2b9e873..69683c0 100644 + )?) +} diff --git a/bindings/c-ffi/src/json_types.rs b/bindings/c-ffi/src/json_types.rs -index 5b606f1..9c9c59e 100644 +index 7afc138..f6e1681 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, + SdkPrepareCreateUtxosRequest, 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, ++ SdkUnlockRequest, SdkVssClearFenceRequest, SendRgbInvoiceRequest, SendRgbRequest, ++ SendRgbResponse, SignMessageResponse, Swap, SwapList, SwapStatus, Token, TokenLight, ++ Transaction, TransactionType, Transfer, TransferTransportEndpoint, TransportEndpoint, Txid, ++ Unspent, Utxo, VerifyMessageResponse, WitnessData, }; use serde::{Deserialize, Serialize}; @@ -1669,7 +1669,7 @@ index 5b606f1..9c9c59e 100644 impl From for WitnessData { fn from(w: JsonWitnessData) -> Self { WitnessData { -@@ -2208,6 +2420,7 @@ impl From for JsonUtxo { +@@ -2240,6 +2452,7 @@ impl From for JsonUtxo { pub(crate) struct JsonUnspent { pub utxo: JsonUtxo, pub rgb_allocations: Vec, @@ -1677,7 +1677,7 @@ index 5b606f1..9c9c59e 100644 } impl From for JsonUnspent { -@@ -2215,6 +2428,30 @@ impl From for JsonUnspent { +@@ -2247,6 +2460,30 @@ impl From for JsonUnspent { JsonUnspent { utxo: u.utxo.into(), rgb_allocations: u.rgb_allocations.into_iter().map(Into::into).collect(), @@ -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..92908fd 100644 +index cb94207..8188e7a 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..92908fd 100644 + ffi_call!("rln_binding_build_info", Ok::(serde_json::json!({ + "abi_version": 1, + "rln_version": "0.17.0-beta.3", -+ "rln_commit": "d10d679f0dd73246f6b475a44bdf13edd5f9b2a4", ++ "rln_commit": "7cced3aa3c2c904a88d8f8a7832981d25a5ea871", + "lightning_commit": "bbe68189872c26088e351d034b05c58769cb996a", + "rgb_lib_commit": "89c37086a68ef792da75c62bce59b58b1d7952a0", + "adapter_sha256": env!("RLN_ADAPTER_SHA256"), @@ -1732,7 +1732,7 @@ index e1dded1..92908fd 100644 + "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", "invoice-first-send-v1", "rgb-proxy-v1", "consignment-export-v1", "mainnet-lightning-rejection-v1", -+ "external-signer-eth-rpc-v1", "external-signer-burn-v1", ++ "external-signer-eth-rpc-v1", "external-signer-burn-v1", "rgb-output-operations-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"], + "vss_policy": { @@ -1836,7 +1836,7 @@ index e1dded1..92908fd 100644 #[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 { +@@ -793,6 +856,39 @@ pub extern "C" fn rln_sdk_shutdown() -> CResultString { // External-signer surface // --------------------------------------------------------------------------- @@ -1876,7 +1876,7 @@ index e1dded1..92908fd 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 +850,86 @@ mod free_tests { +@@ -814,6 +910,86 @@ mod free_tests { free_sdk_node(COpaqueStruct::null()); free_native_external_signer(COpaqueStruct::null()); } @@ -1963,7 +1963,7 @@ index e1dded1..92908fd 100644 } #[unsafe(no_mangle)] -@@ -776,6 +952,21 @@ pub extern "C" fn rln_native_external_signer_bootstrap(signer: &COpaqueStruct) - +@@ -836,6 +1012,21 @@ pub extern "C" fn rln_native_external_signer_bootstrap(signer: &COpaqueStruct) - ) } @@ -1985,7 +1985,7 @@ index e1dded1..92908fd 100644 #[unsafe(no_mangle)] pub extern "C" fn rln_sdk_node_init_with_native_external_signer( node: &COpaqueStruct, -@@ -839,3 +1030,67 @@ pub extern "C" fn rln_sdk_node_unlock_with_attached_external_signer( +@@ -899,3 +1090,67 @@ pub extern "C" fn rln_sdk_node_unlock_with_attached_external_signer( api::sdk_node_unlock_with_attached_external_signer(node, request_json) ) } @@ -2090,7 +2090,7 @@ index 98c3372..c4db3c0 100644 #[cfg(test)] diff --git a/bindings/rgb_lightning_node.udl b/bindings/rgb_lightning_node.udl -index a58949a..8c72dfa 100644 +index 219527a..b428476 100644 --- a/bindings/rgb_lightning_node.udl +++ b/bindings/rgb_lightning_node.udl @@ -5,6 +5,7 @@ namespace rgb_lightning_node { @@ -2121,7 +2121,7 @@ index a58949a..8c72dfa 100644 [Throws=RlnError] void connectpeer(string peer_pubkey_and_addr); [Throws=RlnError] -@@ -640,6 +654,7 @@ dictionary SdkInitRequest { +@@ -652,6 +666,7 @@ dictionary SdkInitRequest { string? lsp_bearer_token; string? vss_url = null; boolean vss_allow_http = false; @@ -2130,7 +2130,7 @@ index a58949a..8c72dfa 100644 boolean reuse_addresses = false; }; diff --git a/src/ldk.rs b/src/ldk.rs -index 3bc967e..3bc9c7b 100644 +index 2c582f4..0797b44 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -108,18 +108,17 @@ use std::collections::HashSet; @@ -2554,7 +2554,7 @@ index 3bc967e..3bc9c7b 100644 // 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 || { let mut rgb_wallet = RgbLibWallet::new( -@@ -4977,16 +5290,14 @@ impl NodeStartup { +@@ -4987,16 +5300,14 @@ impl NodeStartup { &master_fingerprint, )?; @@ -2579,7 +2579,7 @@ index 3bc967e..3bc9c7b 100644 #[cfg(feature = "vss")] if let Some((url, store_id, key)) = rgb_vss_backup { let wrapper = Arc::clone(&rgb_wallet_wrapper); -@@ -4995,15 +5306,20 @@ impl NodeStartup { +@@ -5005,15 +5316,20 @@ impl NodeStartup { .configure_vss_backup( crate::rgb_vss::VssBackupConfig::new(url, store_id, key) .with_encryption(true) @@ -2603,7 +2603,7 @@ index 3bc967e..3bc9c7b 100644 wrapper .vss_backup(&client) .map_err(|error| APIError::FailedVssInit(error.to_string()))?; -@@ -5016,18 +5332,6 @@ impl NodeStartup { +@@ -5026,18 +5342,6 @@ impl NodeStartup { })??; } @@ -2622,7 +2622,7 @@ index 3bc967e..3bc9c7b 100644 Ok(Arc::new(CommonState { config: Arc::clone(&static_state.config), signer, -@@ -5397,6 +5701,44 @@ async fn prepare_node( +@@ -5407,6 +5711,44 @@ async fn prepare_node( async fn start_lightning( #[allow(unused_mut)] mut setup: NodeStartup, @@ -2667,7 +2667,7 @@ index 3bc967e..3bc9c7b 100644 ) -> Result<(Option, Arc), APIError> { // Defense in depth: no chain backend, manager or worker precedes this check. setup.app_state.check_lightning_supported()?; -@@ -5404,6 +5746,29 @@ async fn start_lightning( +@@ -5414,6 +5756,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(); @@ -2697,7 +2697,7 @@ index 3bc967e..3bc9c7b 100644 let kv_store = Arc::clone(&setup.kv_store); let external_signer_mode = setup.external_signer_mode; let internal_mnemonic = setup.internal_mnemonic.clone(); -@@ -5420,6 +5785,19 @@ async fn start_lightning( +@@ -5430,6 +5795,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; @@ -2717,7 +2717,7 @@ index 3bc967e..3bc9c7b 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()) -@@ -5461,6 +5839,9 @@ async fn start_lightning( +@@ -5471,6 +5849,9 @@ async fn start_lightning( handle.clone(), Arc::clone(&logger), static_state.config.chain.fee_refresh_interval_secs, @@ -2727,7 +2727,7 @@ index 3bc967e..3bc9c7b 100644 ) .await { -@@ -5469,7 +5850,11 @@ async fn start_lightning( +@@ -5479,7 +5860,11 @@ async fn start_lightning( }; // Check that the bitcoind we've connected to is running the network we expect @@ -2740,7 +2740,7 @@ index 3bc967e..3bc9c7b 100644 if bitcoind_chain != match bitcoin_network { BitcoinNetwork::Mainnet => "main", -@@ -5485,7 +5870,11 @@ async fn start_lightning( +@@ -5495,7 +5880,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 @@ -2753,7 +2753,7 @@ index 3bc967e..3bc9c7b 100644 let initial_best_block = polled_chain_tip.to_best_block(); ChainSetup { -@@ -5518,16 +5907,14 @@ async fn start_lightning( +@@ -5528,16 +5917,14 @@ async fn start_lightning( Arc::clone(&logger), static_state.config.chain.indexer_timeout_secs, static_state.config.chain.fee_refresh_interval_secs, @@ -2774,7 +2774,7 @@ index 3bc967e..3bc9c7b 100644 ); let initial_best_block = indexer_client .get_best_block() -@@ -5563,7 +5950,7 @@ async fn start_lightning( +@@ -5573,7 +5960,7 @@ async fn start_lightning( let (chain_monitor, mut channelmonitors): (Arc, _) = { let persister = MonitorUpdatingPersisterAsync::new( Arc::clone(&monitor_kv_store), @@ -2783,7 +2783,7 @@ index 3bc967e..3bc9c7b 100644 Arc::clone(&logger), 1000, Arc::clone(&keys_manager), -@@ -5969,7 +6356,7 @@ async fn start_lightning( +@@ -5979,7 +6366,7 @@ async fn start_lightning( }); // The UTXO verifier can only attach to a P2P sync, and only after PeerManager @@ -2792,7 +2792,7 @@ index 3bc967e..3bc9c7b 100644 let (p2p_gossip_sync_for_verifier, route_handler): ( Option>, Arc, -@@ -6084,61 +6471,110 @@ async fn start_lightning( +@@ -6094,61 +6481,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({ @@ -2932,7 +2932,7 @@ index 3bc967e..3bc9c7b 100644 match backend { #[cfg(feature = "block-sync")] ChainBackend::BlockSync { client, .. } => { -@@ -6148,7 +6584,7 @@ async fn start_lightning( +@@ -6158,7 +6594,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; @@ -2941,7 +2941,7 @@ index 3bc967e..3bc9c7b 100644 let chain_poller = poll::ChainPoller::new(client.as_ref(), network); let chain_listener = ( chain_monitor_listener, -@@ -6157,13 +6593,21 @@ async fn start_lightning( +@@ -6167,13 +6603,21 @@ async fn start_lightning( let mut spv_client = SpvClient::new(chain_tip, chain_poller, &mut cache, &chain_listener); loop { @@ -2967,7 +2967,7 @@ index 3bc967e..3bc9c7b 100644 } }); } -@@ -6174,19 +6618,19 @@ async fn start_lightning( +@@ -6184,19 +6628,19 @@ async fn start_lightning( chain_monitor.clone(), output_sweeper.clone(), ]; @@ -2994,7 +2994,7 @@ index 3bc967e..3bc9c7b 100644 } }); } -@@ -6387,6 +6831,7 @@ async fn start_lightning( +@@ -6397,6 +6841,7 @@ async fn start_lightning( let unlocked_state = Arc::new(LightningState { common: Arc::clone(&common), @@ -3002,7 +3002,7 @@ index 3bc967e..3bc9c7b 100644 channel_manager: Arc::clone(&channel_manager), gossip_source: Arc::clone(&gossip_source), inbound_payments, -@@ -6414,15 +6859,19 @@ async fn start_lightning( +@@ -6424,15 +6869,19 @@ async fn start_lightning( asset_link_handler.set_authorizer(Arc::new(NodeAssetLinkAuthorizer)); @@ -3030,7 +3030,7 @@ index 3bc967e..3bc9c7b 100644 } let recent_payments_payment_ids = channel_manager -@@ -6489,17 +6938,22 @@ async fn start_lightning( +@@ -6499,17 +6948,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(); @@ -3057,7 +3057,7 @@ index 3bc967e..3bc9c7b 100644 return; } let db = RlnDatabase::new((*connect_db).clone()); -@@ -6511,14 +6965,18 @@ async fn start_lightning( +@@ -6521,14 +6975,18 @@ async fn start_lightning( .map(|chan| chan.counterparty.node_id) .filter(|id| connect_pm.peer_by_node_id(id).is_none()) { @@ -3080,7 +3080,7 @@ index 3bc967e..3bc9c7b 100644 } } } -@@ -6550,16 +7008,16 @@ async fn start_lightning( +@@ -6560,16 +7018,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); @@ -3101,7 +3101,7 @@ index 3bc967e..3bc9c7b 100644 return; } if link_event && link.is_connected() { -@@ -6581,10 +7039,12 @@ async fn start_lightning( +@@ -6591,10 +7049,12 @@ async fn start_lightning( interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { tokio::select! { @@ -3115,7 +3115,7 @@ index 3bc967e..3bc9c7b 100644 return; } su_chain_monitor.signer_unblocked(None); -@@ -6597,45 +7057,17 @@ async fn start_lightning( +@@ -6607,45 +7067,17 @@ async fn start_lightning( }); } @@ -3167,7 +3167,7 @@ index 3bc967e..3bc9c7b 100644 } sweep_handler.sweep_stale_state(); } -@@ -6645,15 +7077,24 @@ async fn start_lightning( +@@ -6655,15 +7087,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; @@ -3196,7 +3196,7 @@ index 3bc967e..3bc9c7b 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. -@@ -6682,7 +7123,8 @@ async fn start_lightning( +@@ -6692,7 +7133,8 @@ async fn start_lightning( Ok(( Some(LdkBackgroundServices { stop_processing, @@ -3206,7 +3206,7 @@ index 3bc967e..3bc9c7b 100644 peer_manager: peer_manager.clone(), bp_exit, background_processor: Some(background_processor), -@@ -6711,6 +7153,8 @@ pub(crate) fn attach_external_signer_transport( +@@ -6721,6 +7163,8 @@ pub(crate) fn attach_external_signer_transport( Ok(ExternalSignerAttachment { bootstrap, transport, @@ -3215,7 +3215,7 @@ index 3bc967e..3bc9c7b 100644 }) } -@@ -6725,15 +7169,11 @@ impl AppState { +@@ -6735,15 +7179,11 @@ impl AppState { ldk_background_services .stop_processing .store(true, Ordering::Release); @@ -3234,7 +3234,7 @@ index 3bc967e..3bc9c7b 100644 Some(ldk_background_services) } } -@@ -6854,13 +7294,19 @@ async fn release_vss_fence(kv_store: Arc, teardown: VssTeardown) +@@ -6864,13 +7304,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. @@ -3259,7 +3259,7 @@ index 3bc967e..3bc9c7b 100644 } } -@@ -6926,7 +7372,16 @@ mod vss_teardown_tests { +@@ -6936,7 +7382,16 @@ mod vss_teardown_tests { } /// Stop the resources owned by the unlocked session, including wallet-only persistence. @@ -3277,7 +3277,7 @@ index 3bc967e..3bc9c7b 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 -@@ -6948,19 +7403,33 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6958,19 +7413,33 @@ pub(crate) async fn stop_node(app_state: Arc) { ) }); @@ -3313,7 +3313,7 @@ index 3bc967e..3bc9c7b 100644 } } } -@@ -6969,9 +7438,13 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6979,9 +7448,13 @@ pub(crate) async fn stop_node(app_state: Arc) { .as_mut() .and_then(|services| services.background_processor.take()) { @@ -3328,7 +3328,7 @@ index 3bc967e..3bc9c7b 100644 #[cfg(feature = "vss")] if let Some((common, monitor_store)) = common_and_monitor { let deadline = Instant::now() + VSS_TEARDOWN_TIMEOUT; -@@ -6986,34 +7459,95 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6996,34 +7469,95 @@ pub(crate) async fn stop_node(app_state: Arc) { .is_err() { worker.abort(); @@ -3983,10 +3983,10 @@ index a635117..d126aa3 100644 }); } diff --git a/src/lib.rs b/src/lib.rs -index c85e309..6bd8869 100644 +index 235aab8..650780e 100644 --- a/src/lib.rs +++ b/src/lib.rs -@@ -76,6 +76,8 @@ pub mod test_utils; +@@ -77,6 +77,8 @@ pub mod test_utils; mod uniffi_api; mod utils; #[cfg(feature = "vss")] @@ -3996,10 +3996,10 @@ index c85e309..6bd8869 100644 pub use node::{NodeConfig, NodeHandle}; diff --git a/src/main.rs b/src/main.rs -index 003572d..7def4e9 100644 +index 34fb4e1..96dcb12 100644 --- a/src/main.rs +++ b/src/main.rs -@@ -48,6 +48,8 @@ mod swap; +@@ -49,6 +49,8 @@ mod swap; mod synced_kv_store; mod utils; #[cfg(feature = "vss")] @@ -4008,7 +4008,7 @@ index 003572d..7def4e9 100644 mod vss_kv_store; // the test suite calls into `electrum_client` to wait for electrs to catch up with bitcoind, and -@@ -371,7 +373,9 @@ async fn shutdown_signal(app_state: Arc) { +@@ -372,7 +374,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; } @@ -4059,10 +4059,129 @@ index d6f571f..8b920d5 100644 ldk_background_services: Arc::new(Mutex::new(None)), attached_external_signer: Arc::new(Mutex::new(None)), diff --git a/src/mainnet_vss_tests.rs b/src/mainnet_vss_tests.rs -index f9c88b7..e040833 100644 +index f9c88b7..00e2789 100644 --- a/src/mainnet_vss_tests.rs +++ b/src/mainnet_vss_tests.rs -@@ -678,18 +678,20 @@ async fn mainnet_vss_concurrent_stop_keeps_fence_with_teardown_owner() { +@@ -317,10 +317,38 @@ impl Wallet { + let indexer = Indexer::new(bitcoin::Network::Bitcoin).await; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let proxy = format!("rpc://{}/json-rpc", listener.local_addr().unwrap()); +- let router = Router::new().route("/json-rpc", post(|Json(request): Json| async move { +- assert_eq!(request["method"], "server.info"); +- Json(json!({"jsonrpc":"2.0", "id":request["id"], "result":{"protocol_version":"0.2", "version":"fixture", "uptime":1}})) +- })); ++ let router = Router::new().route( ++ "/json-rpc", ++ post(|Json(request): Json| async move { ++ assert_eq!(request["jsonrpc"], "2.0"); ++ let now = chrono::Utc::now().timestamp(); ++ let result = match request["method"].as_str().unwrap() { ++ "server.info" => { ++ json!({"protocol_version":"1.0", "version":"fixture", "uptime":1, ++ "current_time":now.to_string(), "max_expiry_secs":"2592000", "max_quota":"1", ++ "max_pow_difficulty":"0", "retention_grace_secs":"86400"}) ++ } ++ "invoice.prepare" => { ++ let params = request["params"].as_object().unwrap(); ++ assert_eq!(params.len(), 3); ++ for name in ["invoice_id", "recipient_pk"] { ++ let value = params[name].as_str().unwrap(); ++ assert_eq!(value.len(), 64); ++ assert!(value.bytes().all(|byte| byte.is_ascii_hexdigit())); ++ } ++ let expiry = params["expires_at"] ++ .as_str() ++ .unwrap() ++ .parse::() ++ .unwrap(); ++ assert!(expiry > now && expiry <= now + 2_592_000); ++ json!(true) ++ } ++ other => panic!("Unexpected VSS fixture proxy method: {other}"), ++ }; ++ Json(json!({"jsonrpc":"2.0", "id":request["id"], "result":result})) ++ }), ++ ); + let proxy_task = tokio::spawn(async move { axum::serve(listener, router).await.unwrap() }); + let state = start_daemon(&UserArgs { + storage_dir_path: directory.path().into(), +@@ -338,7 +366,7 @@ impl Wallet { + lsp_bearer_token: None, + vss_url: Some(server.url.clone()), + vss_allow_empty_restore: false, +- reuse_addresses: true, ++ reuse_addresses: false, + remote_signer_listen_addr: None, + config: Default::default(), + }) +@@ -356,6 +384,34 @@ impl Wallet { + } + } + ++ async fn vanilla_address_at(&self, index: u32) -> String { ++ use rgb_lib::bdk_wallet::miniscript::{Descriptor, DescriptorPublicKey}; ++ use rgb_lib::wallet::RgbWalletOpsOffline; ++ let state = self.state.unlocked_app_state.lock().await; ++ let descriptor = state ++ .as_ref() ++ .unwrap() ++ .rgb_wallet_wrapper ++ .get_rgb_wallet() ++ .get_descriptors() ++ .vanilla; ++ // This disposable internal-signer fixture may return private descriptors. ++ // Convert them in memory to their public descriptor before deriving expectations. ++ let (descriptor, _) = Descriptor::::parse_descriptor( ++ &bitcoin::secp256k1::Secp256k1::new(), ++ &descriptor, ++ ) ++ .unwrap(); ++ let derived = descriptor ++ .at_derivation_index(index) ++ .unwrap() ++ .derived_descriptor(&bitcoin::secp256k1::Secp256k1::verification_only()) ++ .unwrap(); ++ bitcoin::Address::from_script(&derived.script_pubkey(), bitcoin::Network::Bitcoin) ++ .unwrap() ++ .to_string() ++ } ++ + fn request(&self) -> sdk::UnlockRequest { + sdk::UnlockRequest { + password: PASSWORD.into(), +@@ -450,11 +506,13 @@ async fn mainnet_vss_wallet_backup_reopen_and_fresh_device_restore() { + .is_none()); + let first_fence = server.rows(&node_store)[FENCE].clone(); + let initial = sdk::address(wallet.state.clone()).await.unwrap().address; ++ assert_eq!(initial, wallet.vanilla_address_at(0).await); + let rotated = sdk::rotate_address(wallet.state.clone()) + .await + .unwrap() + .address; + assert_ne!(initial, rotated); ++ assert_eq!(rotated, wallet.vanilla_address_at(1).await); + let version = sdk::vss_backup(wallet.state.clone()).await.unwrap(); + let backup = server.rows(&rgb_store); + assert_eq!(backup["backup/data"].version, version); +@@ -480,7 +538,7 @@ async fn mainnet_vss_wallet_backup_reopen_and_fresh_device_restore() { + assert_ne!(server.rows(&node_store)[FENCE].value, first_fence.value); + assert_eq!( + sdk::address(wallet.state.clone()).await.unwrap().address, +- rotated ++ wallet.vanilla_address_at(2).await + ); + wallet.lock().await; + let request_start = server.state.lock().unwrap().requests.len(); +@@ -489,7 +547,7 @@ async fn mainnet_vss_wallet_backup_reopen_and_fresh_device_restore() { + .unwrap(); + assert_eq!( + sdk::address(other.state.clone()).await.unwrap().address, +- rotated ++ other.vanilla_address_at(3).await + ); + assert!(other + .state +@@ -678,18 +736,20 @@ async fn mainnet_vss_concurrent_stop_keeps_fence_with_teardown_owner() { .forget(); assert!(wallet.state.unlocked_app_state.lock().await.is_none()); assert!(!first.is_finished()); @@ -4213,9 +4332,18 @@ 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..8afd8bf 100644 +index 14d98b1..67a40f9 100644 --- a/src/rgb.rs +++ b/src/rgb.rs +@@ -30,7 +30,7 @@ use rgb_lib::{ + use std::collections::{HashMap, HashSet}; + use std::path::PathBuf; + use std::str::FromStr; +-use std::sync::{Arc, Mutex, MutexGuard}; ++use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; + + use crate::{error::APIError, utils::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. @@ -4225,15 +4353,20 @@ index 83202d4..8afd8bf 100644 let prepared = wallet.burn_begin( wrapper.online, asset_id, -@@ -658,71 +658,140 @@ fn validate_burn_signed_psbt(unsigned: &str, signed: &str) -> Result Result = MutexGuard<'a, RgbLibWallet>; +pub(crate) struct RgbLibWalletWrapper { ++ // Snapshot admission always precedes the RGB mutex. Output-operation writers own it ++ // through every DB+journal transition; ordinary mutators/snapshots hold a reader. ++ snapshot_barrier: RwLock<()>, + pub(crate) wallet: Arc>, + pub(crate) online: Online, ++ /// Serializes durable output-plan ownership, signing and exact-transaction retries. ++ pub(crate) output_operations: Mutex<()>, + #[cfg(feature = "vss")] + pub(crate) snapshot_policy: Option>, + #[cfg(feature = "vss")] @@ -4257,6 +4390,8 @@ index 83202d4..8afd8bf 100644 + mutated: bool, + wrapper: &'a RgbLibWalletWrapper, + sync_before: Option, ++ // Fields are dropped in declaration order: release RGB first, then snapshot admission. ++ _snapshot_admission: Option>, } -#[cfg(feature = "vss")] impl std::ops::Deref for RgbWalletGuard<'_> { @@ -4339,6 +4474,8 @@ index 83202d4..8afd8bf 100644 -pub(crate) struct RgbLibWalletWrapper { - pub(crate) wallet: Arc>, - pub(crate) online: Online, +- /// Serializes durable output-plan ownership, signing and exact-transaction retries. +- pub(crate) output_operations: Mutex<()>, - #[cfg(feature = "vss")] - vss_client: Mutex>>, -} @@ -4346,8 +4483,10 @@ index 83202d4..8afd8bf 100644 impl RgbLibWalletWrapper { pub(crate) fn new(wallet: Arc>, online: Online) -> Self { RgbLibWalletWrapper { ++ snapshot_barrier: RwLock::new(()), wallet, online, + output_operations: Mutex::new(()), #[cfg(feature = "vss")] + snapshot_policy: None, + #[cfg(feature = "vss")] @@ -4363,12 +4502,92 @@ index 83202d4..8afd8bf 100644 + mutated: false, + wrapper: self, + sync_before: None, ++ _snapshot_admission: None, + } + } + ++ fn snapshot_reader(&self) -> RwLockReadGuard<'_, ()> { ++ self.snapshot_barrier.read().unwrap_or_else(|poisoned| { ++ tracing::error!( ++ "Snapshot barrier poisoned; retaining wallet journal recovery admission" ++ ); ++ poisoned.into_inner() ++ }) ++ } ++ ++ fn snapshot_writer(&self) -> RwLockWriteGuard<'_, ()> { ++ self.snapshot_barrier.write().unwrap_or_else(|poisoned| { ++ tracing::error!( ++ "Snapshot barrier poisoned; retaining wallet journal recovery admission" ++ ); ++ poisoned.into_inner() ++ }) ++ } ++ ++ /// A snapshot caller must acquire this guard before the RGB mutex; never nest it ++ /// inside an output-operation writer or another wallet/snapshot guard. ++ pub(crate) fn get_rgb_wallet_for_snapshot(&self) -> RgbWalletGuard<'_> { ++ let admission = self.snapshot_reader(); ++ let mut wallet = self.get_rgb_wallet(); ++ wallet._snapshot_admission = Some(admission); ++ wallet ++ } ++ ++ /// Serialize the complete owned operation with every automatic/manual snapshot. ++ /// Raw backend wallet guards do not upload between reservation and PSBT journal writes. ++ pub(crate) fn with_output_operation_snapshot( ++ &self, ++ operation: impl FnOnce() -> Result, ++ ) -> Result { ++ let _admission = self.snapshot_writer(); ++ #[cfg(feature = "vss")] ++ let can_clear = { ++ let wallet = self.get_rgb_wallet(); ++ self.snapshot_policy.as_ref().is_some_and(|policy| { ++ !policy.pending() ++ && policy.has_completed_backup().unwrap_or(false) ++ && snapshot_is_clean(&wallet.guard) ++ }) ++ }; ++ #[cfg(feature = "vss")] ++ if let Some(policy) = &self.snapshot_policy { ++ // Persist admission before any journal or DB write, including uncertain failures. ++ policy.before_mutation(); ++ } ++ let result = operation(); ++ #[cfg(feature = "vss")] ++ if let Some(policy) = &self.snapshot_policy { ++ let mut wallet = self.get_rgb_wallet(); ++ // A status read with identical acknowledged contents is not a new backup. ++ let unchanged = can_clear ++ && snapshot_is_clean(&wallet.guard) ++ && policy.clear_unchanged_intent().is_ok(); ++ if !unchanged && policy.enabled.load(std::sync::atomic::Ordering::SeqCst) { ++ if let Some(client) = self.vss_client() { ++ if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { ++ policy.after_mutation(&mut wallet.guard, &client) ++ })) ++ .is_err() ++ { ++ policy ++ .failed ++ .store(true, std::sync::atomic::Ordering::SeqCst); ++ tracing::error!("Automatic output-operation snapshot panicked; financial result is preserved"); ++ } ++ } else { ++ policy ++ .failed ++ .store(true, std::sync::atomic::Ordering::SeqCst); ++ tracing::error!("Automatic output-operation snapshot client unavailable; financial result is preserved"); ++ } ++ } ++ } ++ result ++ } ++ + /// Explicit admission covers SDK mutators taking &self as well as &mut self. + pub(crate) fn get_rgb_wallet_mutating(&self) -> RgbWalletGuard<'_> { -+ let mut guard = self.get_rgb_wallet(); ++ let mut guard = self.get_rgb_wallet_for_snapshot(); + guard.mutated = true; + #[cfg(feature = "vss")] + if let Some(policy) = &self.snapshot_policy { @@ -4381,7 +4600,7 @@ index 83202d4..8afd8bf 100644 + if skip_sync { + return self.get_rgb_wallet(); + } -+ let mut guard = self.get_rgb_wallet(); ++ let mut guard = self.get_rgb_wallet_for_snapshot(); + #[cfg(feature = "vss")] + if let Some(policy) = &self.snapshot_policy { + // A fresh/never-backed-up wallet has no acknowledged content digest. Do not @@ -4401,7 +4620,7 @@ index 83202d4..8afd8bf 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. -@@ -744,18 +813,6 @@ impl RgbLibWalletWrapper { +@@ -747,18 +902,6 @@ impl RgbLibWalletWrapper { self.wallet.lock().unwrap() } @@ -4420,34 +4639,34 @@ index 83202d4..8afd8bf 100644 #[cfg(feature = "vss")] pub(crate) fn vss_client(&self) -> Option> { self.vss_client.lock().unwrap().clone() -@@ -781,8 +838,15 @@ impl RgbLibWalletWrapper { +@@ -784,8 +927,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(); ++ let mut wallet = self.get_rgb_wallet_for_snapshot(); + match &self.snapshot_policy { -+ Some(policy) => policy.backup(&mut wallet, client).map_err(|error| { ++ Some(policy) => policy.backup(&mut wallet.guard, client).map_err(|error| { + crate::rgb_vss::Error::Internal { + details: error.to_string(), + } + }), -+ None => crate::rgb_vss::backup_wallet(&mut wallet, client), ++ None => crate::rgb_vss::backup_wallet(&mut wallet.guard, client), + } } #[cfg(feature = "vss")] -@@ -790,7 +854,7 @@ impl RgbLibWalletWrapper { +@@ -793,7 +943,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 backup_required = self.get_rgb_wallet_for_snapshot().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 { +@@ -813,7 +963,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> { @@ -4457,7 +4676,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn bitcoin_network(&self) -> BitcoinNetwork { -@@ -825,7 +890,7 @@ impl RgbLibWalletWrapper { +@@ -828,7 +979,7 @@ impl RgbLibWalletWrapper { transport_endpoints: Vec, min_confirmations: u8, ) -> Result { @@ -4466,7 +4685,7 @@ index 83202d4..8afd8bf 100644 Some(self.online), asset_id, assignment, -@@ -841,7 +906,8 @@ impl RgbLibWalletWrapper { +@@ -844,7 +995,8 @@ impl RgbLibWalletWrapper { fascia: Fascia, witness_ord: Option, ) -> Result<(), RgbLibError> { @@ -4476,7 +4695,7 @@ index 83202d4..8afd8bf 100644 } #[allow(unused_variables)] -@@ -866,7 +932,7 @@ impl RgbLibWalletWrapper { +@@ -869,7 +1021,7 @@ impl RgbLibWalletWrapper { } pub(crate) fn create_consigments(&self, psbt: String) -> Result<(), RgbLibError> { @@ -4485,7 +4704,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn create_utxos( -@@ -877,7 +943,7 @@ impl RgbLibWalletWrapper { +@@ -880,7 +1032,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { @@ -4494,7 +4713,7 @@ index 83202d4..8afd8bf 100644 self.online, up_to, Some(num), -@@ -895,7 +961,7 @@ impl RgbLibWalletWrapper { +@@ -898,7 +1050,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { @@ -4503,7 +4722,7 @@ index 83202d4..8afd8bf 100644 self.online, up_to, Some(num), -@@ -911,7 +977,7 @@ impl RgbLibWalletWrapper { +@@ -914,7 +1066,7 @@ impl RgbLibWalletWrapper { signed_psbt: String, _skip_sync: bool, ) -> Result { @@ -4512,7 +4731,7 @@ index 83202d4..8afd8bf 100644 .create_utxos_end(self.online, signed_psbt) } -@@ -921,7 +987,7 @@ impl RgbLibWalletWrapper { +@@ -924,7 +1076,7 @@ impl RgbLibWalletWrapper { no_asset_only: bool, skip_sync: bool, ) -> Result { @@ -4521,7 +4740,7 @@ index 83202d4..8afd8bf 100644 self.online, batch_transfer_idx, no_asset_only, -@@ -930,11 +996,11 @@ impl RgbLibWalletWrapper { +@@ -933,11 +1085,11 @@ impl RgbLibWalletWrapper { } pub(crate) fn get_address(&self) -> Result { @@ -4535,7 +4754,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn get_asset_balance( -@@ -955,7 +1021,8 @@ impl RgbLibWalletWrapper { +@@ -958,7 +1110,8 @@ impl RgbLibWalletWrapper { pub(crate) fn get_btc_balance(&self, skip_sync: bool) -> Result { let online = if skip_sync { None } else { Some(self.online) }; @@ -4545,7 +4764,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn get_fee_estimation(&self, blocks: u16) -> Result { -@@ -988,7 +1055,7 @@ impl RgbLibWalletWrapper { +@@ -991,7 +1144,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, min_confirmations: u8, ) -> Result { @@ -4554,7 +4773,7 @@ index 83202d4..8afd8bf 100644 self.online, asset_id, amount, -@@ -1005,7 +1072,7 @@ impl RgbLibWalletWrapper { +@@ -1008,7 +1161,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, min_confirmations: u8, ) -> Result { @@ -4563,7 +4782,7 @@ index 83202d4..8afd8bf 100644 self.online, asset_id, inflation_amounts, -@@ -1022,7 +1089,7 @@ impl RgbLibWalletWrapper { +@@ -1025,7 +1178,7 @@ impl RgbLibWalletWrapper { amounts: Vec, file_path: Option, ) -> Result { @@ -4572,7 +4791,7 @@ index 83202d4..8afd8bf 100644 .issue_asset_cfa(name, details, precision, amounts, file_path) } -@@ -1042,7 +1109,7 @@ impl RgbLibWalletWrapper { +@@ -1045,7 +1198,7 @@ impl RgbLibWalletWrapper { details: "IFA link rights are unavailable in the BFA RGB library".into(), }); } @@ -4581,7 +4800,7 @@ index 83202d4..8afd8bf 100644 ticker, name, precision, -@@ -1059,7 +1126,7 @@ impl RgbLibWalletWrapper { +@@ -1062,7 +1215,7 @@ impl RgbLibWalletWrapper { precision: u8, amounts: Vec, ) -> Result { @@ -4590,7 +4809,7 @@ index 83202d4..8afd8bf 100644 .issue_asset_nia(ticker, name, precision, amounts) } -@@ -1072,7 +1139,7 @@ impl RgbLibWalletWrapper { +@@ -1075,7 +1228,7 @@ impl RgbLibWalletWrapper { media_file_path: Option, attachments_file_paths: Vec, ) -> Result { @@ -4599,7 +4818,7 @@ index 83202d4..8afd8bf 100644 ticker, name, details, -@@ -1098,7 +1165,8 @@ impl RgbLibWalletWrapper { +@@ -1101,7 +1254,8 @@ impl RgbLibWalletWrapper { skip_sync: bool, ) -> Result, RgbLibError> { let online = if skip_sync { None } else { Some(self.online) }; @@ -4609,7 +4828,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn list_transfers( -@@ -1115,7 +1183,7 @@ impl RgbLibWalletWrapper { +@@ -1118,7 +1272,7 @@ impl RgbLibWalletWrapper { skip_sync: bool, ) -> Result, RgbLibError> { let online = if skip_sync { None } else { Some(self.online) }; @@ -4618,7 +4837,7 @@ index 83202d4..8afd8bf 100644 .list_unspents(online, settled_only, skip_sync) } -@@ -1126,7 +1194,7 @@ impl RgbLibWalletWrapper { +@@ -1129,7 +1283,7 @@ impl RgbLibWalletWrapper { vout: u32, blinding: u64, ) -> Result<(RgbTransfer, Vec, HashSet), RgbLibError> { @@ -4627,7 +4846,7 @@ index 83202d4..8afd8bf 100644 self.online, consignment_path, txid, -@@ -1139,7 +1207,7 @@ impl RgbLibWalletWrapper { +@@ -1142,7 +1296,7 @@ impl RgbLibWalletWrapper { &self, recipient_id: String, ) -> Result, RgbLibError> { @@ -4636,7 +4855,7 @@ index 83202d4..8afd8bf 100644 .provide_out_of_band_ack(self.online, recipient_id) } -@@ -1148,11 +1216,8 @@ impl RgbLibWalletWrapper { +@@ -1151,11 +1305,8 @@ impl RgbLibWalletWrapper { consignment_path: String, media_file_paths: Vec, ) -> Result, RgbLibError> { @@ -4650,7 +4869,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn refresh( -@@ -1161,7 +1226,10 @@ impl RgbLibWalletWrapper { +@@ -1164,7 +1315,10 @@ impl RgbLibWalletWrapper { filter: Vec, skip_sync: bool, ) -> Result { @@ -4662,7 +4881,7 @@ index 83202d4..8afd8bf 100644 .refresh(self.online, asset_id, filter, skip_sync) } -@@ -1170,7 +1238,7 @@ impl RgbLibWalletWrapper { +@@ -1173,7 +1327,7 @@ impl RgbLibWalletWrapper { consignment: RgbTransfer, offchain_txid: String, ) -> Result<(), RgbLibError> { @@ -4671,7 +4890,7 @@ index 83202d4..8afd8bf 100644 .save_new_asset(self.online, consignment, offchain_txid) } -@@ -1183,7 +1251,7 @@ impl RgbLibWalletWrapper { +@@ -1186,7 +1340,7 @@ impl RgbLibWalletWrapper { expiration_timestamp: u64, sender_endpoint: Option, ) -> Result { @@ -4680,7 +4899,7 @@ index 83202d4..8afd8bf 100644 self.online, recipient_map, donation, -@@ -1211,7 +1279,7 @@ impl RgbLibWalletWrapper { +@@ -1214,7 +1368,7 @@ impl RgbLibWalletWrapper { details: "Custom RGB funding locktime is unavailable in the BFA RGB library".into(), }); } @@ -4689,7 +4908,7 @@ index 83202d4..8afd8bf 100644 self.online, recipient_map, donation, -@@ -1230,7 +1298,7 @@ impl RgbLibWalletWrapper { +@@ -1233,7 +1387,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { @@ -4698,7 +4917,7 @@ index 83202d4..8afd8bf 100644 .send_btc(self.online, address, amount, fee_rate, skip_sync) } -@@ -1241,8 +1309,14 @@ impl RgbLibWalletWrapper { +@@ -1244,8 +1398,14 @@ impl RgbLibWalletWrapper { fee_rate: u64, dry_run: bool, ) -> Result { @@ -4715,7 +4934,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn send_btc_channel_funding_begin( -@@ -1261,18 +1335,20 @@ impl RgbLibWalletWrapper { +@@ -1264,18 +1424,20 @@ impl RgbLibWalletWrapper { } pub(crate) fn send_btc_end(&self, signed_psbt: String) -> Result { @@ -4739,7 +4958,7 @@ index 83202d4..8afd8bf 100644 .send_end_db_update_only(self.online, signed_psbt) } -@@ -1281,7 +1357,7 @@ impl RgbLibWalletWrapper { +@@ -1284,7 +1446,7 @@ impl RgbLibWalletWrapper { } pub(crate) fn sync(&self, options: SyncOptions) -> Result<(), RgbLibError> { @@ -4748,7 +4967,7 @@ index 83202d4..8afd8bf 100644 } pub(crate) fn update_witnesses( -@@ -1289,7 +1365,7 @@ impl RgbLibWalletWrapper { +@@ -1292,7 +1454,7 @@ impl RgbLibWalletWrapper { after_height: u32, force_witnesses: Vec, ) -> Result { @@ -4757,7 +4976,7 @@ index 83202d4..8afd8bf 100644 .update_witnesses(self.online, after_height, force_witnesses) } -@@ -1298,7 +1374,7 @@ impl RgbLibWalletWrapper { +@@ -1301,7 +1463,7 @@ impl RgbLibWalletWrapper { witness_id: RgbTxid, witness_ord: WitnessOrd, ) -> Result<(), RgbLibError> { @@ -4766,7 +4985,7 @@ index 83202d4..8afd8bf 100644 .upsert_witness(witness_id, witness_ord) } -@@ -1310,7 +1386,7 @@ impl RgbLibWalletWrapper { +@@ -1313,7 +1475,7 @@ impl RgbLibWalletWrapper { transport_endpoints: Vec, min_confirmations: u8, ) -> Result { @@ -4775,7 +4994,7 @@ index 83202d4..8afd8bf 100644 Some(self.online), asset_id, assignment, -@@ -1413,7 +1489,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1416,7 +1578,7 @@ impl WalletSource for RgbLibWalletWrapper { Box::pin(async move { let network = Network::from(self.bitcoin_network()); let unspents = self @@ -4784,7 +5003,7 @@ index 83202d4..8afd8bf 100644 .list_unspents_vanilla(self.online, 1, false) .map_err(|e| { tracing::error!(error = %e, "failed to list unspents for fee bumping"); -@@ -1456,7 +1532,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1459,7 +1621,7 @@ impl WalletSource for RgbLibWalletWrapper { fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()> { Box::pin(async move { @@ -4793,10 +5012,131 @@ index 83202d4..8afd8bf 100644 tracing::error!(error = %e, "failed to get change address for fee bumping"); })?; let parsed = Address::from_str(&address).map_err(|e| { -@@ -1725,6 +1801,368 @@ mod wallet_source_tests { +@@ -1728,6 +1890,489 @@ mod wallet_source_tests { RgbLibWalletWrapper::new(Arc::new(Mutex::new(wallet)), Online { id: 0 }) } ++ #[test] ++ fn output_journal_transition_excludes_an_already_waiting_snapshot() { ++ use std::io::{Cursor, Read}; ++ use std::sync::mpsc; ++ use std::time::Duration; ++ ++ let wrapper = Arc::new(signetcustom_wallet_source()); ++ let directory = wrapper.get_rgb_wallet().get_wallet_dir(); ++ let fingerprint = directory.file_name().unwrap().to_str().unwrap().to_owned(); ++ let journal = directory.join("rln_output_operations_v1"); ++ std::fs::create_dir_all(&journal).unwrap(); ++ let journal_file = journal.join("coherence-fixture.json"); ++ let (partial_tx, partial_rx) = mpsc::channel(); ++ let (finish_tx, finish_rx) = mpsc::channel(); ++ let (attempt_tx, attempt_rx) = mpsc::channel(); ++ let (captured_tx, captured_rx) = mpsc::channel(); ++ ++ std::thread::scope(|scope| { ++ let writer_wrapper = Arc::clone(&wrapper); ++ let writer_file = journal_file.clone(); ++ let writer = scope.spawn(move || { ++ writer_wrapper.with_output_operation_snapshot(|| { ++ // Simulate a real DB mutation followed by an interrupted journal write. ++ writer_wrapper.get_rgb_wallet().get_address().unwrap(); ++ std::fs::write(&writer_file, br#"{"status":"preparing"}"#).unwrap(); ++ partial_tx.send(()).unwrap(); ++ finish_rx.recv_timeout(Duration::from_secs(10)).unwrap(); ++ std::fs::write(&writer_file, br#"{"status":"prepared","psbt":"exact"}"#) ++ .unwrap(); ++ Ok(()) ++ }) ++ }); ++ partial_rx.recv_timeout(Duration::from_secs(10)).unwrap(); ++ let backup_wrapper = Arc::clone(&wrapper); ++ let backup = scope.spawn(move || { ++ attempt_tx.send(()).unwrap(); ++ // This is the same admission used by manual backup and policy catch-up. ++ let snapshot = backup_wrapper ++ .get_rgb_wallet_for_snapshot() ++ .prepare_snapshot() ++ .unwrap(); ++ captured_tx.send(()).unwrap(); ++ snapshot ++ }); ++ attempt_rx.recv_timeout(Duration::from_secs(10)).unwrap(); ++ assert_eq!( ++ captured_rx.recv_timeout(Duration::from_millis(100)), ++ Err(mpsc::RecvTimeoutError::Timeout), ++ "snapshot captured a DB/journal transition before the prepared record" ++ ); ++ finish_tx.send(()).unwrap(); ++ writer.join().unwrap().unwrap(); ++ let snapshot = backup.join().unwrap(); ++ let mut archive = zip::ZipArchive::new(Cursor::new(snapshot.data)).unwrap(); ++ let mut saved = String::new(); ++ archive ++ .by_name(&format!( ++ "{fingerprint}/rln_output_operations_v1/coherence-fixture.json" ++ )) ++ .unwrap() ++ .read_to_string(&mut saved) ++ .unwrap(); ++ assert_eq!(saved, r#"{"status":"prepared","psbt":"exact"}"#); ++ }); ++ } ++ ++ #[test] ++ fn an_existing_snapshot_completes_before_output_operation_admission() { ++ use std::sync::mpsc; ++ use std::time::Duration; ++ ++ let wrapper = Arc::new(signetcustom_wallet_source()); ++ let reader = wrapper.get_rgb_wallet_for_snapshot(); ++ let (attempt_tx, attempt_rx) = mpsc::channel(); ++ let (entered_tx, entered_rx) = mpsc::channel(); ++ std::thread::scope(|scope| { ++ let writer_wrapper = Arc::clone(&wrapper); ++ let writer = scope.spawn(move || { ++ attempt_tx.send(()).unwrap(); ++ writer_wrapper.with_output_operation_snapshot(|| { ++ entered_tx.send(()).unwrap(); ++ Ok(()) ++ }) ++ }); ++ attempt_rx.recv_timeout(Duration::from_secs(10)).unwrap(); ++ assert_eq!( ++ entered_rx.recv_timeout(Duration::from_millis(100)), ++ Err(mpsc::RecvTimeoutError::Timeout), ++ "output mutation entered while an existing snapshot owned admission" ++ ); ++ drop(reader); ++ entered_rx.recv_timeout(Duration::from_secs(10)).unwrap(); ++ writer.join().unwrap().unwrap(); ++ }); ++ } ++ ++ #[cfg(feature = "vss")] ++ #[test] ++ fn unchanged_output_status_read_retains_backup_receipt_and_leaves_no_new_upload() { ++ 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()); ++ let before = policy.metadata().unwrap(); ++ wrapper.with_output_operation_snapshot(|| Ok(())).unwrap(); ++ assert!(!policy.pending()); ++ assert!(!policy.failed.load(std::sync::atomic::Ordering::SeqCst)); ++ assert_eq!(policy.metadata().unwrap(), before); ++ assert!(!wrapper.get_rgb_wallet().backup_info().unwrap()); ++ } ++ + #[test] + fn ordinary_bitcoin_prepare_is_distinct_from_unsupported_channel_funding() { + let wrapper = signetcustom_wallet_source(); @@ -5175,8 +5515,52 @@ index a665290..135045c 100644 let already_imported = wallet.is_asset_known(contract_id)?; let imported_id = rgb_lib::wallet::RgbWalletOpsOffline::import_contract( &mut *wallet, +diff --git a/src/rgb_output_operations.rs b/src/rgb_output_operations.rs +index 803e7a8..e15cf14 100644 +--- a/src/rgb_output_operations.rs ++++ b/src/rgb_output_operations.rs +@@ -284,13 +284,18 @@ impl OutputBackend for NativeOutputBackend { + // A prior read must not hide the transaction that this exact retry broadcasts. + *self.observed_cache.lock().map_err(|_| journal_error())? = None; + self.wallet +- .rgb_create_utxos_end(signed_psbt, true) ++ .rgb_wallet_wrapper ++ .get_rgb_wallet() ++ .create_utxos_end(self.wallet.rgb_wallet_wrapper.online, signed_psbt) + .map_err(APIError::from) + } + + fn cancel(&self, txid: String) -> Result<(), APIError> { ++ use rgb_lib::wallet::RgbWalletOpsOffline; + self.wallet +- .rgb_abort_pending_vanilla_tx(txid) ++ .rgb_wallet_wrapper ++ .get_rgb_wallet() ++ .abort_pending_vanilla_tx(txid) + .map_err(APIError::from) + } + +@@ -298,10 +303,14 @@ impl OutputBackend for NativeOutputBackend { + let mut cache = self.observed_cache.lock().map_err(|_| journal_error())?; + if cache.is_none() { + *cache = Some( +- self.wallet +- .rgb_wallet_wrapper +- .list_transactions(false) +- .map_err(|_| ()), ++ { ++ use rgb_lib::wallet::RgbWalletOpsOffline; ++ self.wallet ++ .rgb_wallet_wrapper ++ .get_rgb_wallet() ++ .list_transactions(Some(self.wallet.rgb_wallet_wrapper.online), false) ++ } ++ .map_err(|_| ()), + ); + } + match cache.as_ref().unwrap() { diff --git a/src/routes.rs b/src/routes.rs -index 9f660f5..ec10901 100644 +index 20945c4..23cefcc 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1760,6 +1760,9 @@ impl From for RgbLibWitnessData { @@ -5199,7 +5583,25 @@ index 9f660f5..ec10901 100644 let db = state.get_db(); db.persist_channel_peer(&peer_pubkey, &peer_addr)?; } else { -@@ -4130,7 +4132,7 @@ pub(crate) async fn lock( +@@ -2606,7 +2608,7 @@ pub(crate) async fn create_utxos( + use rgb_lib::wallet::RgbWalletOpsOffline; + wallet.rgb_wallet_wrapper.get_rgb_wallet().get_wallet_dir().join("rln_output_operations_v1") + }; +- tokio::task::spawn_blocking(move || { ++ tokio::task::spawn_blocking(move || wallet.rgb_wallet_wrapper.with_output_operation_snapshot(|| { + let _ownership = wallet.rgb_wallet_wrapper.output_operations.lock().map_err(|_| APIError::Unexpected("RGB output operation ownership is unavailable".into()))?; + let request = crate::rgb_output_operations::OutputRequest { + operation_id: format!("{:032x}", rand::random::()), up_to: payload.up_to, +@@ -2620,7 +2622,7 @@ pub(crate) async fn create_utxos( + let result = store.commit(&backend, &operation.operation_id)?; + if matches!(result.status.as_str(), "broadcast" | "confirmed") { Ok(()) } + else { Err(APIError::Unexpected("RGB output transaction submission is unresolved; inspect its exact operation before retrying".into())) } +- }).await.map_err(|_| APIError::Unexpected("RGB output operation worker failed; inspect its exact operation before retrying".into()))??; ++ })).await.map_err(|_| APIError::Unexpected("RGB output operation worker failed; inspect its exact operation before retrying".into()))??; + tracing::debug!("UTXO creation complete"); + + Ok(Json(EmptyResponse {})) +@@ -4124,7 +4126,7 @@ pub(crate) async fn lock( }; tracing::debug!("Stopping node..."); @@ -5208,7 +5610,7 @@ index 9f660f5..ec10901 100644 tracing::debug!("Node stopped"); state.update_unlocked_app_state(None).await; -@@ -4680,7 +4682,7 @@ pub(crate) async fn open_channel( +@@ -4674,7 +4676,7 @@ pub(crate) async fn open_channel( } } if let Some(peer_addr) = peer_addr { @@ -5217,7 +5619,7 @@ index 9f660f5..ec10901 100644 .await?; db.persist_channel_peer(&peer_pubkey, &peer_addr)?; } else { -@@ -6143,11 +6145,13 @@ mod state_mocks { +@@ -6137,11 +6139,13 @@ mod state_mocks { lsp_base_url: None, lsp_bearer_token: None, vss_url: None, @@ -5232,7 +5634,7 @@ index 9f660f5..ec10901 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 6ec97cf..723e483 100644 +index 14bb967..5b237a1 100644 --- a/src/sdk/mod.rs +++ b/src/sdk/mod.rs @@ -140,6 +140,9 @@ impl Drop for OpenChannelVirtualIntentGuard { @@ -5316,7 +5718,7 @@ index 6ec97cf..723e483 100644 + let guard = check_unlocked(&state).await?; + let wrapper = guard.as_ref().unwrap().rgb_wallet_wrapper.clone(); + tokio::task::spawn_blocking(move || { -+ let mut wallet = wrapper.get_rgb_wallet(); ++ let mut wallet = wrapper.get_rgb_wallet_for_snapshot(); + let policy = wrapper + .snapshot_policy + .as_ref() @@ -5363,7 +5765,45 @@ index 6ec97cf..723e483 100644 state .get_db() .persist_channel_peer(&peer_pubkey, &peer_addr)?; -@@ -3157,8 +3251,7 @@ pub(crate) async fn open_channel( +@@ -2653,18 +2747,26 @@ where + .join("rln_output_operations_v1") + }; + wallet_task(wallet, move |wallet| { +- let _ownership = wallet ++ wallet + .rgb_wallet_wrapper +- .output_operations +- .lock() +- .map_err(|_| { +- APIError::Unexpected("RGB output operation ownership is unavailable".into()) +- })?; +- let backend = crate::rgb_output_operations::NativeOutputBackend::new(Arc::clone(&wallet)); +- operation( +- &crate::rgb_output_operations::OutputStore::new(directory), +- &backend, +- ) ++ .with_output_operation_snapshot(|| { ++ let _ownership = ++ wallet ++ .rgb_wallet_wrapper ++ .output_operations ++ .lock() ++ .map_err(|_| { ++ APIError::Unexpected( ++ "RGB output operation ownership is unavailable".into(), ++ ) ++ })?; ++ let backend = ++ crate::rgb_output_operations::NativeOutputBackend::new(Arc::clone(&wallet)); ++ operation( ++ &crate::rgb_output_operations::OutputStore::new(directory), ++ &backend, ++ ) ++ }) + }) + .await + } +@@ -3234,8 +3336,7 @@ pub(crate) async fn open_channel( } } if let Some(peer_addr) = peer_addr { @@ -5373,7 +5813,7 @@ index 6ec97cf..723e483 100644 state .get_db() .persist_channel_peer(&peer_pubkey, &peer_addr)?; -@@ -4939,11 +5032,13 @@ mod tests { +@@ -5016,11 +5117,13 @@ mod tests { lsp_bearer_token: None, database: RwLock::new(Arc::new(database)), vss_url: None, @@ -6622,7 +7062,7 @@ index 0000000..c9f968e + } +} diff --git a/src/uniffi_api/mod.rs b/src/uniffi_api/mod.rs -index 2724b8d..6df2db9 100644 +index e9d5080..b90a61b 100644 --- a/src/uniffi_api/mod.rs +++ b/src/uniffi_api/mod.rs @@ -1,7 +1,10 @@ @@ -6740,7 +7180,7 @@ index 2724b8d..6df2db9 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))?; -@@ -1784,8 +1842,23 @@ impl SdkNode { +@@ -1857,8 +1915,23 @@ impl SdkNode { ) -> Result<(), RlnError> { let state = self.handle.app_state(); let expected = key_source_from_uniffi_bootstrap(signer.bootstrap()?); @@ -6766,7 +7206,7 @@ index 2724b8d..6df2db9 100644 } #[allow(clippy::too_many_arguments)] -@@ -1828,15 +1901,18 @@ pub fn sdk_initialize(request: SdkInitRequest) -> Result<(), RlnError> { +@@ -1901,15 +1974,18 @@ pub fn sdk_initialize(request: SdkInitRequest) -> Result<(), RlnError> { Ok(()) } @@ -6793,7 +7233,7 @@ index 2724b8d..6df2db9 100644 } pub fn sdk_init_with_external_signer( -@@ -2087,6 +2163,115 @@ impl crate::signer::ExternalSignerTransport for UniffiExternalSignerTransport { +@@ -2160,6 +2236,115 @@ impl crate::signer::ExternalSignerTransport for UniffiExternalSignerTransport { #[cfg(test)] mod tests; @@ -7438,10 +7878,10 @@ index 27ea384..d4abc7f 100644 } diff --git a/src/uniffi_api/types.rs b/src/uniffi_api/types.rs -index 1bf91a1..2251e5b 100644 +index 875ae18..b59e0fe 100644 --- a/src/uniffi_api/types.rs +++ b/src/uniffi_api/types.rs -@@ -863,6 +863,7 @@ pub struct SdkInitRequest { +@@ -922,6 +922,7 @@ pub struct SdkInitRequest { pub lsp_bearer_token: Option, pub vss_url: Option, pub vss_allow_http: bool, diff --git a/scripts/contract.test.js b/scripts/contract.test.js index 857e808..c51d6b6 100644 --- a/scripts/contract.test.js +++ b/scripts/contract.test.js @@ -128,8 +128,7 @@ test('all unsupported APIs throw before accessing a native handle', () => { for (const method of [ 'startUnlockWithNativeExternalSigner', 'nativeOperationStatus', 'adoptNativeOperation', 'cancelNativeOperation', 'vssDeleteAll', 'syncWallet', 'walletSnapshot', 'prepareBtcSend', - 'commitPreparedBtcSend', 'cancelBtcSendPlan', 'prepareCreateUtxos', 'commitPreparedCreateUtxos', - 'cancelCreateUtxosPlan', 'listPendingVanillaTransactions', 'listAddressReceipts', + 'commitPreparedBtcSend', 'cancelBtcSendPlan', 'listAddressReceipts', 'prepareRgbSend', 'commitPreparedRgbSend', 'cancelRgbSendPlan', 'listPendingRgbSendPlans' ]) assert.throws(() => node[method]({}), UnsupportedCapabilityError, method) @@ -317,3 +316,65 @@ test('invoice-first send forwards full invoice, exact amounts and native failure assert.throws(() => node.sendRgbInvoice(request), error) assert.equal(calls.length, 2) }) + + +test('RGB output operations preserve exact identities, phases, integer tokens and native failures', () => { + const operationId = 'ab'.repeat(16) + const txid = 'cd'.repeat(32) + const calls = [] + let failure = false + const nativeError = new Error('RGB output transaction may have been broadcast; cannot cancel') + const dto = { operation_id: operationId, txid, status: 'submitting', can_cancel: false, can_commit: true, + inputs: [{ txid: 'ef'.repeat(32), vout: 0 }], num_outputs: 3, size_sat: 20000, + fee_sat: '9007199254740993', fee_rate: '18446744073709551615' } + const native = Object.fromEntries(['prepareCreateUtxos', 'commitPreparedCreateUtxos', + 'cancelCreateUtxosPlan', 'getCreateUtxosOperation'].map(method => [method, (...args) => { + if (failure) throw nativeError + calls.push([method, args.at(-1)]) + return JSON.stringify(dto).replace(/"(9007199254740993|18446744073709551615)"/g, '$1') + }])) + native.listCreateUtxosOperations = () => JSON.stringify([dto]) + native.listPendingVanillaTransactions = () => JSON.stringify([{ txid, type: 'CreateUtxos' }]) + const node = new (facade().SdkNode)(native) + const request = { operation_id: operationId, up_to: false, num: 4, size: 20000, + fee_rate: '18446744073709551615', skip_sync: false } + for (const method of ['prepareCreateUtxos', 'commitPreparedCreateUtxos', 'cancelCreateUtxosPlan', 'getCreateUtxosOperation']) { + const input = method === 'prepareCreateUtxos' ? request : { operation_id: operationId } + assert.deepEqual(node[method](input), dto) + assert.equal(calls.at(-1)[0], method) + assert.deepEqual(boundary.parse(calls.at(-1)[1]), input) + if (method === 'prepareCreateUtxos') assert.match(calls.at(-1)[1], /"fee_rate":18446744073709551615/) + failure = true + assert.throws(() => node[method](input), nativeError) + failure = false + } + assert.deepEqual(node.listCreateUtxosOperations(), [dto]) + assert.deepEqual(node.listPendingVanillaTransactions(), [{ txid, type: 'CreateUtxos' }]) + native.getCreateUtxosOperation = () => 'null' + const missingNode = new (facade().SdkNode)(native) + assert.equal(missingNode.getCreateUtxosOperation({ operation_id: operationId }), null) + assert.equal(request.fee_rate, '18446744073709551615') +}) + +test('RGB output operation validation rejects lossy or unowned requests before native access', () => { + let calls = 0 + const native = Object.fromEntries(['prepareCreateUtxos', 'commitPreparedCreateUtxos', + 'cancelCreateUtxosPlan', 'getCreateUtxosOperation'].map(method => [method, () => { calls++; return '{}' }])) + const node = new (facade().SdkNode)(native) + const request = { operation_id: 'ab'.repeat(16), up_to: false, num: 4, size: 20000, fee_rate: 2, skip_sync: false } + for (const bad of [undefined, null, [], {}, { ...request, operation_id: 'AB'.repeat(16) }, + { ...request, operation_id: 'x'.repeat(32) }, { ...request, operation_id: 'a'.repeat(65) }, + { ...request, num: 0 }, { ...request, num: 256 }, { ...request, size: -1 }, + { ...request, size: 4294967296 }, { ...request, up_to: 0 }, { ...request, skip_sync: 'false' }, + { ...request, fee_rate: 0 }, { ...request, fee_rate: 1.5 }, { ...request, fee_rate: 2 ** 53 }, + { ...request, fee_rate: '01' }, { ...request, fee_rate: '18446744073709551616' }, + { ...request, fee_rate: null }, { ...request, psbt: 'not-owned-by-operation' }]) { + assert.throws(() => node.prepareCreateUtxos(bad)) + } + for (const method of ['commitPreparedCreateUtxos', 'cancelCreateUtxosPlan', 'getCreateUtxosOperation']) { + for (const bad of [undefined, null, [], {}, { plan_id: request.operation_id }, + { operation_id: request.operation_id, txid: 'cd'.repeat(32) }, + { operation_id: request.operation_id, psbt: 'signed-transaction' }]) assert.throws(() => node[method](bad)) + } + assert.equal(calls, 0) +}) diff --git a/scripts/release-contract.js b/scripts/release-contract.js index 7e12cbb..e619c91 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': 'd10d679f0dd73246f6b475a44bdf13edd5f9b2a4', - 'commit': 'd10d679f0dd73246f6b475a44bdf13edd5f9b2a4', + 'ref': '7cced3aa3c2c904a88d8f8a7832981d25a5ea871', + 'commit': '7cced3aa3c2c904a88d8f8a7832981d25a5ea871', 'lightningCommit': 'bbe68189872c26088e351d034b05c58769cb996a', 'rgbLibRepository': 'https://github.com/UTEXO-Protocol/rgb-lib-bfa.git', 'rgbLibCommit': '89c37086a68ef792da75c62bce59b58b1d7952a0', @@ -32,6 +32,7 @@ const ALLOWED_FILES = Object.freeze([ 'src/lib.rs', 'src/rgb.rs', 'src/rgb_import.rs', + 'src/rgb_output_operations.rs', 'src/vss_snapshot_policy.rs', 'src/uniffi_api/types.rs', 'src/ldk_chain_backend/block_sync.rs', diff --git a/src/lib.rs b/src/lib.rs index 6c8caf2..7e3ba64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,20 +23,24 @@ use napi_derive::napi; use rlncffi::{ free_native_external_signer, free_sdk_node, rln_address, rln_asset_balance, - rln_asset_link_create, rln_asset_metadata, rln_btc_balance, rln_burn, rln_cancel_hodl_invoice, - rln_check_indexer_url, rln_check_proxy_endpoint, rln_claim_hodl_invoice, rln_close_channel, - rln_connect_peer, rln_create_utxos, rln_decode_ln_invoice, rln_decode_rgb_invoice, - rln_disconnect_peer, rln_estimate_fee, rln_fail_transfers, rln_free_string, - rln_get_asset_media, rln_get_channel_id, rln_get_consignment, rln_get_consignment_path, - rln_get_message_signing_address, rln_get_payment, rln_get_swap, rln_import_rgb_contract, - rln_import_rgb_transfer_consignment, rln_inflate, rln_invoice_status, rln_issue_asset_cfa, - rln_issue_asset_ifa, rln_issue_asset_nia, rln_issue_asset_uda, rln_keysend, rln_list_assets, - rln_list_channels, rln_list_payments, rln_list_peers, rln_list_swaps, rln_list_transactions, + rln_asset_link_create, rln_asset_metadata, rln_btc_balance, rln_burn, + rln_cancel_create_utxos_plan, rln_cancel_hodl_invoice, rln_check_indexer_url, + rln_check_proxy_endpoint, rln_claim_hodl_invoice, rln_close_channel, + rln_commit_prepared_create_utxos, rln_connect_peer, rln_create_utxos, rln_decode_ln_invoice, + rln_decode_rgb_invoice, rln_disconnect_peer, rln_estimate_fee, rln_fail_transfers, + rln_free_string, rln_get_asset_media, rln_get_channel_id, rln_get_consignment, + rln_get_consignment_path, rln_get_create_utxos_operation, rln_get_message_signing_address, + rln_get_payment, rln_get_swap, rln_import_rgb_contract, rln_import_rgb_transfer_consignment, + rln_inflate, rln_invoice_status, rln_issue_asset_cfa, rln_issue_asset_ifa, rln_issue_asset_nia, + rln_issue_asset_uda, rln_keysend, rln_list_assets, rln_list_channels, + rln_list_create_utxos_operations, rln_list_payments, rln_list_peers, + rln_list_pending_vanilla_transactions, rln_list_swaps, rln_list_transactions, rln_list_transfers, rln_list_unspents, rln_ln_invoice, rln_maker_execute, rln_maker_init, rln_native_external_signer_bootstrap, rln_native_external_signer_new, rln_native_external_signer_new_with_storage, rln_native_external_signer_shutdown, - rln_network_info, rln_node_info, rln_open_channel, rln_post_asset_media, rln_refresh_transfers, - rln_rgb_invoice, rln_rotate_address, rln_sdk_node_apay_new, rln_sdk_node_apay_new_with_address, + rln_network_info, rln_node_info, rln_open_channel, rln_post_asset_media, + rln_prepare_create_utxos, rln_refresh_transfers, rln_rgb_invoice, rln_rotate_address, + rln_sdk_node_apay_new, rln_sdk_node_apay_new_with_address, rln_sdk_node_attach_native_external_signer, rln_sdk_node_detach_external_signer, rln_sdk_node_init_with_external_signer, rln_sdk_node_init_with_native_external_signer, rln_sdk_node_new, rln_sdk_node_prepare_vss_restore, @@ -507,6 +511,36 @@ impl SdkNode { pub fn create_utxos(&self, request_json: String) -> Result { fwd_json_req!(self, rln_create_utxos, request_json) } + #[napi] + pub fn prepare_create_utxos(&self, request_json: String) -> Result { + fwd_json_req!(self, rln_prepare_create_utxos, request_json) + } + + #[napi] + pub fn commit_prepared_create_utxos(&self, request_json: String) -> Result { + fwd_json_req!(self, rln_commit_prepared_create_utxos, request_json) + } + + #[napi] + pub fn cancel_create_utxos_plan(&self, request_json: String) -> Result { + fwd_json_req!(self, rln_cancel_create_utxos_plan, request_json) + } + + #[napi] + pub fn get_create_utxos_operation(&self, request_json: String) -> Result { + fwd_json_req!(self, rln_get_create_utxos_operation, request_json) + } + + #[napi] + pub fn list_create_utxos_operations(&self) -> Result { + fwd_noarg!(self, rln_list_create_utxos_operations) + } + + #[napi] + pub fn list_pending_vanilla_transactions(&self) -> Result { + fwd_noarg!(self, rln_list_pending_vanilla_transactions) + } + #[napi] pub fn list_transactions(&self, skip_sync: bool) -> Result { let res = rln_list_transactions(&self.handle, skip_sync, std::ptr::null()); From b516a830f579360c88920719527d0a5511da0225 Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Tue, 27 Oct 2026 23:48:52 +0530 Subject: [PATCH 10/11] Adopt qualified RLN overlay and scoped native consumer builds --- .github/workflows/ci.yml | 5 +- .github/workflows/release.yml | 9 + .github/workflows/runtime-matrix.yml | 32 +- BFA-MIGRATION.md | 20 +- Cargo.lock | 717 +----- package.json | 12 +- patches/release-adapter-v0.17.0-beta.3.patch | 2123 +++++++++--------- scripts/install-overlay-addon.test.js | 2 +- scripts/release-contract.js | 14 +- scripts/runtime-matrix.sh | 2 +- scripts/test-native-adapter.js | 23 +- scripts/test-packed-consumer.js | 54 + 12 files changed, 1293 insertions(+), 1720 deletions(-) create mode 100644 scripts/test-packed-consumer.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfdb7c9..2b6b67e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,12 +40,13 @@ jobs: - run: npm test - name: Native adapter tests run: node scripts/test-native-adapter.js - - name: Pack consumer - run: npm pack --ignore-scripts + - name: Execute the packed native consumer + run: node scripts/test-packed-consumer.js - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: node-contract-${{ github.sha }} path: | *.tgz + packed-consumer-evidence.json index-*.provenance.json if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c419382..d270c01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,13 @@ name: Build Candidate Artifacts (No Publication) on: workflow_dispatch: + inputs: + target: + description: Exact host consumer target; select all only for final platform qualification + required: true + type: choice + default: darwin-arm64 + options: [darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu, linux-x64-musl, all] permissions: contents: read @@ -9,5 +16,7 @@ permissions: jobs: candidate: uses: ./.github/workflows/runtime-matrix.yml + with: + target: ${{ inputs.target }} secrets: ORG_READ_TOKEN: ${{ secrets.ORG_READ_TOKEN }} diff --git a/.github/workflows/runtime-matrix.yml b/.github/workflows/runtime-matrix.yml index 18f0e62..f215f02 100644 --- a/.github/workflows/runtime-matrix.yml +++ b/.github/workflows/runtime-matrix.yml @@ -2,7 +2,20 @@ name: Runtime matrix on: workflow_dispatch: + inputs: + target: + description: Exact host consumer target; all is reserved for final platform qualification + type: choice + required: true + default: darwin-arm64 + options: [darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu, linux-x64-musl, all] workflow_call: + inputs: + target: + description: Exact candidate target or explicit all-platform qualification + type: string + required: false + default: darwin-arm64 secrets: ORG_READ_TOKEN: required: true @@ -24,18 +37,11 @@ permissions: jobs: native-runtime: + if: inputs.target != 'linux-x64-musl' strategy: fail-fast: false matrix: - include: - - runner: macos-15 - target: darwin-arm64 - - runner: macos-15-intel - target: darwin-x64 - - runner: ubuntu-22.04 - target: linux-x64-gnu - - runner: ubuntu-24.04-arm - target: linux-arm64-gnu + include: ${{ inputs.target == 'all' && fromJSON('[{"runner":"macos-15","target":"darwin-arm64"},{"runner":"macos-15-intel","target":"darwin-x64"},{"runner":"ubuntu-22.04","target":"linux-x64-gnu"},{"runner":"ubuntu-24.04-arm","target":"linux-arm64-gnu"}]') || inputs.target == 'darwin-arm64' && fromJSON('[{"runner":"macos-15","target":"darwin-arm64"}]') || inputs.target == 'darwin-x64' && fromJSON('[{"runner":"macos-15-intel","target":"darwin-x64"}]') || inputs.target == 'linux-arm64-gnu' && fromJSON('[{"runner":"ubuntu-24.04-arm","target":"linux-arm64-gnu"}]') || fromJSON('[{"runner":"ubuntu-22.04","target":"linux-x64-gnu"}]') }} runs-on: ${{ matrix.runner }} timeout-minutes: ${{ matrix.target == 'darwin-x64' && 150 || 90 }} env: @@ -44,6 +50,11 @@ jobs: EXPECTED_TARGET: ${{ matrix.target }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - name: Validate exact requested target + env: + REQUESTED_TARGET: ${{ inputs.target }} + run: | + case "$REQUESTED_TARGET" in ""|darwin-arm64|darwin-x64|linux-x64-gnu|linux-arm64-gnu|linux-x64-musl|all) ;; *) exit 1 ;; esac - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 if: runner.os == 'macOS' with: @@ -80,6 +91,7 @@ jobs: name: runtime-${{ matrix.target }}-${{ github.sha }} path: | runtime-evidence.json + packed-consumer-evidence.json linux-runtime-compatibility.json index-*.provenance.json *.tgz @@ -92,6 +104,7 @@ jobs: if-no-files-found: warn musl-runtime: + if: inputs.target == 'all' || inputs.target == 'linux-x64-musl' runs-on: ubuntu-22.04 timeout-minutes: 90 steps: @@ -115,6 +128,7 @@ jobs: name: runtime-linux-x64-musl-${{ github.sha }} path: | runtime-evidence.json + packed-consumer-evidence.json linux-runtime-compatibility.json index-*.provenance.json *.tgz diff --git a/BFA-MIGRATION.md b/BFA-MIGRATION.md index a0c3b9d..3e614a3 100644 --- a/BFA-MIGRATION.md +++ b/BFA-MIGRATION.md @@ -9,10 +9,10 @@ Candidate `0.2.0-bfa.1` sends an empty JSON-RPC parameter array for EVM methods | Source | Exact revision | | --- | --- | -| RGB library | `89c37086a68ef792da75c62bce59b58b1d7952a0` | -| LDK | `bbe68189872c26088e351d034b05c58769cb996a` | -| RLN | `7cced3aa3c2c904a88d8f8a7832981d25a5ea871` | -| Maintained adapter SHA-256 | `b70bb70610ff1b47eb55ea2bdf67a65af85e89fbabc006f2345302cbe74f1082` | +| RGB library | `abd41d3afff6d8fb11461e26e843301650e22df3` | +| LDK | `17f2fb56f8d96d0ec512c715d2ebc65f829eb2c2` | +| RLN | `89ebe9fdb1be94cbba0b2bd722cea9af297399f1` | +| Maintained adapter SHA-256 | `4a730e3036e1197b2d9cd2d2584de722745f35e74dd1707dfe9b08e251b8b12b` | This graph requires fresh native builds and separately frozen candidate archives. Previous `0.2.0-bfa.0` artifacts and qualification records retain their original identities and do not qualify this candidate. @@ -64,8 +64,14 @@ Persist a random `operation_id` containing 32–64 lowercase hexadecimal charact The legacy one-shot `createUtxos()` remains available for compatibility. Applications needing review, interruption recovery and exact idempotent retries should use the prepared operation API. Host JavaScript tests are distinct from funded native and Android application qualification; retain the exact source and artifact receipts for each tested build. -## Output-operation qualification scope +## Final accepted graph and maintained-overlay qualification -This source candidate passes 21 durable-output Rust cases, including two real funded isolated regtest fixtures, 13 wallet-source/snapshot-barrier cases and 53 VSS cases. Snapshot capture and every output-operation journal/database transition share an ordered barrier, so an already-running automatic backup cannot capture a reservation without its prepared transaction. Existing automatic-backup ownership and completion semantics remain in place. Status-only reads of an unchanged acknowledged wallet do not invent a new backup receipt. +The maintained overlay is rebased on RLN `89ebe9fdb1be94cbba0b2bd722cea9af297399f1`, RGB `abd41d3afff6d8fb11461e26e843301650e22df3` and the canonical merged LDK `17f2fb56f8d96d0ec512c715d2ebc65f829eb2c2`. LDK's earlier reviewed branch commit is not substituted for its merged identity. The inherited LDK legacy test-harness limitation remains separate. -Bare passes 112 host installer/contract tests; Node passes 47 host contract tests. Both pass their TypeScript checks. These counts do not qualify a packaged native platform. Only an Android ARM64 candidate artifact is requested now; all other platform binaries, full distribution assembly and corresponding runtime qualification remain deferred. The normal all-target publication gates remain unchanged. No package has been published. +All six newly reachable native funding/prepared-sweep mutation sites explicitly admit durable snapshot intent. Nonzero custom locktimes reject before mutation admission; prepared-sweep inspection remains read-only. The complete output-operation writer still spans native reservation and RLN journal writes. Its inner backend guards remain raw to avoid recursive barrier acquisition and incomplete snapshots. Full authenticated invoices, native prepared-sweep confirmation/receipt checks and uncancellable native session owners remain intact. + +Final maintained library qualification passes 453 tests with no failures. Four entries are explicitly ignored in that default invocation: one subprocess helper and three owned funded fixtures. All three funded fixtures were separately executed and passed: watch-only output setup/recovery; independent funding despite a retained legacy reservation; and funding/protected-sweep snapshot admission in both paused and enabled automatic-backup modes. The latter checks actual fixed-zero funding, fee-input retry, prepare/cancel/recovery, stale snapshot rejection, handoff protection, zero-confirmation rejection, confirmed idempotent consumption and exact 100-unit self-receive. Upload failure never changes the financial result or invents a backup completion. + +The full library run includes actual protobuf VSS client/server backup, reopen, fresh restore, legacy-record preservation, canceled-caller lock barriers, durable policy, native signer shutdown/reopen and BIP-322. The supplied 639-byte Dev-USDT public contract imports on native Signet with its exact asset identity, name, ticker and precision; importing it on a different native chain rejects. This metadata-only test creates no USDT allocation and is not a funded hosted-bridge acceptance claim. Only a confirmed cloud commit with matching content acknowledgement can clear pending state; local snapshot acknowledgement alone cannot. + +Bare passes 112 installer/contract tests and Node passes 47 host contract tests; both TypeScript checks pass. These source/host scopes do not qualify an Android APK. Android ARM64 native artifact production and final consumer/application acceptance are recorded separately with exact source, adapter, lock, wrapper and binary identities. Other platform binaries and full distribution assembly remain deferred. Strict all-target publication gates remain unchanged; this candidate is not published. diff --git a/Cargo.lock b/Cargo.lock index f0d3c36..0202cc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,15 +206,6 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -723,9 +714,9 @@ checksum = "43064fa3dd1d3a8b24079cfcb5ede6b785857edc782277f17a1736511ccc1916" [[package]] name = "bdk_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3028782f6bf14a6df987244333d34e6b272b5a40a53e4879ec2dfd82275a3a" +checksum = "6cbb1373ac1fafacd7adab91f58ab18d455dfb6434d47a647e75315613599cad" dependencies = [ "bitcoin", "hashbrown 0.14.5", @@ -734,9 +725,9 @@ dependencies = [ [[package]] name = "bdk_electrum" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00a9846105bf6e751adbb6946b000ff919ce24a15a941cbfe68485549b552a9c" +checksum = "97455296f4f798250eaa5f7aa51fc34298a72fb661784b4c52c190d730ad04f7" dependencies = [ "bdk_core", "electrum-client 0.25.0", @@ -744,9 +735,9 @@ dependencies = [ [[package]] name = "bdk_esplora" -version = "0.22.2" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83986307ea92997c3d051e8c306af8115a05add601e22acb7c1903008e6b614e" +checksum = "5ac04fdd76362dccf600ccf338ff6be7ad6a0d064def5969e4b33c171718e913" dependencies = [ "bdk_core", "esplora-client", @@ -1189,9 +1180,9 @@ dependencies = [ [[package]] name = "cbindgen" -version = "0.29.2" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" +checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20" dependencies = [ "clap", "heck 0.5.0", @@ -1801,17 +1792,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -2117,17 +2097,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - [[package]] name = "etcetera" version = "0.11.0" @@ -2216,17 +2185,6 @@ dependencies = [ "ref-cast", ] -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "flume" version = "0.12.0" @@ -2551,8 +2509,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -2573,15 +2529,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "hashlink" version = "0.11.1" @@ -2979,17 +2926,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "inherent" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee2c455ca60511a054699102d40ce7153621cb2429558f9eaa600e4499b5984" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - [[package]] name = "inout" version = "0.1.4" @@ -3166,9 +3102,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "lexical-core" @@ -3255,10 +3188,7 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags 2.13.1", "libc", - "plain", - "redox_syscall 0.9.0", ] [[package]] @@ -3503,7 +3433,7 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ - "nix", + "nix 0.29.0", "serde", "winapi", ] @@ -3523,16 +3453,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - [[package]] name = "md-5" version = "0.11.0" @@ -3679,9 +3599,9 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" +checksum = "b899b545d3aa6dca985939059f258c5488d34e4ecf39c274e20009748f4b846d" [[package]] name = "napi-derive" @@ -3751,6 +3671,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3780,22 +3712,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.7", - "smallvec", - "zeroize", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -3820,16 +3736,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3995,7 +3901,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -4109,17 +4015,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - [[package]] name = "pkcs8" version = "0.9.0" @@ -4470,7 +4365,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand 0.10.2", + "rand 0.10.3", "rand_pcg", "ring", "rustc-hash", @@ -4547,9 +4442,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20 0.10.1", "getrandom 0.4.3", @@ -4636,15 +4531,6 @@ dependencies = [ "bitflags 2.13.1", ] -[[package]] -name = "redox_syscall" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" -dependencies = [ - "bitflags 2.13.1", -] - [[package]] name = "redox_users" version = "0.4.6" @@ -4886,7 +4772,7 @@ dependencies = [ [[package]] name = "rgb-lib" version = "0.3.0-beta.7" -source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" +source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=abd41d3afff6d8fb11461e26e843301650e22df3#abd41d3afff6d8fb11461e26e843301650e22df3" dependencies = [ "amplify", "base64 0.23.1", @@ -4900,7 +4786,7 @@ dependencies = [ "hkdf 0.13.0", "hmac 0.13.0", "libc", - "rand 0.10.2", + "rand 0.10.3", "reqwest 0.13.4", "rgb-invoicing", "rgb-lib-migration", @@ -4911,8 +4797,8 @@ dependencies = [ "rgb-strict-types", "rustls 0.23.42", "scrypt 0.12.0", - "sea-orm 2.0.2", - "sea-query 1.0.2", + "sea-orm", + "sea-query", "serde", "serde_json", "sha2 0.11.0", @@ -4926,15 +4812,15 @@ dependencies = [ "url", "walkdir", "x25519-dalek", - "zip 8.6.0", + "zip", ] [[package]] name = "rgb-lib-migration" version = "0.3.0-beta.5" -source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" +source = "git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=abd41d3afff6d8fb11461e26e843301650e22df3#abd41d3afff6d8fb11461e26e843301650e22df3" dependencies = [ - "sea-orm-migration 2.0.2", + "sea-orm-migration", "tokio", ] @@ -4983,7 +4869,7 @@ dependencies = [ "rln-migration", "rustls 0.23.42", "scrypt 0.11.0", - "sea-orm 1.1.20", + "sea-orm", "serde", "serde_json", "sha2 0.10.9", @@ -5007,7 +4893,7 @@ dependencies = [ "vls-protocol-signer", "vss-client-ng", "walkdir", - "zip 2.4.2", + "zip", ] [[package]] @@ -5174,7 +5060,7 @@ dependencies = [ name = "rln-migration" version = "0.1.0" dependencies = [ - "sea-orm-migration 1.1.20", + "sea-orm-migration", "tokio", ] @@ -5188,26 +5074,6 @@ dependencies = [ "rln-c-ffi", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", -] - [[package]] name = "rust_decimal" version = "1.42.1" @@ -5495,33 +5361,9 @@ dependencies = [ [[package]] name = "sea-orm" -version = "1.1.20" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957" -dependencies = [ - "async-stream", - "async-trait", - "chrono", - "derive_more", - "futures-util", - "log", - "ouroboros", - "sea-orm-macros 1.1.20", - "sea-query 0.32.7", - "sea-query-binder", - "serde", - "sqlx 0.8.6", - "strum 0.26.3", - "thiserror 2.0.19", - "tracing", - "url", -] - -[[package]] -name = "sea-orm" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334e83ced3ae3ee44db0f84d1fcf8d2087a1ad9bb9036f00f9f6067156ea197" +checksum = "40bc08e0aa1963295a30473f98d9b4c9c2b0b3a028381e0143cb2647d0a59a32" dependencies = [ "async-stream", "async-trait", @@ -5537,15 +5379,15 @@ dependencies = [ "pgvector", "rust_decimal", "sea-orm-arrow", - "sea-orm-macros 2.0.2", - "sea-query 1.0.2", + "sea-orm-macros", + "sea-query", "sea-query-sqlx", - "sea-schema 0.18.1", + "sea-schema", "serde", "serde_json", - "sqlx 0.9.0", - "sqlx-core 0.9.0", - "strum 0.28.0", + "sqlx", + "sqlx-core", + "strum", "thiserror 2.0.19", "time", "tracing", @@ -5561,34 +5403,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c800d9db902534d7d01728faf98e33d13c1d57bb8c57d8e4c518309172bddda" dependencies = [ "arrow", - "sea-query 1.0.2", + "sea-query", "thiserror 2.0.19", ] [[package]] name = "sea-orm-cli" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da80ebcdb44571e86f03a2bdcb5532136a87397f366f38bbce64673fc5e6a450" -dependencies = [ - "chrono", - "clap", - "dotenvy", - "glob", - "regex", - "sea-schema 0.16.2", - "sqlx 0.8.6", - "tokio", - "tracing", - "tracing-subscriber", - "url", -] - -[[package]] -name = "sea-orm-cli" -version = "2.0.2" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a53505884d7c907bcf4f7b4ddb1b29425e62fef8b98aea9c99e17781cceb798" +checksum = "04469c4a54e7271fe3fe2b8140102a2cd2eb7e48129a3afcd013b9909ddebc3c" dependencies = [ "chrono", "clap", @@ -5596,8 +5419,8 @@ dependencies = [ "glob", "indoc", "regex", - "sea-schema 0.18.1", - "sqlx 0.9.0", + "sea-schema", + "sqlx", "tokio", "tracing", "tracing-subscriber", @@ -5606,23 +5429,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9a3f90e336ec74803e8eb98c61bc98754c1adfba3b4f84d946237b752b1c88" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "sea-bae", - "syn 2.0.119", - "unicode-ident", -] - -[[package]] -name = "sea-orm-macros" -version = "2.0.2" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4039a86f9acc4d3b52747508b347dddc6fd725bbc429902ebeb6d26225fc2528" +checksum = "91fa8c81c20fc8cfe78d22d9a2721ca5ef151dc4a8a7b372de218aaba33733fa" dependencies = [ "heck 0.5.0", "itertools 0.14.0", @@ -5636,48 +5445,20 @@ dependencies = [ [[package]] name = "sea-orm-migration" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c577f2959277e936c1d08109acd1e08fc36a95ef29ec028190ba82cad8f96e" -dependencies = [ - "async-trait", - "clap", - "dotenvy", - "sea-orm 1.1.20", - "sea-orm-cli 1.1.20", - "sea-schema 0.16.2", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "sea-orm-migration" -version = "2.0.2" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd09adbef87100d07131a60a8c5508b53d0cf2136521f654aae47af5e6a097fe" +checksum = "07970137dbbed6daf56a2429db349abfb68ed1c79206957c30d4a0e9ea808c08" dependencies = [ "async-trait", "clap", "dotenvy", - "sea-orm 2.0.2", - "sea-orm-cli 2.0.2", - "sea-schema 0.18.1", + "sea-orm", + "sea-orm-cli", + "sea-schema", "tracing", "tracing-subscriber", ] -[[package]] -name = "sea-query" -version = "0.32.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" -dependencies = [ - "chrono", - "inherent", - "ordered-float", - "sea-query-derive 0.4.3", -] - [[package]] name = "sea-query" version = "1.0.2" @@ -5688,37 +5469,12 @@ dependencies = [ "itoa", "ordered-float", "rust_decimal", - "sea-query-derive 1.0.0", + "sea-query-derive", "serde_json", "time", "uuid", ] -[[package]] -name = "sea-query-binder" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" -dependencies = [ - "chrono", - "sea-query 0.32.7", - "sqlx 0.8.6", -] - -[[package]] -name = "sea-query-derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" -dependencies = [ - "darling 0.20.11", - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.119", - "thiserror 2.0.19", -] - [[package]] name = "sea-query-derive" version = "1.0.0" @@ -5739,21 +5495,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eaa419cdb9157da1361186b1959983eb2ea0dcb9a3c69dc45c449ecb2af8fef" dependencies = [ - "sea-query 1.0.2", - "sqlx 0.9.0", -] - -[[package]] -name = "sea-schema" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338" -dependencies = [ - "futures", - "sea-query 0.32.7", - "sea-query-binder", - "sea-schema-derive", - "sqlx 0.8.6", + "sea-query", + "sqlx", ] [[package]] @@ -5763,10 +5506,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3553c77dceed56e95bece9ea876c4dd67ca879ef51055a0b97a7bb89a8ae4fed" dependencies = [ "async-trait", - "sea-query 1.0.2", + "sea-query", "sea-query-sqlx", "sea-schema-derive", - "sqlx 0.9.0", + "sqlx", ] [[package]] @@ -5954,9 +5697,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" dependencies = [ "serde_core", ] @@ -6024,17 +5767,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.11.0" @@ -6262,67 +5994,17 @@ dependencies = [ "der 0.7.10", ] -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core 0.8.6", - "sqlx-macros 0.8.6", - "sqlx-mysql 0.8.6", - "sqlx-postgres 0.8.6", - "sqlx-sqlite 0.8.6", -] - [[package]] name = "sqlx" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ - "sqlx-core 0.9.0", - "sqlx-macros 0.9.0", - "sqlx-mysql 0.9.0", - "sqlx-postgres 0.9.0", - "sqlx-sqlite 0.9.0", -] - -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64 0.22.1", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink 0.10.0", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "rustls 0.23.42", - "serde", - "serde_json", - "sha2 0.10.9", - "smallvec", - "thiserror 2.0.19", - "tokio", - "tokio-stream", - "tracing", - "url", - "webpki-roots 0.26.11", + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] @@ -6344,7 +6026,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink 0.11.1", + "hashlink", "indexmap", "log", "memchr", @@ -6365,19 +6047,6 @@ dependencies = [ "webpki-roots 1.0.9", ] -[[package]] -name = "sqlx-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core 0.8.6", - "sqlx-macros-core 0.8.6", - "syn 2.0.119", -] - [[package]] name = "sqlx-macros" version = "0.9.0" @@ -6386,36 +6055,11 @@ checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", - "sqlx-core 0.9.0", - "sqlx-macros-core 0.9.0", + "sqlx-core", + "sqlx-macros-core", "syn 2.0.119", ] -[[package]] -name = "sqlx-macros-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck 0.5.0", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2 0.10.9", - "sqlx-core 0.8.6", - "sqlx-mysql 0.8.6", - "sqlx-postgres 0.8.6", - "sqlx-sqlite 0.8.6", - "syn 2.0.119", - "tokio", - "url", -] - [[package]] name = "sqlx-macros-core" version = "0.9.0" @@ -6432,58 +6076,15 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "sqlx-core 0.9.0", - "sqlx-mysql 0.9.0", - "sqlx-postgres 0.9.0", - "sqlx-sqlite 0.9.0", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", "syn 2.0.119", "tokio", "url", ] -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags 2.13.1", - "byteorder", - "bytes", - "chrono", - "crc", - "digest 0.10.7", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf 0.12.4", - "hmac 0.12.1", - "itoa", - "log", - "md-5 0.10.6", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.7", - "rsa", - "serde", - "sha1 0.10.7", - "sha2 0.10.9", - "smallvec", - "sqlx-core 0.8.6", - "stringprep", - "thiserror 2.0.19", - "tracing", - "whoami 1.6.1", -] - [[package]] name = "sqlx-mysql" version = "0.9.0" @@ -6505,53 +6106,15 @@ dependencies = [ "percent-encoding", "rust_decimal", "serde", - "sha1 0.11.0", + "sha1", "sha2 0.11.0", - "sqlx-core 0.9.0", + "sqlx-core", "thiserror 2.0.19", "time", "tracing", "uuid", ] -[[package]] -name = "sqlx-postgres" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" -dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags 2.13.1", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera 0.8.0", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf 0.12.4", - "hmac 0.12.1", - "home", - "itoa", - "log", - "md-5 0.10.6", - "memchr", - "once_cell", - "rand 0.8.7", - "serde", - "serde_json", - "sha2 0.10.9", - "smallvec", - "sqlx-core 0.8.6", - "stringprep", - "thiserror 2.0.19", - "tracing", - "whoami 1.6.1", -] - [[package]] name = "sqlx-postgres" version = "0.9.0" @@ -6565,7 +6128,7 @@ dependencies = [ "chrono", "crc", "dotenvy", - "etcetera 0.11.0", + "etcetera", "futures-channel", "futures-core", "futures-util", @@ -6574,46 +6137,21 @@ dependencies = [ "hmac 0.13.0", "itoa", "log", - "md-5 0.11.0", + "md-5", "memchr", - "rand 0.10.2", + "rand 0.10.3", "rust_decimal", "serde", "serde_json", "sha2 0.11.0", "smallvec", - "sqlx-core 0.9.0", + "sqlx-core", "stringprep", "thiserror 2.0.19", "time", "tracing", "uuid", - "whoami 2.1.3", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "chrono", - "flume 0.11.1", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core 0.8.6", - "thiserror 2.0.19", - "tracing", - "url", + "whoami", ] [[package]] @@ -6624,7 +6162,7 @@ checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", "chrono", - "flume 0.12.0", + "flume", "form_urlencoded", "futures-channel", "futures-core", @@ -6635,7 +6173,7 @@ dependencies = [ "log", "percent-encoding", "serde", - "sqlx-core 0.9.0", + "sqlx-core", "thiserror 2.0.19", "time", "tracing", @@ -6672,12 +6210,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" - [[package]] name = "strum" version = "0.28.0" @@ -6864,11 +6396,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde_core", @@ -7027,7 +6560,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.1.1", + "serde_spanned 1.1.2", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", @@ -7104,9 +6637,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -7252,9 +6785,9 @@ dependencies = [ [[package]] name = "typed-path" -version = "0.12.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" [[package]] name = "typenum" @@ -7297,9 +6830,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -7685,12 +7218,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -7782,15 +7309,6 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.9", -] - [[package]] name = "webpki-roots" version = "1.0.9" @@ -7821,16 +7339,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] - [[package]] name = "whoami" version = "2.1.3" @@ -8255,31 +7763,18 @@ dependencies = [ [[package]] name = "zip" -version = "2.4.2" +version = "9.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +checksum = "ec2e3161de27091f18a831d9494e9c53378a34833af5c1dda35ad85849da18d6" dependencies = [ - "arbitrary", "crc32fast", - "crossbeam-utils", - "displaydoc", + "getrandom 0.4.3", "indexmap", "memchr", - "thiserror 2.0.19", + "nix 0.31.3", "time", - "zstd", -] - -[[package]] -name = "zip" -version = "8.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" -dependencies = [ - "crc32fast", - "indexmap", - "memchr", "typed-path", + "windows-sys 0.61.2", "zstd", ] @@ -8291,27 +7786,27 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" -version = "0.13.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.4" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.2+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" dependencies = [ "cc", "pkg-config", diff --git a/package.json b/package.json index ea0fcc9..ba04cb9 100644 --- a/package.json +++ b/package.json @@ -60,16 +60,16 @@ "author": "UTEXO", "utexoNativeOverlay": { "repository": "https://github.com/UTEXO-Protocol/rgb-lightning-node.git", - "ref": "7cced3aa3c2c904a88d8f8a7832981d25a5ea871", - "commit": "7cced3aa3c2c904a88d8f8a7832981d25a5ea871", - "lightningCommit": "bbe68189872c26088e351d034b05c58769cb996a", + "ref": "89ebe9fdb1be94cbba0b2bd722cea9af297399f1", + "commit": "89ebe9fdb1be94cbba0b2bd722cea9af297399f1", + "lightningCommit": "17f2fb56f8d96d0ec512c715d2ebc65f829eb2c2", "patch": "patches/release-adapter-v0.17.0-beta.3.patch", - "patchSha256": "b70bb70610ff1b47eb55ea2bdf67a65af85e89fbabc006f2345302cbe74f1082", - "cffiLockSha256": "3039a36910386f9d6f350ed33791cd6461174cd340247998b80726a6581c3763", + "patchSha256": "4a730e3036e1197b2d9cd2d2584de722745f35e74dd1707dfe9b08e251b8b12b", + "cffiLockSha256": "741bd3d0f81b28c1f40e426b44f5c9ae152d777269934d7c0dc919d74f908da9", "rustToolchain": "1.94.0", "rlnVersion": "0.17.0-beta.3", "rgbLibRepository": "https://github.com/UTEXO-Protocol/rgb-lib-bfa.git", - "rgbLibCommit": "89c37086a68ef792da75c62bce59b58b1d7952a0" + "rgbLibCommit": "abd41d3afff6d8fb11461e26e843301650e22df3" }, "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 168835f..149f490 100644 --- a/patches/release-adapter-v0.17.0-beta.3.patch +++ b/patches/release-adapter-v0.17.0-beta.3.patch @@ -1,825 +1,8 @@ diff --git a/bindings/c-ffi/Cargo.lock b/bindings/c-ffi/Cargo.lock -index be5eede..980a432 100644 +index 904b9b4..f3afb74 100644 --- a/bindings/c-ffi/Cargo.lock +++ b/bindings/c-ffi/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" -@@ -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", -@@ -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", - ] -@@ -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 = [ - "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]] - name = "file-format" - version = "0.29.0" -@@ -3084,13 +3133,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", - ] - -@@ -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 = [ - "bitflags 2.13.1", - "libc", - "plain", -- "redox_syscall 0.7.5", -+ "redox_syscall 0.9.0", - ] - - [[package]] -@@ -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" --source = "registry+https://github.com/rust-lang/crates.io-index" --checksum = "b7260095a79a44b09ebd97e77855ec8b7996ab939223369f66244974a374bd2c" - dependencies = [ - "bitcoin", - "electrum-client 0.24.1", - "esplora-client", - "lightning 0.2.2", -- "lightning-macros 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -+ "lightning-macros 0.2.1", - ] - - [[package]] -@@ -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 = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" -+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" - dependencies = [ - "bitflags 2.13.1", - "cfg-if", -@@ -3783,9 +3821,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", -@@ -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]] - 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", - "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=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" - 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", - "serde", - "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]] - 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=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0" - 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 = "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", -@@ -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", - ] - -@@ -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]] - 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 = "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", -@@ -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 = [ - "cfg-if", - "once_cell", -@@ -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]] - 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", -@@ -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" +@@ -7263,7 +7263,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ @@ -828,54 +11,14 @@ index be5eede..980a432 100644 ] [[package]] -@@ -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 = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" - dependencies = [ - "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..7ad7229 100644 +index 53d5925..7ad7229 100644 --- a/bindings/c-ffi/Cargo.toml +++ b/bindings/c-ffi/Cargo.toml -@@ -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", rev = "45c72edfd58620849eb486925439a76526b415ae" } -+vls-protocol-signer = { git = "https://github.com/UTEXO-Protocol/vls-protocol-signer.git", rev = "dddd336fcdf84c4febdd7af8363bda8d1721d893" } +@@ -58,3 +58,17 @@ lightning-rapid-gossip-sync = { path = "../../rust-lightning/lightning-rapid-gos + lightning-transaction-sync = { path = "../../rust-lightning/lightning-transaction-sync" } + 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. @@ -1189,7 +332,7 @@ index 764cc94..b84559f 100644 + } // extern "C" diff --git a/bindings/c-ffi/src/api.rs b/bindings/c-ffi/src/api.rs -index 453f97f..6648a9d 100644 +index 453f97f..248c8b7 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( @@ -1244,11 +387,14 @@ index 453f97f..6648a9d 100644 } pub(crate) fn get_payment( -@@ -338,6 +365,13 @@ pub(crate) fn send_rgb(node: &COpaqueStruct, request_json: *const c_char) -> Res +@@ -338,6 +365,16 @@ pub(crate) fn send_rgb(node: &COpaqueStruct, request_json: *const c_char) -> Res json(JsonSendRgbResponse::from(resp)) } -+pub(crate) fn send_rgb_invoice(node: &COpaqueStruct, request_json: *const c_char) -> Result { ++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()?)?; @@ -1258,7 +404,7 @@ index 453f97f..6648a9d 100644 pub(crate) fn import_rgb_transfer_consignment( node: &COpaqueStruct, request_json: *const c_char, -@@ -793,7 +827,7 @@ pub(crate) fn sdk_global_initialize(request_json: *const c_char) -> Result Result Result { @@ -1267,7 +413,7 @@ index 453f97f..6648a9d 100644 ok_void() } -@@ -827,6 +861,34 @@ pub(crate) fn native_external_signer_bootstrap(signer: &COpaqueStruct) -> Result +@@ -827,6 +864,34 @@ pub(crate) fn native_external_signer_bootstrap(signer: &COpaqueStruct) -> Result json(JsonSdkExternalSignerBootstrap::from(bootstrap)) } @@ -1302,7 +448,7 @@ index 453f97f..6648a9d 100644 pub(crate) fn sdk_node_init_with_native_external_signer( node: &COpaqueStruct, signer: &COpaqueStruct, -@@ -884,3 +946,62 @@ pub(crate) fn sdk_node_unlock_with_attached_external_signer( +@@ -884,3 +949,62 @@ pub(crate) fn sdk_node_unlock_with_attached_external_signer( node.unlock_with_attached_external_signer_request(r.into())?; ok_void() } @@ -1366,7 +512,7 @@ index 453f97f..6648a9d 100644 + )?) +} diff --git a/bindings/c-ffi/src/json_types.rs b/bindings/c-ffi/src/json_types.rs -index 7afc138..f6e1681 100644 +index 06dd5dc..d6820dc 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::{ @@ -1417,7 +563,7 @@ index 7afc138..f6e1681 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,31 +841,147 @@ impl From for JsonDecodeLnInvoiceResponse { +@@ -836,6 +841,45 @@ impl From for JsonDecodeLnInvoiceResponse { } } @@ -1463,8 +609,7 @@ index 7afc138..f6e1681 100644 #[derive(Debug, Serialize)] pub(crate) struct JsonDecodeRgbInvoiceResponse { pub recipient_id: String, -- pub proxy_recipient_id: String, -+ pub proxy_recipient_id: Option, +@@ -843,7 +887,7 @@ pub(crate) struct JsonDecodeRgbInvoiceResponse { pub recipient_type: String, pub asset_schema: Option, pub asset_id: Option, @@ -1473,8 +618,8 @@ index 7afc138..f6e1681 100644 pub network: String, pub expiration_timestamp: Option, pub transport_endpoints: Vec, -+ pub public_key: Option, -+ pub encryption: String, +@@ -851,20 +895,93 @@ pub(crate) struct JsonDecodeRgbInvoiceResponse { + pub encryption: String, } -impl From for JsonDecodeRgbInvoiceResponse { @@ -1495,8 +640,8 @@ index 7afc138..f6e1681 100644 network: r.network, expiration_timestamp: r.expiration_timestamp, transport_endpoints: r.transport_endpoints, -+ public_key: r.public_key, -+ encryption: r.encryption, + public_key: r.public_key, + encryption: r.encryption, + }) + } +} @@ -1571,7 +716,7 @@ index 7afc138..f6e1681 100644 } } } -@@ -926,6 +1047,97 @@ pub(crate) struct JsonWitnessData { +@@ -930,6 +1047,97 @@ pub(crate) struct JsonWitnessData { pub blinding: Option, } @@ -1669,7 +814,7 @@ index 7afc138..f6e1681 100644 impl From for WitnessData { fn from(w: JsonWitnessData) -> Self { WitnessData { -@@ -2240,6 +2452,7 @@ impl From for JsonUtxo { +@@ -2244,6 +2452,7 @@ impl From for JsonUtxo { pub(crate) struct JsonUnspent { pub utxo: JsonUtxo, pub rgb_allocations: Vec, @@ -1677,7 +822,7 @@ index 7afc138..f6e1681 100644 } impl From for JsonUnspent { -@@ -2247,6 +2460,30 @@ impl From for JsonUnspent { +@@ -2251,6 +2460,30 @@ impl From for JsonUnspent { JsonUnspent { utxo: u.utxo.into(), rgb_allocations: u.rgb_allocations.into_iter().map(Into::into).collect(), @@ -1708,8 +853,18 @@ index 7afc138..f6e1681 100644 } } } +@@ -2476,7 +2709,8 @@ mod tests { + encryption: encryption.into(), + }; + let json = +- serde_json::to_value(JsonDecodeRgbInvoiceResponse::from(response)).unwrap(); ++ serde_json::to_value(JsonDecodeRgbInvoiceResponse::try_from(response).unwrap()) ++ .unwrap(); + assert_eq!( + json["proxy_recipient_id"], + serde_json::json!(proxy_recipient_id) diff --git a/bindings/c-ffi/src/lib.rs b/bindings/c-ffi/src/lib.rs -index cb94207..8188e7a 100644 +index cb94207..474160b 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,9 +877,9 @@ index cb94207..8188e7a 100644 + ffi_call!("rln_binding_build_info", Ok::(serde_json::json!({ + "abi_version": 1, + "rln_version": "0.17.0-beta.3", -+ "rln_commit": "7cced3aa3c2c904a88d8f8a7832981d25a5ea871", -+ "lightning_commit": "bbe68189872c26088e351d034b05c58769cb996a", -+ "rgb_lib_commit": "89c37086a68ef792da75c62bce59b58b1d7952a0", ++ "rln_commit": "89ebe9fdb1be94cbba0b2bd722cea9af297399f1", ++ "lightning_commit": "17f2fb56f8d96d0ec512c715d2ebc65f829eb2c2", ++ "rgb_lib_commit": "abd41d3afff6d8fb11461e26e843301650e22df3", + "adapter_sha256": env!("RLN_ADAPTER_SHA256"), + "wrapper_sha256": env!("RLN_WRAPPER_SHA256"), + "lock_sha256": env!("RLN_LOCK_SHA256"), @@ -1824,19 +979,25 @@ index cb94207..8188e7a 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, -@@ -342,6 +400,11 @@ pub extern "C" fn rln_send_rgb(node: &COpaqueStruct, request_json: *const c_char +@@ -342,6 +400,17 @@ 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)) ++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, -@@ -793,6 +856,39 @@ pub extern "C" fn rln_sdk_shutdown() -> CResultString { +@@ -793,6 +862,39 @@ pub extern "C" fn rln_sdk_shutdown() -> CResultString { // External-signer surface // --------------------------------------------------------------------------- @@ -1876,7 +1037,7 @@ index cb94207..8188e7a 100644 /// Drop a `NativeExternalSigner` handle. Safe to call immediately after /// attach / init / unlock succeeds: RLN holds its own `Arc` clone. #[unsafe(no_mangle)] -@@ -814,6 +910,86 @@ mod free_tests { +@@ -814,6 +916,86 @@ mod free_tests { free_sdk_node(COpaqueStruct::null()); free_native_external_signer(COpaqueStruct::null()); } @@ -1963,7 +1124,7 @@ index cb94207..8188e7a 100644 } #[unsafe(no_mangle)] -@@ -836,6 +1012,21 @@ pub extern "C" fn rln_native_external_signer_bootstrap(signer: &COpaqueStruct) - +@@ -836,6 +1018,21 @@ pub extern "C" fn rln_native_external_signer_bootstrap(signer: &COpaqueStruct) - ) } @@ -1985,7 +1146,7 @@ index cb94207..8188e7a 100644 #[unsafe(no_mangle)] pub extern "C" fn rln_sdk_node_init_with_native_external_signer( node: &COpaqueStruct, -@@ -899,3 +1090,67 @@ pub extern "C" fn rln_sdk_node_unlock_with_attached_external_signer( +@@ -899,3 +1096,67 @@ pub extern "C" fn rln_sdk_node_unlock_with_attached_external_signer( api::sdk_node_unlock_with_attached_external_signer(node, request_json) ) } @@ -2090,7 +1251,7 @@ index 98c3372..c4db3c0 100644 #[cfg(test)] diff --git a/bindings/rgb_lightning_node.udl b/bindings/rgb_lightning_node.udl -index 219527a..b428476 100644 +index f54f150..fe7751a 100644 --- a/bindings/rgb_lightning_node.udl +++ b/bindings/rgb_lightning_node.udl @@ -5,6 +5,7 @@ namespace rgb_lightning_node { @@ -2121,7 +1282,7 @@ index 219527a..b428476 100644 [Throws=RlnError] void connectpeer(string peer_pubkey_and_addr); [Throws=RlnError] -@@ -652,6 +666,7 @@ dictionary SdkInitRequest { +@@ -654,6 +668,7 @@ dictionary SdkInitRequest { string? lsp_bearer_token; string? vss_url = null; boolean vss_allow_http = false; @@ -2130,10 +1291,10 @@ index 219527a..b428476 100644 boolean reuse_addresses = false; }; diff --git a/src/ldk.rs b/src/ldk.rs -index 2c582f4..0797b44 100644 +index 0b40cc2..599e10b 100644 --- a/src/ldk.rs +++ b/src/ldk.rs -@@ -108,18 +108,17 @@ use std::collections::HashSet; +@@ -109,18 +109,17 @@ use std::collections::HashSet; use std::convert::TryInto; use std::fs; use std::hash::{DefaultHasher, Hash, Hasher}; @@ -2154,7 +1315,7 @@ index 2c582f4..0797b44 100644 use tokio::runtime::Handle; use tokio::sync::watch::Sender; use tokio::task::JoinHandle; -@@ -183,8 +182,8 @@ use crate::signer::{ +@@ -184,8 +183,8 @@ use crate::signer::{ }; use crate::swap::SwapData; use crate::utils::{ @@ -2165,7 +1326,7 @@ index 2c582f4..0797b44 100644 validate_and_parse_payment_hash, validate_and_parse_payment_preimage, AppState, CommonState, LightningState, StaticState, UnlockedAppState, FATAL_ERROR, PROXY_ENDPOINT_LOCAL, }; -@@ -336,14 +335,290 @@ pub(crate) fn node_override_matches( +@@ -337,14 +336,290 @@ pub(crate) fn node_override_matches( .is_some_and(|id| *id == our_node_id) } @@ -2457,7 +1618,7 @@ index 2c582f4..0797b44 100644 #[derive(Clone, Debug)] pub(crate) struct PaymentInfo { pub(crate) preimage: Option, -@@ -1098,12 +1373,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")] @@ -2510,7 +1671,7 @@ index 2c582f4..0797b44 100644 } Event::SplicePending { .. } => { // We don't use the splice feature, so this event should never be seen. -@@ -4848,6 +5133,19 @@ impl NodeStartup { +@@ -4883,6 +5168,19 @@ impl NodeStartup { Some(mnemonic_str.clone()), ) }; @@ -2530,7 +1691,7 @@ index 2c582f4..0797b44 100644 let data_dir = static_state .storage_dir_path .clone() -@@ -4897,7 +5195,22 @@ impl NodeStartup { +@@ -4932,7 +5230,22 @@ impl NodeStartup { )), _ => None, }; @@ -2554,7 +1715,7 @@ index 2c582f4..0797b44 100644 // 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 || { let mut rgb_wallet = RgbLibWallet::new( -@@ -4987,16 +5300,14 @@ impl NodeStartup { +@@ -5022,16 +5335,14 @@ impl NodeStartup { &master_fingerprint, )?; @@ -2579,7 +1740,7 @@ index 2c582f4..0797b44 100644 #[cfg(feature = "vss")] if let Some((url, store_id, key)) = rgb_vss_backup { let wrapper = Arc::clone(&rgb_wallet_wrapper); -@@ -5005,15 +5316,20 @@ impl NodeStartup { +@@ -5040,15 +5351,20 @@ impl NodeStartup { .configure_vss_backup( crate::rgb_vss::VssBackupConfig::new(url, store_id, key) .with_encryption(true) @@ -2603,7 +1764,7 @@ index 2c582f4..0797b44 100644 wrapper .vss_backup(&client) .map_err(|error| APIError::FailedVssInit(error.to_string()))?; -@@ -5026,18 +5342,6 @@ impl NodeStartup { +@@ -5061,18 +5377,6 @@ impl NodeStartup { })??; } @@ -2622,7 +1783,7 @@ index 2c582f4..0797b44 100644 Ok(Arc::new(CommonState { config: Arc::clone(&static_state.config), signer, -@@ -5407,6 +5711,44 @@ async fn prepare_node( +@@ -5442,6 +5746,44 @@ async fn prepare_node( async fn start_lightning( #[allow(unused_mut)] mut setup: NodeStartup, @@ -2667,7 +1828,7 @@ index 2c582f4..0797b44 100644 ) -> Result<(Option, Arc), APIError> { // Defense in depth: no chain backend, manager or worker precedes this check. setup.app_state.check_lightning_supported()?; -@@ -5414,6 +5756,29 @@ async fn start_lightning( +@@ -5449,6 +5791,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(); @@ -2697,7 +1858,7 @@ index 2c582f4..0797b44 100644 let kv_store = Arc::clone(&setup.kv_store); let external_signer_mode = setup.external_signer_mode; let internal_mnemonic = setup.internal_mnemonic.clone(); -@@ -5430,6 +5795,19 @@ async fn start_lightning( +@@ -5465,6 +5830,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; @@ -2717,7 +1878,7 @@ index 2c582f4..0797b44 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()) -@@ -5471,6 +5849,9 @@ async fn start_lightning( +@@ -5506,6 +5884,9 @@ async fn start_lightning( handle.clone(), Arc::clone(&logger), static_state.config.chain.fee_refresh_interval_secs, @@ -2727,7 +1888,7 @@ index 2c582f4..0797b44 100644 ) .await { -@@ -5479,7 +5860,11 @@ async fn start_lightning( +@@ -5514,7 +5895,11 @@ async fn start_lightning( }; // Check that the bitcoind we've connected to is running the network we expect @@ -2740,7 +1901,7 @@ index 2c582f4..0797b44 100644 if bitcoind_chain != match bitcoin_network { BitcoinNetwork::Mainnet => "main", -@@ -5495,7 +5880,11 @@ async fn start_lightning( +@@ -5530,7 +5915,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 @@ -2753,7 +1914,7 @@ index 2c582f4..0797b44 100644 let initial_best_block = polled_chain_tip.to_best_block(); ChainSetup { -@@ -5528,16 +5917,14 @@ async fn start_lightning( +@@ -5563,16 +5952,14 @@ async fn start_lightning( Arc::clone(&logger), static_state.config.chain.indexer_timeout_secs, static_state.config.chain.fee_refresh_interval_secs, @@ -2774,7 +1935,7 @@ index 2c582f4..0797b44 100644 ); let initial_best_block = indexer_client .get_best_block() -@@ -5573,7 +5960,7 @@ async fn start_lightning( +@@ -5608,7 +5995,7 @@ async fn start_lightning( let (chain_monitor, mut channelmonitors): (Arc, _) = { let persister = MonitorUpdatingPersisterAsync::new( Arc::clone(&monitor_kv_store), @@ -2783,7 +1944,7 @@ index 2c582f4..0797b44 100644 Arc::clone(&logger), 1000, Arc::clone(&keys_manager), -@@ -5979,7 +6366,7 @@ async fn start_lightning( +@@ -6014,7 +6401,7 @@ async fn start_lightning( }); // The UTXO verifier can only attach to a P2P sync, and only after PeerManager @@ -2792,7 +1953,7 @@ index 2c582f4..0797b44 100644 let (p2p_gossip_sync_for_verifier, route_handler): ( Option>, Arc, -@@ -6094,61 +6481,110 @@ async fn start_lightning( +@@ -6129,61 +6516,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({ @@ -2932,7 +2093,7 @@ index 2c582f4..0797b44 100644 match backend { #[cfg(feature = "block-sync")] ChainBackend::BlockSync { client, .. } => { -@@ -6158,7 +6594,7 @@ async fn start_lightning( +@@ -6193,7 +6629,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; @@ -2941,7 +2102,7 @@ index 2c582f4..0797b44 100644 let chain_poller = poll::ChainPoller::new(client.as_ref(), network); let chain_listener = ( chain_monitor_listener, -@@ -6167,13 +6603,21 @@ async fn start_lightning( +@@ -6202,13 +6638,21 @@ async fn start_lightning( let mut spv_client = SpvClient::new(chain_tip, chain_poller, &mut cache, &chain_listener); loop { @@ -2967,7 +2128,7 @@ index 2c582f4..0797b44 100644 } }); } -@@ -6184,19 +6628,19 @@ async fn start_lightning( +@@ -6219,19 +6663,19 @@ async fn start_lightning( chain_monitor.clone(), output_sweeper.clone(), ]; @@ -2994,7 +2155,7 @@ index 2c582f4..0797b44 100644 } }); } -@@ -6397,6 +6841,7 @@ async fn start_lightning( +@@ -6432,6 +6876,7 @@ async fn start_lightning( let unlocked_state = Arc::new(LightningState { common: Arc::clone(&common), @@ -3002,7 +2163,7 @@ index 2c582f4..0797b44 100644 channel_manager: Arc::clone(&channel_manager), gossip_source: Arc::clone(&gossip_source), inbound_payments, -@@ -6424,15 +6869,19 @@ async fn start_lightning( +@@ -6459,15 +6904,19 @@ async fn start_lightning( asset_link_handler.set_authorizer(Arc::new(NodeAssetLinkAuthorizer)); @@ -3030,7 +2191,7 @@ index 2c582f4..0797b44 100644 } let recent_payments_payment_ids = channel_manager -@@ -6499,17 +6948,22 @@ async fn start_lightning( +@@ -6534,17 +6983,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(); @@ -3057,7 +2218,7 @@ index 2c582f4..0797b44 100644 return; } let db = RlnDatabase::new((*connect_db).clone()); -@@ -6521,14 +6975,18 @@ async fn start_lightning( +@@ -6556,14 +7010,18 @@ async fn start_lightning( .map(|chan| chan.counterparty.node_id) .filter(|id| connect_pm.peer_by_node_id(id).is_none()) { @@ -3080,7 +2241,7 @@ index 2c582f4..0797b44 100644 } } } -@@ -6560,16 +7018,16 @@ async fn start_lightning( +@@ -6595,16 +7053,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); @@ -3101,7 +2262,7 @@ index 2c582f4..0797b44 100644 return; } if link_event && link.is_connected() { -@@ -6591,10 +7049,12 @@ async fn start_lightning( +@@ -6626,10 +7084,12 @@ async fn start_lightning( interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { tokio::select! { @@ -3115,7 +2276,7 @@ index 2c582f4..0797b44 100644 return; } su_chain_monitor.signer_unblocked(None); -@@ -6607,45 +7067,17 @@ async fn start_lightning( +@@ -6642,45 +7102,17 @@ async fn start_lightning( }); } @@ -3167,7 +2328,7 @@ index 2c582f4..0797b44 100644 } sweep_handler.sweep_stale_state(); } -@@ -6655,15 +7087,24 @@ async fn start_lightning( +@@ -6690,15 +7122,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; @@ -3196,7 +2357,7 @@ index 2c582f4..0797b44 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. -@@ -6692,7 +7133,8 @@ async fn start_lightning( +@@ -6727,7 +7168,8 @@ async fn start_lightning( Ok(( Some(LdkBackgroundServices { stop_processing, @@ -3206,7 +2367,7 @@ index 2c582f4..0797b44 100644 peer_manager: peer_manager.clone(), bp_exit, background_processor: Some(background_processor), -@@ -6721,6 +7163,8 @@ pub(crate) fn attach_external_signer_transport( +@@ -6756,6 +7198,8 @@ pub(crate) fn attach_external_signer_transport( Ok(ExternalSignerAttachment { bootstrap, transport, @@ -3215,7 +2376,7 @@ index 2c582f4..0797b44 100644 }) } -@@ -6735,15 +7179,11 @@ impl AppState { +@@ -6770,15 +7214,11 @@ impl AppState { ldk_background_services .stop_processing .store(true, Ordering::Release); @@ -3234,7 +2395,7 @@ index 2c582f4..0797b44 100644 Some(ldk_background_services) } } -@@ -6864,13 +7304,19 @@ async fn release_vss_fence(kv_store: Arc, teardown: VssTeardown) +@@ -6899,13 +7339,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. @@ -3259,7 +2420,7 @@ index 2c582f4..0797b44 100644 } } -@@ -6936,7 +7382,16 @@ mod vss_teardown_tests { +@@ -6971,7 +7417,16 @@ mod vss_teardown_tests { } /// Stop the resources owned by the unlocked session, including wallet-only persistence. @@ -3277,7 +2438,7 @@ index 2c582f4..0797b44 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 -@@ -6958,19 +7413,33 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -6993,19 +7448,33 @@ pub(crate) async fn stop_node(app_state: Arc) { ) }); @@ -3313,7 +2474,7 @@ index 2c582f4..0797b44 100644 } } } -@@ -6979,9 +7448,13 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -7014,9 +7483,13 @@ pub(crate) async fn stop_node(app_state: Arc) { .as_mut() .and_then(|services| services.background_processor.take()) { @@ -3328,7 +2489,7 @@ index 2c582f4..0797b44 100644 #[cfg(feature = "vss")] if let Some((common, monitor_store)) = common_and_monitor { let deadline = Instant::now() + VSS_TEARDOWN_TIMEOUT; -@@ -6996,34 +7469,95 @@ pub(crate) async fn stop_node(app_state: Arc) { +@@ -7031,34 +7504,95 @@ pub(crate) async fn stop_node(app_state: Arc) { .is_err() { worker.abort(); @@ -3983,32 +3144,32 @@ index a635117..d126aa3 100644 }); } diff --git a/src/lib.rs b/src/lib.rs -index 235aab8..650780e 100644 +index 235aab8..993d74a 100644 --- a/src/lib.rs +++ b/src/lib.rs -@@ -77,6 +77,8 @@ pub mod test_utils; - mod uniffi_api; +@@ -78,6 +78,8 @@ mod uniffi_api; mod utils; #[cfg(feature = "vss")] -+mod vss_snapshot_policy; -+#[cfg(feature = "vss")] mod vss_kv_store; ++#[cfg(feature = "vss")] ++mod vss_snapshot_policy; pub use node::{NodeConfig, NodeHandle}; + diff --git a/src/main.rs b/src/main.rs -index 34fb4e1..96dcb12 100644 +index ba473e1..868436f 100644 --- a/src/main.rs +++ b/src/main.rs -@@ -49,6 +49,8 @@ mod swap; - mod synced_kv_store; +@@ -50,6 +50,8 @@ mod synced_kv_store; mod utils; #[cfg(feature = "vss")] -+mod vss_snapshot_policy; -+#[cfg(feature = "vss")] mod vss_kv_store; ++#[cfg(feature = "vss")] ++mod vss_snapshot_policy; // the test suite calls into `electrum_client` to wait for electrs to catch up with bitcoind, and -@@ -372,7 +374,9 @@ async fn shutdown_signal(app_state: Arc) { + // that crate is only pulled in by the `electrum` feature +@@ -373,7 +375,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; } @@ -4059,7 +3220,7 @@ index d6f571f..8b920d5 100644 ldk_background_services: Arc::new(Mutex::new(None)), attached_external_signer: Arc::new(Mutex::new(None)), diff --git a/src/mainnet_vss_tests.rs b/src/mainnet_vss_tests.rs -index f9c88b7..00e2789 100644 +index f9c88b7..95ac894 100644 --- a/src/mainnet_vss_tests.rs +++ b/src/mainnet_vss_tests.rs @@ -317,10 +317,38 @@ impl Wallet { @@ -4208,6 +3369,126 @@ index f9c88b7..00e2789 100644 assert!(!server.rows(&node_store).contains_key(FENCE)); assert!(common.persistence_worker.lock().unwrap().is_none()); } +@@ -766,3 +826,119 @@ async fn mainnet_vss_lock_waits_for_canceled_manual_backup_callers() { + ); + } + } ++ ++/// Exercise the real SDK policy switch against the actual encrypted protobuf client. ++/// Temporary suspension never turns a local content acknowledgement into cloud completion. ++#[tokio::test(flavor = "multi_thread", worker_threads = 4)] ++async fn automatic_snapshot_policy_pauses_and_acknowledges_only_confirmed_cloud_commit() { ++ let server = Server::new().await; ++ let wallet = Wallet::new(&server).await; ++ sdk::unlock(wallet.state.clone(), wallet.request()) ++ .await ++ .unwrap(); ++ let wrapper = wallet ++ .state ++ .unlocked_app_state ++ .lock() ++ .await ++ .as_ref() ++ .unwrap() ++ .rgb_wallet_wrapper ++ .clone(); ++ let policy = wrapper.snapshot_policy.as_ref().unwrap().clone(); ++ let rgb_store = format!("{}_rgb", store_id()); ++ sdk::address(wallet.state.clone()).await.unwrap(); ++ let initial: Value = serde_json::from_str(&policy.metadata().unwrap()).unwrap(); ++ assert_eq!(initial["automatic_backup_enabled"], true); ++ assert!(initial["last_backup"]["completed_at_ms"].as_u64().unwrap() > 0); ++ assert!(!policy.pending()); ++ let first = server.rows(&rgb_store)["backup/manifest"].version; ++ sdk::set_vss_automatic_backup_enabled(wallet.state.clone(), false) ++ .await ++ .unwrap(); ++ sdk::rotate_address(wallet.state.clone()).await.unwrap(); ++ assert!(policy.pending()); ++ assert!(!policy.enabled.load(std::sync::atomic::Ordering::SeqCst)); ++ assert_eq!(server.rows(&rgb_store)["backup/manifest"].version, first); ++ let paused: Value = serde_json::from_str(&policy.metadata().unwrap()).unwrap(); ++ assert_eq!(paused["last_backup"], initial["last_backup"]); ++ let gate = Arc::new(BackupGate { ++ store: rgb_store.clone(), ++ entered: Semaphore::new(0), ++ resume: Semaphore::new(0), ++ }); ++ server.state.lock().unwrap().backup_gate = Some(gate.clone()); ++ let state = wallet.state.clone(); ++ let enabling = ++ tokio::spawn(async move { sdk::set_vss_automatic_backup_enabled(state, true).await }); ++ tokio::time::timeout(std::time::Duration::from_secs(10), gate.entered.acquire()) ++ .await ++ .unwrap() ++ .unwrap() ++ .forget(); ++ assert!( ++ policy.pending(), ++ "inflight cloud write cleared pending before confirmation" ++ ); ++ assert!( ++ !policy.enabled.load(std::sync::atomic::Ordering::SeqCst), ++ "switch acknowledged before catch-up commit" ++ ); ++ assert_eq!( ++ serde_json::from_str::(&policy.metadata().unwrap()).unwrap()["last_backup"], ++ initial["last_backup"] ++ ); ++ // A canceled UI waiter cannot cancel the admitted policy catch-up or reopen its writer. ++ enabling.abort(); ++ assert!(enabling.await.unwrap_err().is_cancelled()); ++ gate.resume.add_permits(1); ++ let until = tokio::time::Instant::now() + std::time::Duration::from_secs(10); ++ while !policy.enabled.load(std::sync::atomic::Ordering::SeqCst) { ++ assert!( ++ tokio::time::Instant::now() < until, ++ "policy catch-up did not complete" ++ ); ++ tokio::time::sleep(std::time::Duration::from_millis(10)).await; ++ } ++ assert!(!policy.pending()); ++ assert!(!policy.failed.load(std::sync::atomic::Ordering::SeqCst)); ++ assert_eq!( ++ server.rows(&rgb_store)["backup/manifest"].version, ++ first + 1 ++ ); ++ let acknowledged: Value = serde_json::from_str(&policy.metadata().unwrap()).unwrap(); ++ assert!( ++ acknowledged["last_backup"]["completed_at_ms"] ++ .as_u64() ++ .unwrap() ++ >= initial["last_backup"]["completed_at_ms"].as_u64().unwrap() ++ ); ++ sdk::rotate_address(wallet.state.clone()).await.unwrap(); ++ assert!(!policy.pending()); ++ assert_eq!( ++ server.rows(&rgb_store)["backup/manifest"].version, ++ first + 2 ++ ); ++ sdk::set_vss_automatic_backup_enabled(wallet.state.clone(), false) ++ .await ++ .unwrap(); ++ wallet.lock().await; ++ sdk::unlock(wallet.state.clone(), wallet.request()) ++ .await ++ .unwrap(); ++ let reopened: Value = serde_json::from_str( ++ &sdk::vss_backup_metadata(wallet.state.clone()) ++ .await ++ .unwrap(), ++ ) ++ .unwrap(); ++ assert_eq!(reopened["automatic_backup_enabled"], false); ++ wallet.lock().await; ++ assert!(server ++ .state ++ .lock() ++ .unwrap() ++ .requests ++ .iter() ++ .all(|r| r.authenticated)); ++} diff --git a/src/node.rs b/src/node.rs index d504a55..9294786 100644 --- a/src/node.rs @@ -4332,7 +3613,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 14d98b1..67a40f9 100644 +index 84c070c..00f71e2 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -30,7 +30,7 @@ use rgb_lib::{ @@ -4344,7 +3625,7 @@ index 14d98b1..67a40f9 100644 use crate::{error::APIError, utils::CommonState}; -@@ -285,7 +285,7 @@ impl CommonState { +@@ -261,7 +261,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; @@ -4353,7 +3634,7 @@ index 14d98b1..67a40f9 100644 let prepared = wallet.burn_begin( wrapper.online, asset_id, -@@ -658,74 +658,229 @@ fn validate_burn_signed_psbt(unsigned: &str, signed: &str) -> Result Result Option> { self.vss_client.lock().unwrap().clone() -@@ -784,8 +927,15 @@ impl RgbLibWalletWrapper { +@@ -760,8 +903,15 @@ impl RgbLibWalletWrapper { &self, client: &crate::rgb_vss::VssBackupClient, ) -> Result { @@ -4657,7 +3938,7 @@ index 14d98b1..67a40f9 100644 } #[cfg(feature = "vss")] -@@ -793,7 +943,7 @@ impl RgbLibWalletWrapper { +@@ -769,7 +919,7 @@ impl RgbLibWalletWrapper { &self, client: &crate::rgb_vss::VssBackupClient, ) -> Result { @@ -4666,7 +3947,7 @@ index 14d98b1..67a40f9 100644 let server_version = std::thread::scope(|scope| { scope .spawn(|| client.handle().block_on(client.get_backup_version())) -@@ -813,7 +963,8 @@ impl RgbLibWalletWrapper { +@@ -789,7 +939,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> { @@ -4676,7 +3957,7 @@ index 14d98b1..67a40f9 100644 } pub(crate) fn bitcoin_network(&self) -> BitcoinNetwork { -@@ -828,7 +979,7 @@ impl RgbLibWalletWrapper { +@@ -804,7 +955,7 @@ impl RgbLibWalletWrapper { transport_endpoints: Vec, min_confirmations: u8, ) -> Result { @@ -4685,17 +3966,57 @@ index 14d98b1..67a40f9 100644 Some(self.online), asset_id, assignment, -@@ -844,7 +995,8 @@ impl RgbLibWalletWrapper { +@@ -820,7 +971,8 @@ impl RgbLibWalletWrapper { fascia: Fascia, witness_ord: Option, ) -> Result<(), RgbLibError> { -- self.get_rgb_wallet().consume_fascia(fascia, witness_ord) +- self.get_rgb_wallet().consume_fascia(fascia, witness_ord) ++ self.get_rgb_wallet_mutating() ++ .consume_fascia(fascia, witness_ord) + } + + pub(crate) fn color_psbt_and_prepare_consume( +@@ -828,12 +980,9 @@ impl RgbLibWalletWrapper { + psbt_to_color: &mut BitcoinPsbt, + coloring_info: ColoringInfo, + ) -> Result { +- let prepared = self.get_rgb_wallet().color_psbt_and_prepare_consume( +- psbt_to_color, +- coloring_info, +- 1, +- None, +- )?; ++ let prepared = self ++ .get_rgb_wallet_mutating() ++ .color_psbt_and_prepare_consume(psbt_to_color, coloring_info, 1, None)?; + Ok(ColorPrepareResult { + batch_transfer_idx: prepared.batch_transfer_idx, + transfers: prepared.transfers, +@@ -841,7 +990,7 @@ impl RgbLibWalletWrapper { + } + + pub(crate) fn cancel_prepared_sweep(&self, batch_transfer_idx: i32) -> Result<(), RgbLibError> { +- self.get_rgb_wallet() ++ self.get_rgb_wallet_mutating() + .cancel_prepared_sweep(batch_transfer_idx) + } + +@@ -850,7 +999,7 @@ impl RgbLibWalletWrapper { + batch_transfer_idx: i32, + expected_txid: String, + ) -> Result<(), RgbLibError> { +- self.get_rgb_wallet() + self.get_rgb_wallet_mutating() -+ .consume_fascia(fascia, witness_ord) + .mark_prepared_sweep_handed_off(batch_transfer_idx, expected_txid) } - #[allow(unused_variables)] -@@ -869,7 +1021,7 @@ impl RgbLibWalletWrapper { +@@ -884,12 +1033,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) } pub(crate) fn create_consigments(&self, psbt: String) -> Result<(), RgbLibError> { @@ -4703,26 +4024,8 @@ index 14d98b1..67a40f9 100644 + self.get_rgb_wallet_mutating().create_consignments(psbt) } - pub(crate) fn create_utxos( -@@ -880,7 +1032,7 @@ impl RgbLibWalletWrapper { - fee_rate: u64, - skip_sync: bool, - ) -> Result { -- self.get_rgb_wallet().create_utxos( -+ self.get_rgb_wallet_mutating().create_utxos( - self.online, - up_to, - Some(num), -@@ -898,7 +1050,7 @@ impl RgbLibWalletWrapper { - fee_rate: u64, - skip_sync: bool, - ) -> Result { -- self.get_rgb_wallet().create_utxos_begin( -+ self.get_rgb_wallet_mutating().create_utxos_begin( - self.online, - up_to, - Some(num), -@@ -914,7 +1066,7 @@ impl RgbLibWalletWrapper { + pub(crate) fn create_utxos_end( +@@ -897,7 +1046,7 @@ impl RgbLibWalletWrapper { signed_psbt: String, _skip_sync: bool, ) -> Result { @@ -4731,7 +4034,7 @@ index 14d98b1..67a40f9 100644 .create_utxos_end(self.online, signed_psbt) } -@@ -924,7 +1076,7 @@ impl RgbLibWalletWrapper { +@@ -907,7 +1056,7 @@ impl RgbLibWalletWrapper { no_asset_only: bool, skip_sync: bool, ) -> Result { @@ -4740,7 +4043,7 @@ index 14d98b1..67a40f9 100644 self.online, batch_transfer_idx, no_asset_only, -@@ -933,11 +1085,11 @@ impl RgbLibWalletWrapper { +@@ -916,11 +1065,11 @@ impl RgbLibWalletWrapper { } pub(crate) fn get_address(&self) -> Result { @@ -4754,7 +4057,7 @@ index 14d98b1..67a40f9 100644 } pub(crate) fn get_asset_balance( -@@ -958,7 +1110,8 @@ impl RgbLibWalletWrapper { +@@ -941,7 +1090,8 @@ impl RgbLibWalletWrapper { pub(crate) fn get_btc_balance(&self, skip_sync: bool) -> Result { let online = if skip_sync { None } else { Some(self.online) }; @@ -4764,7 +4067,7 @@ index 14d98b1..67a40f9 100644 } pub(crate) fn get_fee_estimation(&self, blocks: u16) -> Result { -@@ -991,7 +1144,7 @@ impl RgbLibWalletWrapper { +@@ -974,7 +1124,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, min_confirmations: u8, ) -> Result { @@ -4773,7 +4076,7 @@ index 14d98b1..67a40f9 100644 self.online, asset_id, amount, -@@ -1008,7 +1161,7 @@ impl RgbLibWalletWrapper { +@@ -991,7 +1141,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, min_confirmations: u8, ) -> Result { @@ -4782,7 +4085,7 @@ index 14d98b1..67a40f9 100644 self.online, asset_id, inflation_amounts, -@@ -1025,7 +1178,7 @@ impl RgbLibWalletWrapper { +@@ -1008,7 +1158,7 @@ impl RgbLibWalletWrapper { amounts: Vec, file_path: Option, ) -> Result { @@ -4791,7 +4094,7 @@ index 14d98b1..67a40f9 100644 .issue_asset_cfa(name, details, precision, amounts, file_path) } -@@ -1045,7 +1198,7 @@ impl RgbLibWalletWrapper { +@@ -1028,7 +1178,7 @@ impl RgbLibWalletWrapper { details: "IFA link rights are unavailable in the BFA RGB library".into(), }); } @@ -4800,7 +4103,7 @@ index 14d98b1..67a40f9 100644 ticker, name, precision, -@@ -1062,7 +1215,7 @@ impl RgbLibWalletWrapper { +@@ -1045,7 +1195,7 @@ impl RgbLibWalletWrapper { precision: u8, amounts: Vec, ) -> Result { @@ -4809,7 +4112,7 @@ index 14d98b1..67a40f9 100644 .issue_asset_nia(ticker, name, precision, amounts) } -@@ -1075,7 +1228,7 @@ impl RgbLibWalletWrapper { +@@ -1058,7 +1208,7 @@ impl RgbLibWalletWrapper { media_file_path: Option, attachments_file_paths: Vec, ) -> Result { @@ -4818,7 +4121,7 @@ index 14d98b1..67a40f9 100644 ticker, name, details, -@@ -1101,7 +1254,8 @@ impl RgbLibWalletWrapper { +@@ -1084,7 +1234,8 @@ impl RgbLibWalletWrapper { skip_sync: bool, ) -> Result, RgbLibError> { let online = if skip_sync { None } else { Some(self.online) }; @@ -4828,7 +4131,7 @@ index 14d98b1..67a40f9 100644 } pub(crate) fn list_transfers( -@@ -1118,7 +1272,7 @@ impl RgbLibWalletWrapper { +@@ -1101,7 +1252,7 @@ impl RgbLibWalletWrapper { skip_sync: bool, ) -> Result, RgbLibError> { let online = if skip_sync { None } else { Some(self.online) }; @@ -4837,7 +4140,7 @@ index 14d98b1..67a40f9 100644 .list_unspents(online, settled_only, skip_sync) } -@@ -1129,7 +1283,7 @@ impl RgbLibWalletWrapper { +@@ -1112,7 +1263,7 @@ impl RgbLibWalletWrapper { vout: u32, blinding: u64, ) -> Result<(RgbTransfer, Vec, HashSet), RgbLibError> { @@ -4846,7 +4149,7 @@ index 14d98b1..67a40f9 100644 self.online, consignment_path, txid, -@@ -1142,7 +1296,7 @@ impl RgbLibWalletWrapper { +@@ -1125,7 +1276,7 @@ impl RgbLibWalletWrapper { &self, recipient_id: String, ) -> Result, RgbLibError> { @@ -4855,7 +4158,7 @@ index 14d98b1..67a40f9 100644 .provide_out_of_band_ack(self.online, recipient_id) } -@@ -1151,11 +1305,8 @@ impl RgbLibWalletWrapper { +@@ -1134,11 +1285,8 @@ impl RgbLibWalletWrapper { consignment_path: String, media_file_paths: Vec, ) -> Result, RgbLibError> { @@ -4869,7 +4172,7 @@ index 14d98b1..67a40f9 100644 } pub(crate) fn refresh( -@@ -1164,7 +1315,10 @@ impl RgbLibWalletWrapper { +@@ -1147,7 +1295,10 @@ impl RgbLibWalletWrapper { filter: Vec, skip_sync: bool, ) -> Result { @@ -4881,7 +4184,7 @@ index 14d98b1..67a40f9 100644 .refresh(self.online, asset_id, filter, skip_sync) } -@@ -1173,7 +1327,7 @@ impl RgbLibWalletWrapper { +@@ -1156,7 +1307,7 @@ impl RgbLibWalletWrapper { consignment: RgbTransfer, offchain_txid: String, ) -> Result<(), RgbLibError> { @@ -4890,7 +4193,7 @@ index 14d98b1..67a40f9 100644 .save_new_asset(self.online, consignment, offchain_txid) } -@@ -1186,7 +1340,7 @@ impl RgbLibWalletWrapper { +@@ -1169,7 +1320,7 @@ impl RgbLibWalletWrapper { expiration_timestamp: u64, sender_endpoint: Option, ) -> Result { @@ -4899,16 +4202,25 @@ index 14d98b1..67a40f9 100644 self.online, recipient_map, donation, -@@ -1214,7 +1368,7 @@ impl RgbLibWalletWrapper { - details: "Custom RGB funding locktime is unavailable in the BFA RGB library".into(), - }); +@@ -1194,7 +1345,7 @@ impl RgbLibWalletWrapper { + ) -> Result { + match lock_time { + Some(0) => { +- return self.get_rgb_wallet().send_channel_funding_begin( ++ return self.get_rgb_wallet_mutating().send_channel_funding_begin( + self.online, + recipient_map, + donation, +@@ -1212,7 +1363,7 @@ impl RgbLibWalletWrapper { + } + None => {} } - self.get_rgb_wallet().send_begin( + self.get_rgb_wallet_mutating().send_begin( self.online, recipient_map, donation, -@@ -1233,7 +1387,7 @@ impl RgbLibWalletWrapper { +@@ -1231,7 +1382,7 @@ impl RgbLibWalletWrapper { fee_rate: u64, skip_sync: bool, ) -> Result { @@ -4917,7 +4229,7 @@ index 14d98b1..67a40f9 100644 .send_btc(self.online, address, amount, fee_rate, skip_sync) } -@@ -1244,8 +1398,14 @@ impl RgbLibWalletWrapper { +@@ -1242,8 +1393,14 @@ impl RgbLibWalletWrapper { fee_rate: u64, dry_run: bool, ) -> Result { @@ -4934,7 +4246,20 @@ index 14d98b1..67a40f9 100644 } pub(crate) fn send_btc_channel_funding_begin( -@@ -1264,18 +1424,20 @@ impl RgbLibWalletWrapper { +@@ -1252,29 +1409,25 @@ impl RgbLibWalletWrapper { + amount: u64, + fee_rate: u64, + ) -> Result { +- self.get_rgb_wallet().send_btc_channel_funding_begin( +- self.online, +- address, +- amount, +- fee_rate, +- false, +- false, +- ) ++ self.get_rgb_wallet_mutating() ++ .send_btc_channel_funding_begin(self.online, address, amount, fee_rate, false, false) } pub(crate) fn send_btc_end(&self, signed_psbt: String) -> Result { @@ -4958,7 +4283,7 @@ index 14d98b1..67a40f9 100644 .send_end_db_update_only(self.online, signed_psbt) } -@@ -1284,7 +1446,7 @@ impl RgbLibWalletWrapper { +@@ -1283,7 +1436,7 @@ impl RgbLibWalletWrapper { } pub(crate) fn sync(&self, options: SyncOptions) -> Result<(), RgbLibError> { @@ -4967,7 +4292,7 @@ index 14d98b1..67a40f9 100644 } pub(crate) fn update_witnesses( -@@ -1292,7 +1454,7 @@ impl RgbLibWalletWrapper { +@@ -1291,7 +1444,7 @@ impl RgbLibWalletWrapper { after_height: u32, force_witnesses: Vec, ) -> Result { @@ -4976,7 +4301,7 @@ index 14d98b1..67a40f9 100644 .update_witnesses(self.online, after_height, force_witnesses) } -@@ -1301,7 +1463,7 @@ impl RgbLibWalletWrapper { +@@ -1300,7 +1453,7 @@ impl RgbLibWalletWrapper { witness_id: RgbTxid, witness_ord: WitnessOrd, ) -> Result<(), RgbLibError> { @@ -4985,7 +4310,7 @@ index 14d98b1..67a40f9 100644 .upsert_witness(witness_id, witness_ord) } -@@ -1313,7 +1475,7 @@ impl RgbLibWalletWrapper { +@@ -1312,7 +1465,7 @@ impl RgbLibWalletWrapper { transport_endpoints: Vec, min_confirmations: u8, ) -> Result { @@ -4994,7 +4319,7 @@ index 14d98b1..67a40f9 100644 Some(self.online), asset_id, assignment, -@@ -1416,7 +1578,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1415,7 +1568,7 @@ impl WalletSource for RgbLibWalletWrapper { Box::pin(async move { let network = Network::from(self.bitcoin_network()); let unspents = self @@ -5003,7 +4328,7 @@ index 14d98b1..67a40f9 100644 .list_unspents_vanilla(self.online, 1, false) .map_err(|e| { tracing::error!(error = %e, "failed to list unspents for fee bumping"); -@@ -1459,7 +1621,7 @@ impl WalletSource for RgbLibWalletWrapper { +@@ -1458,7 +1611,7 @@ impl WalletSource for RgbLibWalletWrapper { fn get_change_script<'a>(&'a self) -> AsyncResult<'a, ScriptBuf, ()> { Box::pin(async move { @@ -5012,7 +4337,30 @@ index 14d98b1..67a40f9 100644 tracing::error!(error = %e, "failed to get change address for fee bumping"); })?; let parsed = Address::from_str(&address).map_err(|e| { -@@ -1728,6 +1890,489 @@ mod wallet_source_tests { +@@ -1704,15 +1857,19 @@ mod wallet_source_tests { + } + + fn signetcustom_wallet_source() -> RgbLibWalletWrapper { ++ wallet_source_for_network(BitcoinNetwork::SignetCustom) ++ } ++ ++ fn wallet_source_for_network(network: BitcoinNetwork) -> RgbLibWalletWrapper { + let tmp = tempfile::tempdir().expect("tempdir"); +- let keys = generate_keys(BitcoinNetwork::SignetCustom, WitnessVersion::Taproot); ++ let keys = generate_keys(network, WitnessVersion::Taproot); + let wallet = RgbLibWallet::new( + WalletData { + data_dir: tmp.keep().display().to_string(), +- bitcoin_network: BitcoinNetwork::SignetCustom, ++ bitcoin_network: network, + database_type: DatabaseType::Sqlite, + max_allocations_per_utxo: 1, +- supported_schemas: vec![AssetSchema::Nia], ++ supported_schemas: vec![AssetSchema::Nia, AssetSchema::Bfa], + }, + SinglesigKeys { + account_xpub_vanilla: keys.account_xpub_vanilla, +@@ -1727,6 +1884,1068 @@ mod wallet_source_tests { RgbLibWalletWrapper::new(Arc::new(Mutex::new(wallet)), Online { id: 0 }) } @@ -5138,7 +4486,7 @@ index 14d98b1..67a40f9 100644 + } + + #[test] -+ fn ordinary_bitcoin_prepare_is_distinct_from_unsupported_channel_funding() { ++ fn ordinary_and_fixed_zero_bitcoin_funding_reach_native_online_admission() { + let wrapper = signetcustom_wallet_source(); + // This fresh offline fixture reaches the actual RGB wallet's online check. + assert!(matches!( @@ -5147,7 +4495,7 @@ index 14d98b1..67a40f9 100644 + )); + assert!(matches!( + wrapper.send_btc_channel_funding_begin("unused".into(), 1000, 1), -+ Err(RgbLibError::Internal { details }) if details.contains("channel funding locktime") ++ Err(RgbLibError::Offline) + )); + } + @@ -5407,6 +4755,581 @@ index 14d98b1..67a40f9 100644 + + #[cfg(feature = "vss")] + #[test] ++ fn final_graph_mutation_branches_durably_admit_paused_and_enabled_backup() { ++ for enabled in [false, true] { ++ for operation in [ ++ "rgb_funding", ++ "btc_funding", ++ "prepare", ++ "cancel", ++ "handoff", ++ "consume", ++ ] { ++ 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(), ++ enabled, ++ ) ++ .unwrap(), ++ ); ++ let mut wrapper = signetcustom_wallet_source(); ++ wrapper.snapshot_policy = Some(policy.clone()); ++ completed_snapshot_fixture(&wrapper, tmp.path()); ++ let before = policy.metadata().unwrap(); ++ assert!(!policy.pending()); ++ let failed = match operation { ++ // Some(0) must enter the real builder, independently of the ordinary None branch. ++ "rgb_funding" => wrapper ++ .send_begin(HashMap::new(), false, 1, 1, 0, false, Some(0), None) ++ .is_err(), ++ "btc_funding" => wrapper ++ .send_btc_channel_funding_begin("unused".into(), 1000, 1) ++ .is_err(), ++ "prepare" => { ++ let tx = bitcoin::Transaction { ++ version: bitcoin::transaction::Version::TWO, ++ lock_time: bitcoin::absolute::LockTime::ZERO, ++ input: vec![], ++ output: vec![], ++ }; ++ let mut psbt = BitcoinPsbt::from_unsigned_tx(tx).unwrap(); ++ wrapper ++ .color_psbt_and_prepare_consume( ++ &mut psbt, ++ ColoringInfo { ++ asset_info_map: HashMap::new(), ++ static_blinding: None, ++ nonce: None, ++ }, ++ ) ++ .is_err() ++ } ++ "cancel" => wrapper.cancel_prepared_sweep(i32::MAX).is_err(), ++ "handoff" => wrapper ++ .mark_prepared_sweep_handed_off(i32::MAX, "00".repeat(32)) ++ .is_err(), ++ "consume" => wrapper.consume_transfer_fascia(i32::MAX).is_err(), ++ _ => unreachable!(), ++ }; ++ assert!( ++ failed, ++ "{operation}: invalid offline fixture unexpectedly succeeded" ++ ); ++ assert!(policy.pending(), "{operation}: mutation admission missing"); ++ assert_eq!( ++ policy.failed.load(std::sync::atomic::Ordering::SeqCst), ++ enabled, ++ "only enabled automatic backup should attempt the absent client" ++ ); ++ assert_eq!( ++ policy.metadata().unwrap(), ++ before, ++ "failed/unattempted upload changed completion" ++ ); ++ let reopened = crate::vss_snapshot_policy::SnapshotPolicy::load( ++ tmp.path(), ++ BitcoinNetwork::SignetCustom, ++ "https://example.invalid/vss".into(), ++ "test-node".into(), ++ !enabled, ++ ) ++ .unwrap(); ++ assert!(reopened.pending(), "{operation}: admission was not durable"); ++ assert_eq!( ++ reopened.enabled.load(std::sync::atomic::Ordering::SeqCst), ++ enabled ++ ); ++ } ++ } ++ } ++ ++ #[cfg(feature = "vss")] ++ #[test] ++ fn rejected_locktime_and_prepared_recovery_never_admit_mutation() { ++ 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()); ++ for locktime in [1, u32::MAX] { ++ assert!( ++ matches!(wrapper.send_begin(HashMap::new(), false, 1, 1, 0, false, Some(locktime), None), ++ Err(RgbLibError::Internal { details }) if details.contains("final-zero-locktime")) ++ ); ++ assert!(!policy.pending()); ++ assert!(!policy.failed.load(std::sync::atomic::Ordering::SeqCst)); ++ } ++ assert!(wrapper ++ .recover_prepared_sweep(&HashSet::new()) ++ .unwrap() ++ .is_none()); ++ assert!(!policy.pending()); ++ assert!(!policy.failed.load(std::sync::atomic::Ordering::SeqCst)); ++ } ++ ++ #[cfg(feature = "vss")] ++ #[test] ++ fn public_dev_bfa_contract_import_retains_pending_until_confirmed_cloud_commit() { ++ use base64::Engine; ++ const CONTRACT: &str = "UkdCAENPTgEAAAAAAEcpeXkGdyB61NOXtwFnUGSUeBQeRr8WT8venMISYgKX71PSagAAAAANAHNzaTphbm9ueW1vdXMDAAAE0AcBABUABUJVU0RUDEJyaWRnZWQgVVNEVAAG0QcBAAMAAAAAwAsBAB0AALGkAAAAAAAAvv2h8A1USz/KeLiVhUix/AKmSjbCCwEAHQABsaQAAAAAAACLDCXD5DLIx/5v0GUtFEC14W1WAwEArQ8ACgAAJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YAAAAAzOVnzSjGG2IAJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YBAAAAtlb1Tqs0b8gAJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YDAAAAN/jlgQ0tV2oAJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YFAAAAKOWYjX6TiA0AJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YGAAAAVJHxgbimjzMAJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YIAAAAZXC2LjNLRekAJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YJAAAAkkcrglMERl4AJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YKAAAAZuR27o7HDs0AJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YLAAAA8ysnr6oXWnEAJl57BwYGZ0aMOJBIO1Vd9fByD/NuhzI25XndZZ5Wb7YMAAAAK7dVveLPBloAAAAA"; ++ const ASSET: &str = "rgb:OG9No4hZ-UrxoWmR-09ohtOk-ppxBN7h-g~KkQro-ncKc9A4"; ++ let bytes = base64::engine::general_purpose::STANDARD ++ .decode(CONTRACT) ++ .unwrap(); ++ assert_eq!(bytes.len(), 639); ++ assert_eq!( ++ bitcoin::hashes::sha256::Hash::hash(&bytes).to_string(), ++ "dfb49261fed5ad3dd237dd3a03913793b76fe70e486372bbeb0769b82158a895" ++ ); ++ // The transport alias does not change the contract's native chain identity. ++ let wrong_network = signetcustom_wallet_source(); ++ let negative = tempfile::tempdir().unwrap(); ++ let wrong_contract = negative.path().join("dev-usdt.con"); ++ std::fs::write(&wrong_contract, &bytes).unwrap(); ++ assert!(wrong_network ++ .get_rgb_wallet_mutating() ++ .import_contract(wrong_contract.display().to_string(), Some(ASSET.into())) ++ .is_err()); ++ for enabled in [false, true] { ++ let tmp = tempfile::tempdir().unwrap(); ++ let policy = Arc::new( ++ crate::vss_snapshot_policy::SnapshotPolicy::load( ++ tmp.path(), ++ BitcoinNetwork::Signet, ++ "https://example.invalid/vss".into(), ++ "test-node".into(), ++ enabled, ++ ) ++ .unwrap(), ++ ); ++ let mut wrapper = wallet_source_for_network(BitcoinNetwork::Signet); ++ wrapper.snapshot_policy = Some(policy.clone()); ++ let contract = tmp.path().join("dev-usdt.con"); ++ std::fs::write(&contract, &bytes).unwrap(); ++ { ++ let mut guard = wrapper.get_rgb_wallet_mutating(); ++ let id = guard ++ .import_contract(contract.display().to_string(), Some(ASSET.into())) ++ .unwrap(); ++ assert_eq!(id, ASSET); ++ let metadata = guard.get_asset_metadata(id.clone()).unwrap(); ++ assert_eq!(metadata.name, "Bridged USDT"); ++ assert_eq!(metadata.ticker.as_deref(), Some("BUSDT")); ++ assert_eq!(metadata.precision, 6); ++ assert_eq!(guard.get_asset_balance(id).unwrap().settled, 0); ++ } ++ assert!(policy.pending()); ++ assert_eq!( ++ policy.failed.load(std::sync::atomic::Ordering::SeqCst), ++ enabled ++ ); ++ let snapshot = wrapper ++ .get_rgb_wallet_for_snapshot() ++ .prepare_snapshot() ++ .unwrap(); ++ assert!(wrapper ++ .get_rgb_wallet_for_snapshot() ++ .acknowledge_snapshot(&snapshot.token) ++ .unwrap()); ++ assert!( ++ policy.pending(), ++ "local acknowledgement cannot assert a cloud upload" ++ ); ++ let metadata: serde_json::Value = ++ serde_json::from_str(&policy.metadata().unwrap()).unwrap(); ++ assert!(metadata["last_backup"].is_null()); ++ } ++ } ++ /// Uses only the explicitly owned fixture, never the shared Iris stack or hosted services. ++ /// An absent cloud client intentionally proves upload failure cannot roll back financial work. ++ #[cfg(feature = "vss")] ++ #[test] ++ #[ignore = "requires explicitly admitted isolated regtest fixture"] ++ fn final_graph_funded_mutations_preserve_snapshot_admission_and_exact_sweeps() { ++ use rgb_lib::wallet::{OnlineOptions, SyncKeychain, SyncStrategy, WitnessData}; ++ use rgb_lib::{no_wallet::IndexerConfig, Encryption, FileContent}; ++ use serde_json::{json, Value}; ++ use std::{ ++ process::Command, ++ time::{Duration, Instant}, ++ }; ++ const PROJECT: &str = "iris_rgb_output_20261026"; ++ const CONTAINER: &str = "iris_rgb_output_20261026-bitcoind-1"; ++ assert_eq!(std::env::var("COMPOSE_PROJECT_NAME").unwrap(), PROJECT); ++ let compose = PathBuf::from(env!("CARGO_MANIFEST_DIR")) ++ .parent() ++ .unwrap() ++ .join("qualification-consumers/rgb-output-native-fixture-20261026/compose.yaml") ++ .canonicalize() ++ .unwrap(); ++ assert_eq!( ++ PathBuf::from(std::env::var("COMPOSE_FILE").unwrap()) ++ .canonicalize() ++ .unwrap(), ++ compose ++ ); ++ let rpc = |wallet: bool, method: &str, params: Vec| -> Value { ++ let inspected = Command::new("docker") ++ .args(["inspect", CONTAINER, "--format", "{{json .Config.Labels}}"]) ++ .output() ++ .unwrap(); ++ assert!(inspected.status.success()); ++ let labels: Value = serde_json::from_slice(&inspected.stdout).unwrap(); ++ assert_eq!(labels["com.docker.compose.project"], PROJECT); ++ assert_eq!( ++ PathBuf::from( ++ labels["com.docker.compose.project.config_files"] ++ .as_str() ++ .unwrap() ++ ) ++ .canonicalize() ++ .unwrap(), ++ compose ++ ); ++ let mut args = vec![ ++ "exec".to_owned(), ++ CONTAINER.into(), ++ "bitcoin-cli".into(), ++ "-regtest".into(), ++ "-rpcuser=user".into(), ++ "-rpcpassword=password".into(), ++ ]; ++ if wallet { ++ args.push("-rpcwallet=iris-output-tests".into()); ++ } ++ args.push(method.into()); ++ args.extend(params.into_iter().map(|p| { ++ p.as_str() ++ .map(str::to_owned) ++ .unwrap_or_else(|| p.to_string()) ++ })); ++ let result = Command::new("docker").args(&args).output().unwrap(); ++ assert!(result.status.success(), "owned fixture RPC {method} failed"); ++ serde_json::from_slice(&result.stdout).unwrap_or_else(|_| { ++ Value::String(String::from_utf8(result.stdout).unwrap().trim().into()) ++ }) ++ }; ++ assert_eq!(rpc(false, "getblockchaininfo", vec![])["chain"], "regtest"); ++ assert_eq!(rpc(false, "getmempoolinfo", vec![])["size"], 0); ++ let mining = rpc(true, "getnewaddress", vec![]); ++ let mine = || { ++ rpc(false, "generatetoaddress", vec![json!(1), mining.clone()]); ++ let target = rpc(false, "getblockcount", vec![]).as_u64().unwrap(); ++ let until = Instant::now() + Duration::from_secs(30); ++ loop { ++ let result = Command::new("curl") ++ .args([ ++ "--fail", ++ "--silent", ++ "--max-time", ++ "2", ++ "http://127.0.0.1:3002/blocks/tip/height", ++ ]) ++ .output() ++ .unwrap(); ++ let seen = result.status.success().then(|| { ++ String::from_utf8(result.stdout) ++ .unwrap() ++ .trim() ++ .parse::() ++ .unwrap() ++ }); ++ if seen == Some(target) { ++ break; ++ } ++ assert!( ++ Instant::now() < until, ++ "owned indexer did not reach mined height" ++ ); ++ std::thread::sleep(Duration::from_millis(100)); ++ } ++ }; ++ for enabled in [false, true] { ++ let storage = tempfile::tempdir().unwrap(); ++ let keys = generate_keys(BitcoinNetwork::Regtest, WitnessVersion::Taproot); ++ let mut native = RgbLibWallet::new( ++ WalletData { ++ data_dir: storage.path().display().to_string(), ++ bitcoin_network: BitcoinNetwork::Regtest, ++ database_type: DatabaseType::Sqlite, ++ max_allocations_per_utxo: 5, ++ supported_schemas: vec![AssetSchema::Nia], ++ }, ++ SinglesigKeys { ++ account_xpub_vanilla: keys.account_xpub_vanilla, ++ account_xpub_colored: keys.account_xpub_colored, ++ vanilla_keychain: None, ++ master_fingerprint: keys.master_fingerprint, ++ mnemonic: Some(keys.mnemonic), ++ witness_version: WitnessVersion::Taproot, ++ }, ++ ) ++ .unwrap(); ++ let address = native.get_address().unwrap(); ++ rpc(true, "sendtoaddress", vec![json!(address), json!(0.01)]); ++ mine(); ++ let online = native ++ .go_online(OnlineOptions { ++ indexer_config: IndexerConfig::Esplora { ++ url: "http://127.0.0.1:3002".into(), ++ }, ++ skip_consistency_check: false, ++ vanilla_sync_lookback: 20, ++ failed_witness_receive_grace_secs: 604800, ++ proxy_auth_keys: HashMap::new(), ++ evm_rpc_urls: HashMap::new(), ++ }) ++ .unwrap(); ++ let policy = Arc::new( ++ crate::vss_snapshot_policy::SnapshotPolicy::load( ++ storage.path(), ++ BitcoinNetwork::Regtest, ++ "https://example.invalid/vss".into(), ++ "funded-node".into(), ++ enabled, ++ ) ++ .unwrap(), ++ ); ++ let mut wrapper = RgbLibWalletWrapper::new(Arc::new(Mutex::new(native)), online); ++ wrapper.snapshot_policy = Some(policy.clone()); ++ // Keep the complete existing writer around native reservation and its RLN journal cutpoint. ++ wrapper ++ .with_output_operation_snapshot(|| { ++ let count = wrapper.get_rgb_wallet().create_utxos( ++ online, ++ false, ++ Some(5), ++ Some(10_000), ++ 2, ++ false, ++ )?; ++ assert_eq!(count, 5); ++ Ok(()) ++ }) ++ .unwrap(); ++ mine(); ++ wrapper ++ .sync(SyncOptions { ++ keychain: SyncKeychain::Colored, ++ strategy: SyncStrategy::FullSync, ++ }) ++ .unwrap(); ++ let asset = wrapper ++ .issue_asset_nia("SNAP".into(), "Snapshot admission".into(), 0, vec![100]) ++ .unwrap() ++ .asset_id; ++ let funding = wrapper ++ .send_btc_channel_funding_begin(mining.as_str().unwrap().into(), 25_000, 2) ++ .unwrap(); ++ let funding = BitcoinPsbt::from_str(&funding).unwrap(); ++ assert_eq!( ++ funding.unsigned_tx.lock_time, ++ bitcoin::absolute::LockTime::ZERO ++ ); ++ wrapper ++ .abort_pending_vanilla_tx(funding.unsigned_tx.compute_txid().to_string()) ++ .unwrap(); ++ let expires = std::time::SystemTime::now() ++ .duration_since(std::time::UNIX_EPOCH) ++ .unwrap() ++ .as_secs() ++ + 3600; ++ let receive = wrapper ++ .witness_receive(None, Assignment::Fungible(40), expires, vec![], 1) ++ .unwrap(); ++ let recipients = HashMap::from([( ++ asset.clone(), ++ vec![Recipient { ++ recipient_id: receive.recipient_id, ++ witness_data: Some(WitnessData { ++ amount_sat: 30_000, ++ blinding: None, ++ }), ++ assignment: Assignment::Fungible(40), ++ transport_endpoints: vec![], ++ public_key: None, ++ encryption: Encryption::Optional, ++ expiration_timestamp: None, ++ }], ++ )]); ++ let begin = wrapper ++ .send_begin(recipients, true, 2, 1, expires, true, Some(0), None) ++ .unwrap(); ++ let preview = BitcoinPsbt::from_str(&begin.psbt).unwrap(); ++ assert_eq!( ++ preview.unsigned_tx.lock_time, ++ bitcoin::absolute::LockTime::ZERO ++ ); ++ assert!(begin.batch_transfer_idx.is_none()); ++ assert!( ++ preview.unsigned_tx.input.len() >= 4, ++ "real fixed-zero retry must include fee inputs" ++ ); ++ let receive = wrapper ++ .witness_receive(None, Assignment::Fungible(100), expires, vec![], 1) ++ .unwrap(); ++ let script = rgb_lib::utils::script_buf_from_recipient_id(receive.recipient_id) ++ .unwrap() ++ .unwrap(); ++ let unspent = wrapper ++ .list_unspents(false, false) ++ .unwrap() ++ .into_iter() ++ .find(|u| { ++ u.rgb_allocations.iter().any(|a| { ++ a.asset_id.as_deref() == Some(asset.as_str()) ++ && a.assignment == Assignment::Fungible(100) ++ }) ++ }) ++ .unwrap(); ++ let input: OutPoint = unspent.utxo.outpoint.into(); ++ let txout = rpc( ++ false, ++ "gettxout", ++ vec![json!(input.txid.to_string()), json!(input.vout)], ++ ); ++ let prev = TxOut { ++ value: bitcoin::Amount::from_sat(unspent.utxo.btc_amount), ++ script_pubkey: ScriptBuf::from_hex(txout["scriptPubKey"]["hex"].as_str().unwrap()) ++ .unwrap(), ++ }; ++ let tx = Transaction { ++ version: bitcoin::transaction::Version::TWO, ++ lock_time: bitcoin::absolute::LockTime::ZERO, ++ input: vec![bitcoin::TxIn { ++ previous_output: input, ++ script_sig: ScriptBuf::new(), ++ sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME, ++ witness: bitcoin::Witness::new(), ++ }], ++ output: vec![TxOut { ++ value: bitcoin::Amount::from_sat(unspent.utxo.btc_amount - 1000), ++ script_pubkey: script, ++ }], ++ }; ++ let mut psbt = BitcoinPsbt::from_unsigned_tx(tx).unwrap(); ++ psbt.inputs[0].witness_utxo = Some(prev); ++ let original = psbt.clone(); ++ let coloring = ColoringInfo { ++ asset_info_map: HashMap::from([( ++ ContractId::from_str(&asset).unwrap(), ++ rgb_lib::wallet::rust_only::AssetColoringInfo { ++ output_map: HashMap::from([(0, 100)]), ++ static_blinding: Some(777), ++ }, ++ )]), ++ static_blinding: None, ++ nonce: None, ++ }; ++ let first = wrapper ++ .color_psbt_and_prepare_consume(&mut psbt, coloring.clone()) ++ .unwrap(); ++ let inputs = HashSet::from([input]); ++ assert!(wrapper.recover_prepared_sweep(&inputs).unwrap().is_some()); ++ let snapshot = wrapper ++ .get_rgb_wallet_for_snapshot() ++ .prepare_snapshot() ++ .unwrap(); ++ wrapper ++ .cancel_prepared_sweep(first.batch_transfer_idx) ++ .unwrap(); ++ assert!( ++ !wrapper ++ .get_rgb_wallet_for_snapshot() ++ .acknowledge_snapshot(&snapshot.token) ++ .unwrap(), ++ "stale snapshot acknowledged cancelled preparation" ++ ); ++ psbt = original; ++ let prepared = wrapper ++ .color_psbt_and_prepare_consume(&mut psbt, coloring) ++ .unwrap(); ++ let batch = prepared.batch_transfer_idx; ++ let txid = psbt.unsigned_tx.compute_txid().to_string(); ++ let proof_directory = tempfile::tempdir().unwrap(); ++ let proof = proof_directory.path().join("sweep.rgb"); ++ prepared.transfers[0].save_file(&proof).unwrap(); ++ wrapper ++ .provide_out_of_band_consignment(proof.display().to_string(), vec![]) ++ .unwrap(); ++ assert!(wrapper.consume_transfer_fascia(batch).is_err()); ++ wrapper ++ .mark_prepared_sweep_handed_off(batch, txid.clone()) ++ .unwrap(); ++ assert!( ++ wrapper.cancel_prepared_sweep(batch).is_err(), ++ "handoff reservations must not be discarded" ++ ); ++ let signed = wrapper.sign_psbt(psbt.to_string()).unwrap(); ++ let tx = BitcoinPsbt::from_str(&signed) ++ .unwrap() ++ .extract_tx() ++ .unwrap(); ++ assert_eq!(tx.compute_txid().to_string(), txid); ++ rpc( ++ false, ++ "sendrawtransaction", ++ vec![json!(bitcoin::consensus::encode::serialize_hex(&tx))], ++ ); ++ assert!( ++ wrapper.consume_transfer_fascia(batch).is_err(), ++ "zero-confirmation consume was admitted" ++ ); ++ mine(); ++ wrapper.consume_transfer_fascia(batch).unwrap(); ++ wrapper.consume_transfer_fascia(batch).unwrap(); ++ wrapper.refresh(None, vec![], false).unwrap(); ++ assert_eq!( ++ wrapper ++ .get_asset_balance(ContractId::from_str(&asset).unwrap()) ++ .unwrap() ++ .settled, ++ 100 ++ ); ++ assert!( ++ policy.pending(), ++ "successful financial mutation lost its pending backup" ++ ); ++ assert_eq!( ++ policy.failed.load(std::sync::atomic::Ordering::SeqCst), ++ enabled ++ ); ++ let metadata: Value = serde_json::from_str(&policy.metadata().unwrap()).unwrap(); ++ assert!( ++ metadata["last_backup"].is_null(), ++ "failed/unattempted upload cannot invent a completion" ++ ); ++ let current = wrapper ++ .get_rgb_wallet_for_snapshot() ++ .prepare_snapshot() ++ .unwrap(); ++ assert!(wrapper ++ .get_rgb_wallet_for_snapshot() ++ .acknowledge_snapshot(¤t.token) ++ .unwrap()); ++ assert!( ++ policy.pending(), ++ "local content acknowledgement is not cloud commit" ++ ); ++ assert!(wrapper.recover_prepared_sweep(&inputs).unwrap().is_none()); ++ } ++ assert_eq!(rpc(false, "getmempoolinfo", vec![])["size"], 0); ++ } ++ #[cfg(feature = "vss")] ++ #[test] + fn every_adapter_state_mutator_uses_explicit_snapshot_admission() { + let source = include_str!("rgb.rs"); + let adapter = source @@ -5420,9 +5343,11 @@ index 14d98b1..67a40f9 100644 + "abort_pending_vanilla_tx", + "blind_receive", + "consume_fascia", ++ "color_psbt_and_prepare_consume", ++ "cancel_prepared_sweep", ++ "mark_prepared_sweep_handed_off", ++ "consume_transfer_fascia", + "create_consigments", -+ "create_utxos", -+ "create_utxos_begin", + "create_utxos_end", + "fail_transfers", + "get_address", @@ -5441,6 +5366,7 @@ index 14d98b1..67a40f9 100644 + "send_begin", + "send_btc", + "send_btc_begin", ++ "send_btc_channel_funding_begin", + "send_btc_end", + "send_end", + "send_end_db_update_only", @@ -5482,6 +5408,7 @@ index 14d98b1..67a40f9 100644 + "list_transactions", + "list_transfers", + "list_unspents", ++ "recover_prepared_sweep", + "sign_psbt", + ] { + let signature = format!("fn {method}("); @@ -5503,7 +5430,7 @@ index 14d98b1..67a40f9 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 a665290..135045c 100644 +index 490de05..dbf9a9f 100644 --- a/src/rgb_import.rs +++ b/src/rgb_import.rs @@ -209,7 +209,7 @@ pub(crate) async fn import_rgb_contract( @@ -5516,7 +5443,7 @@ index a665290..135045c 100644 let imported_id = rgb_lib::wallet::RgbWalletOpsOffline::import_contract( &mut *wallet, diff --git a/src/rgb_output_operations.rs b/src/rgb_output_operations.rs -index 803e7a8..e15cf14 100644 +index 73072b0..6ccae47 100644 --- a/src/rgb_output_operations.rs +++ b/src/rgb_output_operations.rs @@ -284,13 +284,18 @@ impl OutputBackend for NativeOutputBackend { @@ -5560,7 +5487,7 @@ index 803e7a8..e15cf14 100644 } match cache.as_ref().unwrap() { diff --git a/src/routes.rs b/src/routes.rs -index 20945c4..23cefcc 100644 +index 7265f37..94c396a 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1760,6 +1760,9 @@ impl From for RgbLibWitnessData { @@ -5601,7 +5528,7 @@ index 20945c4..23cefcc 100644 tracing::debug!("UTXO creation complete"); Ok(Json(EmptyResponse {})) -@@ -4124,7 +4126,7 @@ pub(crate) async fn lock( +@@ -4126,7 +4128,7 @@ pub(crate) async fn lock( }; tracing::debug!("Stopping node..."); @@ -5610,7 +5537,7 @@ index 20945c4..23cefcc 100644 tracing::debug!("Node stopped"); state.update_unlocked_app_state(None).await; -@@ -4674,7 +4676,7 @@ pub(crate) async fn open_channel( +@@ -4676,7 +4678,7 @@ pub(crate) async fn open_channel( } } if let Some(peer_addr) = peer_addr { @@ -5619,7 +5546,7 @@ index 20945c4..23cefcc 100644 .await?; db.persist_channel_peer(&peer_pubkey, &peer_addr)?; } else { -@@ -6137,11 +6139,13 @@ mod state_mocks { +@@ -6234,11 +6236,13 @@ mod state_mocks { lsp_base_url: None, lsp_bearer_token: None, vss_url: None, @@ -5634,7 +5561,7 @@ index 20945c4..23cefcc 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 14bb967..5b237a1 100644 +index 5bfa190..96c1154 100644 --- a/src/sdk/mod.rs +++ b/src/sdk/mod.rs @@ -140,6 +140,9 @@ impl Drop for OpenChannelVirtualIntentGuard { @@ -5656,7 +5583,7 @@ index 14bb967..5b237a1 100644 state: &Arc, ) -> Result>>, APIError> { check_changing_state(state)?; -@@ -2152,6 +2155,98 @@ pub(crate) async fn vss_backup(state: Arc) -> Result { +@@ -2211,6 +2214,98 @@ pub(crate) async fn vss_backup(state: Arc) -> Result { .await } @@ -5755,7 +5682,7 @@ index 14bb967..5b237a1 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. -@@ -2389,8 +2484,7 @@ pub(crate) async fn connect_peer( +@@ -2448,8 +2543,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 { @@ -5765,7 +5692,7 @@ index 14bb967..5b237a1 100644 state .get_db() .persist_channel_peer(&peer_pubkey, &peer_addr)?; -@@ -2653,18 +2747,26 @@ where +@@ -2712,18 +2806,26 @@ where .join("rln_output_operations_v1") }; wallet_task(wallet, move |wallet| { @@ -5803,7 +5730,7 @@ index 14bb967..5b237a1 100644 }) .await } -@@ -3234,8 +3336,7 @@ pub(crate) async fn open_channel( +@@ -3294,8 +3396,7 @@ pub(crate) async fn open_channel( } } if let Some(peer_addr) = peer_addr { @@ -5813,7 +5740,7 @@ index 14bb967..5b237a1 100644 state .get_db() .persist_channel_peer(&peer_pubkey, &peer_addr)?; -@@ -5016,11 +5117,13 @@ mod tests { +@@ -5352,11 +5453,13 @@ mod tests { lsp_bearer_token: None, database: RwLock::new(Arc::new(database)), vss_url: None, @@ -5853,7 +5780,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 df8a444..dedc116 100644 +index 764a9f0..bb2d758 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -41,11 +41,13 @@ pub fn mock_locked_app_state() -> TestAppState { @@ -7062,7 +6989,7 @@ index 0000000..c9f968e + } +} diff --git a/src/uniffi_api/mod.rs b/src/uniffi_api/mod.rs -index e9d5080..b90a61b 100644 +index 39479e7..4fea44d 100644 --- a/src/uniffi_api/mod.rs +++ b/src/uniffi_api/mod.rs @@ -1,7 +1,10 @@ @@ -7180,7 +7107,7 @@ index e9d5080..b90a61b 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))?; -@@ -1857,8 +1915,23 @@ impl SdkNode { +@@ -1912,8 +1970,23 @@ impl SdkNode { ) -> Result<(), RlnError> { let state = self.handle.app_state(); let expected = key_source_from_uniffi_bootstrap(signer.bootstrap()?); @@ -7206,7 +7133,7 @@ index e9d5080..b90a61b 100644 } #[allow(clippy::too_many_arguments)] -@@ -1901,15 +1974,18 @@ pub fn sdk_initialize(request: SdkInitRequest) -> Result<(), RlnError> { +@@ -1956,15 +2029,18 @@ pub fn sdk_initialize(request: SdkInitRequest) -> Result<(), RlnError> { Ok(()) } @@ -7233,7 +7160,7 @@ index e9d5080..b90a61b 100644 } pub fn sdk_init_with_external_signer( -@@ -2160,6 +2236,115 @@ impl crate::signer::ExternalSignerTransport for UniffiExternalSignerTransport { +@@ -2215,6 +2291,115 @@ impl crate::signer::ExternalSignerTransport for UniffiExternalSignerTransport { #[cfg(test)] mod tests; @@ -7350,10 +7277,10 @@ index e9d5080..b90a61b 100644 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 +index 42f6c14..365b3ea 100644 --- a/src/uniffi_api/native_signer.rs +++ b/src/uniffi_api/native_signer.rs -@@ -11,15 +11,63 @@ use bitcoin::Network; +@@ -11,15 +11,67 @@ use bitcoin::Network; use rand::rngs::OsRng; use rand::RngCore; use signer_external::contract::{BootstrapData, ExternalSignerBackend, SignerRequest}; @@ -7387,7 +7314,11 @@ index 42f6c14..a587e0d 100644 + ) -> Arc { + Arc::new(Self { + shutting_down: AtomicBool::new(false), -+ state: RwLock::new(Some(NativeExternalSignerState { backend, transport, message_key })), ++ state: RwLock::new(Some(NativeExternalSignerState { ++ backend, ++ transport, ++ message_key, ++ })), + }) + } + @@ -7420,32 +7351,42 @@ index 42f6c14..a587e0d 100644 fn parse_network(network: &str) -> Result { match network.to_lowercase().as_str() { "mainnet" | "bitcoin" => Ok(Network::Bitcoin), -@@ -94,7 +142,14 @@ impl NativeExternalSigner { +@@ -94,7 +146,19 @@ impl NativeExternalSigner { tracing::error!(error = ?e, "native signer transport init failed"); RlnError::internal(format!("native signer transport init failed: {e}")) })?; - Ok(Arc::new(Self { backend, transport })) -+ let message_key = super::bip322::MessageKey::from_seed(&seed, network).map_err(RlnError::InvalidRequest)?; ++ let message_key = super::bip322::MessageKey::from_seed(&seed, network) ++ .map_err(RlnError::InvalidRequest)?; + let signer = Self::from_backend(backend, transport, message_key); + let bootstrap = signer.bootstrap()?; + signer.with_state(|state| { -+ if state.message_key.account_xpub != bootstrap.account_xpub_vanilla { return Err(RlnError::ExternalSignerMismatch("BIP-322 key does not match wallet bootstrap".into())); } ++ if state.message_key.account_xpub != bootstrap.account_xpub_vanilla { ++ return Err(RlnError::ExternalSignerMismatch( ++ "BIP-322 key does not match wallet bootstrap".into(), ++ )); ++ } + Ok(()) + })?; + Ok(signer) } /// Like [`Self::new`], but with a disk-backed VLS store under `storage_dir_path`, so a -@@ -133,34 +188,65 @@ impl NativeExternalSigner { +@@ -133,34 +197,70 @@ impl NativeExternalSigner { "native signer persistent transport init failed: {e}" )) })?; - Ok(Arc::new(Self { backend, transport })) -+ let message_key = super::bip322::MessageKey::from_seed(&seed, network).map_err(RlnError::InvalidRequest)?; ++ let message_key = super::bip322::MessageKey::from_seed(&seed, network) ++ .map_err(RlnError::InvalidRequest)?; + let signer = Self::from_backend(backend, transport, message_key); + let bootstrap = signer.bootstrap()?; + signer.with_state(|state| { -+ if state.message_key.account_xpub != bootstrap.account_xpub_vanilla { return Err(RlnError::ExternalSignerMismatch("BIP-322 key does not match wallet bootstrap".into())); } ++ if state.message_key.account_xpub != bootstrap.account_xpub_vanilla { ++ return Err(RlnError::ExternalSignerMismatch( ++ "BIP-322 key does not match wallet bootstrap".into(), ++ )); ++ } + Ok(()) + })?; + Ok(signer) @@ -7521,7 +7462,32 @@ index 42f6c14..a587e0d 100644 } } -@@ -210,4 +296,185 @@ mod tests { +@@ -168,12 +268,23 @@ impl ExternalSignerHost for NativeExternalSigner { + mod tests { + use super::*; + ++ fn private_signer_directory() -> tempfile::TempDir { ++ let directory = tempfile::tempdir().unwrap(); ++ #[cfg(unix)] ++ { ++ use std::os::unix::fs::PermissionsExt; ++ std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700)) ++ .unwrap(); ++ } ++ directory ++ } ++ + /// The production/persistent native signer must never run VLS with + /// `PolicyFilter::new_permissive()` on mainnet — the same rule the remote signer daemon + /// enforces for its `--permissive` flag. + #[test] + fn native_persistent_signer_rejects_mainnet_permissive() { +- let dir = tempfile::tempdir().unwrap(); ++ let dir = private_signer_directory(); + let res = NativeExternalSigner::new_with_storage( + "11".repeat(32), + "bitcoin".to_string(), +@@ -210,4 +321,204 @@ mod tests { .expect("explicit permissive is allowed off-mainnet") ); } @@ -7543,10 +7509,20 @@ index 42f6c14..a587e0d 100644 + let message = " exact\nUTF-8😄\0message"; + let signature = signer.sign_message_bip322(message, &address).unwrap(); + assert!(signature.starts_with("smp")); -+ assert!(super::super::bip322::verify(message, &address, &signature, Network::Regtest).unwrap()); -+ assert!(!super::super::bip322::verify(message.trim(), &address, &signature, Network::Regtest).unwrap()); ++ assert!( ++ super::super::bip322::verify(message, &address, &signature, Network::Regtest).unwrap() ++ ); ++ assert!(!super::super::bip322::verify( ++ message.trim(), ++ &address, ++ &signature, ++ Network::Regtest ++ ) ++ .unwrap()); + let other = NativeExternalSigner::new("02".repeat(32), "regtest".into(), None).unwrap(); -+ assert!(signer.sign_message_bip322(message, &other.message_signing_address().unwrap()).is_err()); ++ assert!(signer ++ .sign_message_bip322(message, &other.message_signing_address().unwrap()) ++ .is_err()); + let clone = signer.clone(); + signer.shutdown().unwrap(); + assert!(clone.message_signing_address().is_err()); @@ -7587,7 +7563,7 @@ index 42f6c14..a587e0d 100644 + + #[test] + fn shutdown_releases_store_and_revokes_every_outstanding_clone() { -+ let dir = tempfile::tempdir().unwrap(); ++ let dir = private_signer_directory(); + let signer = persistent_signer(dir.path()); + let retained_clone = Arc::clone(&signer); + let before = signer.bootstrap().unwrap(); @@ -7610,7 +7586,7 @@ index 42f6c14..a587e0d 100644 + + #[test] + fn ordinary_handle_drop_does_not_revoke_retained_signer() { -+ let dir = tempfile::tempdir().unwrap(); ++ let dir = private_signer_directory(); + let signer = persistent_signer(dir.path()); + let attached_clone = Arc::clone(&signer); + drop(signer); @@ -7624,7 +7600,7 @@ index 42f6c14..a587e0d 100644 + use std::sync::mpsc; + use std::time::{Duration, Instant}; + -+ let dir = tempfile::tempdir().unwrap(); ++ let dir = private_signer_directory(); + let signer = persistent_signer(dir.path()); + let (entered_tx, entered_rx) = mpsc::channel(); + let (release_tx, release_rx) = mpsc::channel(); @@ -7665,7 +7641,7 @@ index 42f6c14..a587e0d 100644 + + #[test] + fn concurrent_shutdown_is_idempotent_and_waits_for_store_release() { -+ let dir = tempfile::tempdir().unwrap(); ++ let dir = private_signer_directory(); + let signer = persistent_signer(dir.path()); + let closes: Vec<_> = (0..8) + .map(|_| { @@ -7682,7 +7658,7 @@ index 42f6c14..a587e0d 100644 + + #[test] + fn shutdown_recovers_poison_without_reusing_signing_state() { -+ let dir = tempfile::tempdir().unwrap(); ++ let dir = private_signer_directory(); + let signer = persistent_signer(dir.path()); + let poisoned = Arc::clone(&signer); + assert!(std::thread::spawn(move || { @@ -7703,12 +7679,21 @@ index 42f6c14..a587e0d 100644 + pub(crate) fn message_signing_address(&self) -> Result { + self.with_state(|state| Ok(state.message_key.address.to_string())) + } -+ pub(crate) fn sign_message_bip322(&self, message: &str, address: &str) -> Result { -+ self.with_state(|state| state.message_key.sign(message, address).map_err(RlnError::InvalidRequest)) ++ pub(crate) fn sign_message_bip322( ++ &self, ++ message: &str, ++ address: &str, ++ ) -> Result { ++ self.with_state(|state| { ++ state ++ .message_key ++ .sign(message, address) ++ .map_err(RlnError::InvalidRequest) ++ }) + } } diff --git a/src/uniffi_api/tests.rs b/src/uniffi_api/tests.rs -index 27ea384..d4abc7f 100644 +index 26157cb..d647ce8 100644 --- a/src/uniffi_api/tests.rs +++ b/src/uniffi_api/tests.rs @@ -176,6 +176,124 @@ mod uniffi_smoke_tests { @@ -7850,16 +7835,15 @@ index 27ea384..d4abc7f 100644 unlocked_app_state: Arc::new(TokioMutex::new(None)), ldk_background_services: Arc::new(Mutex::new(None)), attached_external_signer: Arc::new(Mutex::new(None)), -@@ -608,7 +728,7 @@ mod uniffi_smoke_tests { - } - - #[test] -- fn sdk_create_rejects_invalid_vss_url() { -+ fn sdk_create_rejects_insecure_vss_url_before_storage() { - let res = SdkNode::create(SdkInitRequest { - storage_dir_path: "/tmp/rln_vss_validation_test".to_string(), - daemon_listening_port: 3001, -@@ -621,10 +741,16 @@ mod uniffi_smoke_tests { +@@ -624,6 +744,7 @@ mod uniffi_smoke_tests { + vss_url: None, + vss_allow_http: false, + vss_allow_empty_restore: false, ++ vss_automatic_backup_enabled: None, + reuse_addresses: true, + }); + assert!( +@@ -646,10 +767,16 @@ mod uniffi_smoke_tests { lsp_bearer_token: None, vss_url: Some("http://example.com/vss".to_string()), vss_allow_http: false, @@ -7878,7 +7862,7 @@ index 27ea384..d4abc7f 100644 } diff --git a/src/uniffi_api/types.rs b/src/uniffi_api/types.rs -index 875ae18..b59e0fe 100644 +index b31e9b9..734de97 100644 --- a/src/uniffi_api/types.rs +++ b/src/uniffi_api/types.rs @@ -922,6 +922,7 @@ pub struct SdkInitRequest { @@ -7891,10 +7875,10 @@ index 875ae18..b59e0fe 100644 } diff --git a/src/uniffi_api/vss_restore.rs b/src/uniffi_api/vss_restore.rs new file mode 100644 -index 0000000..1d36b2c +index 0000000..9c1b034 --- /dev/null +++ b/src/uniffi_api/vss_restore.rs -@@ -0,0 +1,1216 @@ +@@ -0,0 +1,1219 @@ +//! 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}; @@ -8436,7 +8420,10 @@ index 0000000..1d36b2c + let entry = archive + .by_index(i) + .map_err(|_| error("VSS_RESTORE_INVALID_ARCHIVE"))?; -+ let name = entry.name().trim_end_matches('/').to_owned(); ++ let name = std::str::from_utf8(entry.name_raw()) ++ .map_err(|_| error("VSS_RESTORE_INVALID_ARCHIVE"))? ++ .trim_end_matches('/') ++ .to_owned(); + if name == format!("{}/{RECEIPT}", ctx.fp) { + continue; + } @@ -9112,7 +9099,7 @@ index 0000000..1d36b2c + } +} diff --git a/src/utils.rs b/src/utils.rs -index 069153f..132ca59 100644 +index 0f52204..077df5b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,7 +30,7 @@ use std::{ @@ -9270,7 +9257,7 @@ index 069153f..132ca59 100644 return Ok(()); } } -@@ -806,6 +828,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr +@@ -814,6 +836,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr lsp_base_url: args.lsp_base_url.clone(), lsp_bearer_token: args.lsp_bearer_token.clone(), vss_url: args.vss_url.clone(), @@ -9278,7 +9265,7 @@ index 069153f..132ca59 100644 vss_allow_empty_restore: args.vss_allow_empty_restore, reuse_addresses: args.reuse_addresses, remote_signer_listen_addr: args.remote_signer_listen_addr, -@@ -817,6 +840,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr +@@ -825,6 +848,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr let app_state = Arc::new(AppState { static_state, cancel_token, @@ -9286,7 +9273,7 @@ index 069153f..132ca59 100644 unlocked_app_state: Arc::new(TokioMutex::new(None)), ldk_background_services: Arc::new(Mutex::new(None)), attached_external_signer: Arc::new(Mutex::new(None)), -@@ -1069,6 +1093,24 @@ mod utils_tests { +@@ -1108,6 +1132,24 @@ mod utils_tests { assert!(validate_vss_url("http://example.com/vss", true).is_ok()); } diff --git a/scripts/install-overlay-addon.test.js b/scripts/install-overlay-addon.test.js index 232df2b..3bca1bb 100644 --- a/scripts/install-overlay-addon.test.js +++ b/scripts/install-overlay-addon.test.js @@ -68,7 +68,7 @@ test('manifest rejects wrong artifact, source, wrapper, lock and target identiti 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-bfa\.git\?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0/) + assert.match(lock, /rgb-lib-bfa\.git\?rev=abd41d3afff6d8fb11461e26e843301650e22df3#abd41d3afff6d8fb11461e26e843301650e22df3/) 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) diff --git a/scripts/release-contract.js b/scripts/release-contract.js index e619c91..5cdea2a 100644 --- a/scripts/release-contract.js +++ b/scripts/release-contract.js @@ -7,13 +7,13 @@ const { execFileSync } = require('node:child_process') const RELEASE = Object.freeze({ 'rlnVersion': '0.17.0-beta.3', - 'ref': '7cced3aa3c2c904a88d8f8a7832981d25a5ea871', - 'commit': '7cced3aa3c2c904a88d8f8a7832981d25a5ea871', - 'lightningCommit': 'bbe68189872c26088e351d034b05c58769cb996a', + 'ref': '89ebe9fdb1be94cbba0b2bd722cea9af297399f1', + 'commit': '89ebe9fdb1be94cbba0b2bd722cea9af297399f1', + 'lightningCommit': '17f2fb56f8d96d0ec512c715d2ebc65f829eb2c2', 'rgbLibRepository': 'https://github.com/UTEXO-Protocol/rgb-lib-bfa.git', - 'rgbLibCommit': '89c37086a68ef792da75c62bce59b58b1d7952a0', + 'rgbLibCommit': 'abd41d3afff6d8fb11461e26e843301650e22df3', 'rustToolchain': '1.94.0', - 'cffiLockSha256': '3039a36910386f9d6f350ed33791cd6461174cd340247998b80726a6581c3763' + 'cffiLockSha256': '741bd3d0f81b28c1f40e426b44f5c9ae152d777269934d7c0dc919d74f908da9' }) // Exact reviewed C adapter plus the shared native lifecycle repair. This remains // an upstream release with an explicitly hashed runtime overlay, not plain RLN. @@ -56,8 +56,8 @@ const ALLOWED_FILES = Object.freeze([ ]) const GIT_SOURCES = Object.freeze({ 'musig2': 'git+https://github.com/arik-so/rust-musig2?rev=6f95a05718cbb44d8fe3fa6021aea8117aa38d50#6f95a05718cbb44d8fe3fa6021aea8117aa38d50', - 'rgb-lib': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0', - 'rgb-lib-migration': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=89c37086a68ef792da75c62bce59b58b1d7952a0#89c37086a68ef792da75c62bce59b58b1d7952a0', + 'rgb-lib': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=abd41d3afff6d8fb11461e26e843301650e22df3#abd41d3afff6d8fb11461e26e843301650e22df3', + 'rgb-lib-migration': 'git+https://github.com/UTEXO-Protocol/rgb-lib-bfa.git?rev=abd41d3afff6d8fb11461e26e843301650e22df3#abd41d3afff6d8fb11461e26e843301650e22df3', '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', diff --git a/scripts/runtime-matrix.sh b/scripts/runtime-matrix.sh index 4a13049..925a61f 100644 --- a/scripts/runtime-matrix.sh +++ b/scripts/runtime-matrix.sh @@ -12,7 +12,7 @@ if [[ "$EXPECTED_TARGET" == linux-* ]]; then fi node test.js node scripts/test-native-adapter.js -npm pack --ignore-scripts +node scripts/test-packed-consumer.js node -e ' const fs = require("node:fs"); const info = require("./index").getRuntimeInfo(); diff --git a/scripts/test-native-adapter.js b/scripts/test-native-adapter.js index b1152c6..cfc43d7 100644 --- a/scripts/test-native-adapter.js +++ b/scripts/test-native-adapter.js @@ -1,15 +1,22 @@ 'use strict' const { spawnSync } = require('node:child_process') +const path = require('node:path') const { readConfig, identity, prepareSource } = require('./install-overlay-addon') const config = readConfig() prepareSource(config) const descriptor = identity(config) -const result = spawnSync('cargo', ['test', '--locked', '-p', 'rln-c-ffi', '--lib', '--target', descriptor.target], { - stdio: 'inherit', - env: { ...process.env, RUSTUP_TOOLCHAIN: config.rustToolchain, - RLN_ADAPTER_SHA256: config.patchSha256, RLN_WRAPPER_SHA256: descriptor.wrapperSha256, - RLN_LOCK_SHA256: descriptor.lockSha256 } -}) -if (result.error) throw result.error -process.exitCode = result.status ?? 1 +// Exercise the maintained Rust overlay itself, including mutation/snapshot admission, +// staged restore, signer lifetime and BIP-322 tests, rather than only its C JSON boundary. +const environment = { ...process.env, RUSTUP_TOOLCHAIN: config.rustToolchain, + RLN_ADAPTER_SHA256: config.patchSha256, RLN_WRAPPER_SHA256: descriptor.wrapperSha256, + RLN_LOCK_SHA256: descriptor.lockSha256 } +for (const args of [ + ['test', '--locked', '-p', 'rln-c-ffi', '--lib', '--target', descriptor.target], + ['test', '--locked', '--manifest-path', path.resolve(__dirname, '../.native-source/Cargo.toml'), + '--lib', '--features', 'uniffi,vls,vss', '--target', descriptor.target, '--', '--test-threads=1'] +]) { + const result = spawnSync('cargo', args, { stdio: 'inherit', env: environment }) + if (result.error) throw result.error + if (result.status !== 0) { process.exitCode = result.status ?? 1; break } +} diff --git a/scripts/test-packed-consumer.js b/scripts/test-packed-consumer.js new file mode 100644 index 0000000..4503405 --- /dev/null +++ b/scripts/test-packed-consumer.js @@ -0,0 +1,54 @@ +'use strict' + +// Candidate qualification only. Publication retains its independent all-target gate. +const assert = require('node:assert/strict') +const crypto = require('node:crypto') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const { execFileSync } = require('node:child_process') +const { readConfig, platformSuffix, verifyAddonAdmission } = require('./install-overlay-addon') +const root = path.resolve(__dirname, '..') +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'rln-packed-consumer-')) +const digest = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') +try { + const packed = JSON.parse(execFileSync('npm', ['pack', '--ignore-scripts', '--json'], { cwd: root, encoding: 'utf8' })) + assert.equal(packed.length, 1) + const tarball = path.join(root, packed[0].filename) + execFileSync('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--prefix', temporary, tarball], { + stdio: 'inherit' + }) + const installed = path.join(temporary, 'node_modules', '@utexo', 'rgb-lightning-node-nodejs') + const suffix = platformSuffix() + const config = readConfig() + verifyAddonAdmission(installed, config, suffix) + // Run the same actual native lifecycle/VSS canary through the installed archive, + // resolving every package import inside the clean consumer, never the checkout. + let canary = fs.readFileSync(path.join(root, 'test.js'), 'utf8') + for (const relative of ['./index', './scripts/install-overlay-addon', './scripts/test-vss-safety-host']) { + const original = `require('${relative}')` + assert.ok(canary.includes(original), `Missing reviewed canary import: ${relative}`) + canary = canary.replaceAll(original, `require(${JSON.stringify(path.join(installed, relative))})`) + } + const entry = path.join(temporary, 'consumer.cjs') + fs.writeFileSync(entry, canary) + execFileSync(process.execPath, [entry], { cwd: temporary, stdio: 'inherit' }) + const info = JSON.parse(execFileSync(process.execPath, ['-e', + `process.stdout.write(JSON.stringify(require(${JSON.stringify(installed)}).getRuntimeInfo()))`], { encoding: 'utf8' })) + assert.equal(info.rln_commit, config.commit) + assert.equal(info.rgb_lib_commit, config.rgbLibCommit) + assert.equal(info.lightning_commit, config.lightningCommit) + assert.equal(info.adapter_sha256, config.patchSha256) + const original = path.join(root, `index-${suffix}.node`) + const copied = path.join(installed, `index-${suffix}.node`) + assert.equal(digest(copied), digest(original)) + fs.writeFileSync(path.join(root, 'packed-consumer-evidence.json'), JSON.stringify({ + sourceCommit: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(), + target: suffix, tarball: path.basename(tarball), tarballSha256: digest(tarball), + nativeSha256: digest(copied), info, + scope: 'Actual clean npm archive install/native load, exact identities, VSS admission, signer persistence/reopen and lifecycle; no funded network or mobile claim' + }, null, 2) + '\n') + console.log('Clean packed native consumer passed: ' + suffix) +} finally { + fs.rmSync(temporary, { recursive: true, force: true }) +} From 3b1bd68049f6a827508d6f762a0a155ae6d8876a Mon Sep 17 00:00:00 2001 From: Hardik Jain Date: Wed, 28 Oct 2026 00:26:32 +0530 Subject: [PATCH 11/11] Fix packed consumer source receipt in isolated CI containers --- scripts/test-packed-consumer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/test-packed-consumer.js b/scripts/test-packed-consumer.js index 4503405..ca4d2f4 100644 --- a/scripts/test-packed-consumer.js +++ b/scripts/test-packed-consumer.js @@ -43,7 +43,9 @@ try { const copied = path.join(installed, `index-${suffix}.node`) assert.equal(digest(copied), digest(original)) fs.writeFileSync(path.join(root, 'packed-consumer-evidence.json'), JSON.stringify({ - sourceCommit: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(), + // Docker's /candidate is owned by the runner, not the container's root UID. + // Trust only this exact source root for this read; do not persist global trust. + sourceCommit: execFileSync('git', ['-c', `safe.directory=${root}`, 'rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(), target: suffix, tarball: path.basename(tarball), tarballSha256: digest(tarball), nativeSha256: digest(copied), info, scope: 'Actual clean npm archive install/native load, exact identities, VSS admission, signer persistence/reopen and lifecycle; no funded network or mobile claim'