diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..120c9c83 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - omni-main + pull_request: + +permissions: + checks: write + +jobs: + rust-checks: + runs-on: warp-ubuntu-latest-x64-4x + steps: + - uses: actions/checkout@v5 + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libudev-dev + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + cache-provider: warpbuild + + - name: Run lint + run: make lint + + - name: Install cargo-near + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/near/cargo-near/releases/latest/download/cargo-near-installer.sh | sh + + - name: Run tests + run: make test + diff --git a/.github/workflows/update-contracts.yaml b/.github/workflows/update-contracts.yaml new file mode 100644 index 00000000..b8711a32 --- /dev/null +++ b/.github/workflows/update-contracts.yaml @@ -0,0 +1,43 @@ +on: + push: + tags: + - 'btc-bridge-v[0-9]+.[0-9]+.[0-9]+*' + + workflow_dispatch: + +name: Update Contracts +jobs: + update-contracts: + runs-on: ubuntu-latest + name: Update Contracts + permissions: + contents: write + steps: + - name: Clone the repository + uses: actions/checkout@v3 + + - name: Install cargo-near + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/near/cargo-near/releases/latest/download/cargo-near-installer.sh | sh + + - name: Build NEAR contracts + run: | + make release + timeout-minutes: 60 + + - name: Archive built WASM files + env: + RAW_TAG: ${{ github.ref_name }} + run: | + SAFE_TAG="${RAW_TAG//./-}" + ZIP_NAME="${SAFE_TAG}.zip" + mkdir -p artifacts + find ./res -name "*.wasm" -exec cp {} artifacts/ \; + zip -j "$ZIP_NAME" artifacts/* + shell: bash + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + files: "*.zip" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ceb2b988 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..c803a0bf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,149 @@ +# NEAR BTC/Zcash Bridge + +Bridge between Bitcoin/Zcash and NEAR Protocol. Users deposit BTC/ZEC to receive nBTC/nZEC (NEP-141 token) and withdraw nBTC/nZEC to receive BTC/ZEC back. + +**Trust Model:** +- **BTC → NEAR (deposit):** Trustless verification via BTC Light Client (Merkle proof validation) +- **NEAR → BTC (withdraw):** Requires trust in NEAR validator set for Chain Signatures (MPC) + +--- + +## Build / Test / Lint + +```bash +# Build for development (non-reproducible) +make build-local-bitcoin # Bitcoin bridge +make build-local-zcash # Zcash bridge + +# Run tests +make test + +# Format and clippy +cargo fmt --all # Format all code +make clippy-bitcoin # Clippy for Bitcoin +make clippy-zcash # Clippy for Zcash +``` + +--- + +## Key Architecture + +**Contracts:** `contracts/nbtc/` (NEP-141 token), `contracts/satoshi-bridge/` (main bridge), `contracts/mock-*` (testing) + +**External Dependencies:** BTC Light Client (Merkle proof verification), Chain Signatures (MPC signing) + +### Bridge Flows + +**Deposit (BTC → nBTC)** +``` +1. User sends BTC to deposit address (derived from DepositMsg hash) +2. Relayer: bridge.verify_deposit(tx_proof) +3. Bridge verifies with Light Client → calls nbtc.mint(user, amount) +4. UTXO added to bridge's available set +``` + +**Withdraw (nBTC → BTC)** +``` +1. User: nbtc.ft_transfer(bridge, amount, WithdrawMsg) + → Tokens TRANSFERRED to bridge (not burned yet!) +2. nBTC: bridge.ft_on_transfer(user, amount, msg) → Bridge returns 0 (keeps tokens) +3. Bridge creates BTC tx, Chain Signatures signs +4. Tx broadcast to Bitcoin network +5. Relayer: bridge.verify_withdraw(tx_proof) +6. Bridge verifies → calls nbtc.burn(user, amount, relayer, fee) + → Burns from bridge balance (tokens already there!) +``` + +--- + +## Security Invariants + +### Token Flow (NEP-141) +- **Withdraw tokens already transferred:** By the time `burn()` is called, tokens are in bridge balance via `ft_transfer` +- **burn_account_id is for events only:** Actual burn happens from bridge balance, not from burn_account_id +- **ft_on_transfer return value:** `0` = keep all tokens, `amount` = refund amount +- Only burn after BTC tx is verified on-chain + +### Arithmetic Safety +- **overflow-checks = true:** All overflow panics in release mode (fail-safe) +- Use `checked_mul()`, `checked_add()` for explicit error handling +- Prefer panic over silent + +### State Management +- Mutate state (mark UTXO used, update balances) BEFORE cross-contract calls +- Create and emit events AFTER all state mutations complete +- **Cross-contract calls are NOT atomic:** Each callback is a separate transaction - must manually rollback state in callback if external call fails + +### Zcash-Specific +- **Mutual exclusion:** `actual_received_amounts.len() == 1` ensures EITHER transparent OR Orchard output, never both +- **OVK required:** All Orchard bundles must provide valid Outgoing Viewing Key for decryption +- **Address restrictions:** Transparent addresses CANNOT accept Orchard bundles (panics) +- **Bridge transparency:** Full transaction tracking required, privacy is NOT a design goal +- **Branch IDs hardcoded:** Network upgrades require contract redeployment anyway + +--- + +## Critical Patterns + +**NEAR decorators:** `#[private]` for callbacks, `#[access_control_any(roles(...))]` for admin functions, `#[pause(except(roles(...)))]` for pausable functions, `assert_one_yocto()` to prevent batching + +**Security checks:** Always use `require!(condition, "message")` for validation, `checked_*` arithmetic for money operations, emit events AFTER state changes + +--- + +## Safe Functions (Omni Bridge Integration) + +The bridge provides "safe" versions of deposit/mint functions primarily used by Omni Bridge: + +### verify_deposit vs safe_verify_deposit + +**verify_deposit (standard):** +- Normal deposit flow with fees +- Charges deposit bridge fee +- Pays for user's token storage +- Requires `safe_deposit: None` in DepositMsg +- Does NOT revert on mint failures (uses lost & found) + +**safe_verify_deposit (integration):** +- Primarily used by Omni Bridge +- NO fees charged +- User must attach NEAR for storage (via `#[payable]`) +- **Reverts entire transaction if mint fails** (no lost & found) +- Requires `safe_deposit: Some(SafeDepositMsg)` in DepositMsg +- **post_actions must be None** (not supported in safe mode) +- Safer for integrations - atomic success/failure + +### mint vs safe_mint (nbtc contract) + +**mint (standard):** +- Mints tokens unconditionally +- If account not registered → panics or creates account + +**safe_mint (integration):** +- Checks if account is registered first +- If NOT registered → returns `U128(0)` instead of panicking +- Used by safe_verify_deposit to detect failures + +--- + +## Design Decisions (Non-Issues) + +These patterns are intentional. Do not flag or "fix" them: + +- **DAO powers are by design:** Governance functions with DAO role are necessary, not a vulnerability +- **Expiry height gap:** Buffer for transaction processing delays (Zcash) +- **No validation for self-serialized data:** Format guaranteed by construction - only validate external inputs +- **Public API vs private callbacks:** If parameter cannot be passed through public API, no vulnerability exists + +--- + +## Git Workflow + +**Main branch:** `omni-main` (use for PRs) + +**Before committing:** Run `cargo test`, `cargo fmt`, `cargo clippy`. **Only commit if user explicitly requests.** + +--- + +*Version: 2.1* +*Last Updated: 2026-02-16* diff --git a/Cargo.lock b/Cargo.lock index 4b15e5a1..2188155e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,13 +27,23 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.6", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cipher", "cpufeatures", ] @@ -70,19 +80,25 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -91,13 +107,13 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -108,9 +124,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" @@ -119,12 +135,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -167,6 +183,22 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "bellman" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afceed28bac7f9f5a508bca8aeeff51cdfa4770c0b967ac55c621e2ddfd6171" +dependencies = [ + "bitvec", + "blake2s_simd", + "byteorder", + "ff", + "group", + "pairing", + "rand_core", + "subtle", +] + [[package]] name = "binary-install" version = "0.2.0" @@ -185,11 +217,27 @@ dependencies = [ "zip", ] +[[package]] +name = "bip32" +version = "0.6.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143f5327f23168716be068f8e1014ba2ea16a6c91e8777bc8927da7b51e1df1f" +dependencies = [ + "bs58 0.5.1", + "hmac 0.13.0-pre.4", + "rand_core", + "ripemd 0.2.0-pre.4", + "secp256k1 0.29.1", + "sha2 0.11.0-pre.4", + "subtle", + "zeroize", +] + [[package]] name = "bitcoin" -version = "0.32.6" +version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8929a18b8e33ea6b3c09297b687baaa71fb1b97353243a3f1029fad5c59c5b" +checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" dependencies = [ "base58ck", "bech32", @@ -247,9 +295,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitvec" @@ -269,7 +317,29 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq 0.3.1", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90f7deecfac93095eb874a40febd69427776e24e1bd7f87f33ac62d6f0174df" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq 0.3.1", ] [[package]] @@ -281,6 +351,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd016a0ddc7cb13661bf5576073ce07330a693f8608a1320b4e20561cc12cdc" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff", + "group", + "pairing", + "rand_core", + "subtle", +] + [[package]] name = "blst" version = "0.3.15" @@ -295,9 +387,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.6.4" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6" +checksum = "c2529c31017402be841eb45892278a6c21a000c0a17643af326c73a73f83f0fb" dependencies = [ "bon-macros", "rustversion", @@ -305,17 +397,17 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.6.4" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca" +checksum = "d82020dadcb845a345591863adb65d74fa8dc5c18a0b6d408470e13b7adc7005" dependencies = [ - "darling 0.20.11", + "darling 0.21.3", "ident_case", - "prettyplease 0.2.33", + "prettyplease 0.2.37", "proc-macro2", "quote", "rustversion", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -338,7 +430,7 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -353,14 +445,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ + "sha2 0.10.9", "tinyvec", ] [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -405,9 +498,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.10" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" dependencies = [ "serde", ] @@ -429,7 +522,7 @@ dependencies = [ "indenter", "pathdiff", "rustc_version", - "sha2", + "sha2 0.10.9", "tracing", ] @@ -456,12 +549,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" -version = "1.2.26" +version = "1.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -475,9 +578,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -485,6 +588,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.3", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.41" @@ -506,8 +633,9 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", + "zeroize", ] [[package]] @@ -532,6 +660,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.5.0" @@ -554,6 +688,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -565,11 +708,11 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -599,9 +742,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -625,6 +768,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b8ce8218c97789f16356e7896b3714f26c2ee1079b79c0b7ae7064bb9089fa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "crypto-shared" version = "0.1.0" @@ -634,7 +786,7 @@ dependencies = [ "borsh", "getrandom 0.2.16", "k256", - "near-account-id", + "near-account-id 1.1.3", "near-sdk", "serde", "serde_json", @@ -648,10 +800,10 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -666,7 +818,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -689,6 +841,16 @@ dependencies = [ "darling_macro 0.20.11", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling_core" version = "0.13.4" @@ -714,7 +876,21 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.102", + "syn 2.0.106", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.106", ] [[package]] @@ -736,7 +912,18 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.102", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.106", ] [[package]] @@ -751,9 +938,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", "serde", @@ -761,13 +948,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -776,7 +963,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -787,7 +983,19 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", ] [[package]] @@ -796,9 +1004,20 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" +dependencies = [ + "block-buffer 0.11.0-rc.3", + "crypto-common 0.2.0-rc.1", "subtle", ] @@ -808,7 +1027,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "dirs-sys-next", ] @@ -831,7 +1050,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", ] [[package]] @@ -842,9 +1070,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "easy-ext" @@ -859,7 +1087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "serdect", @@ -879,15 +1107,15 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", "rand_core", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -906,7 +1134,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -924,7 +1152,7 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", ] [[package]] @@ -944,7 +1172,17 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", +] + +[[package]] +name = "equihash" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4f333d4ccc9d23c06593733673026efa71a332e028b00f12cf427b9677dce9" +dependencies = [ + "blake2b_simd", + "core2", ] [[package]] @@ -955,12 +1193,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -973,6 +1211,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "f4jumble" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d42773cb15447644d170be20231a3268600e0c4cea8987d013b93ac973d3cf7" +dependencies = [ + "blake2b_simd", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -985,6 +1232,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec", "rand_core", "subtle", ] @@ -997,16 +1245,22 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "fixed-hash" version = "0.7.0" @@ -1064,13 +1318,27 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] +[[package]] +name = "fpe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" +dependencies = [ + "cbc", + "cipher", + "libm", + "num-bigint 0.4.6", + "num-integer", + "num-traits", +] + [[package]] name = "fs2" version = "0.4.3" @@ -1143,7 +1411,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "wasi 0.11.1+wasi-snapshot-preview1", ] @@ -1154,10 +1422,22 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.3+wasi-0.2.4", +] + +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -1168,9 +1448,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "group" @@ -1179,15 +1459,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", + "memuse", "rand_core", "subtle", ] [[package]] name = "h2" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1195,13 +1476,68 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "halo2_gadgets" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a5e510d58a07d8ed238a5a8a436fe6c2c79e1bb2611f62688bc65007b4e6e7" +dependencies = [ + "arrayvec", + "bitvec", + "ff", + "group", + "halo2_poseidon", + "halo2_proofs", + "lazy_static", + "pasta_curves", + "rand", + "sinsemilla", + "subtle", + "uint", +] + +[[package]] +name = "halo2_legacy_pdqsort" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" + +[[package]] +name = "halo2_poseidon" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" +dependencies = [ + "bitvec", + "ff", + "group", + "pasta_curves", +] + +[[package]] +name = "halo2_proofs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019561b5f3be60731e7b72f3f7878c5badb4174362d860b03d3cf64cb47f90db" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_legacy_pdqsort", + "indexmap 1.9.3", + "maybe-rayon", + "pasta_curves", + "rand_core", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1210,9 +1546,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -1233,9 +1569,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1267,16 +1603,25 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] -name = "home" -version = "0.5.11" +name = "hmac" +version = "0.13.0-pre.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "e4b1fb14e4df79f9406b434b60acef9f45c26c50062cccf1346c6103b8c47d58" dependencies = [ - "windows-sys 0.59.0", + "digest 0.11.0-pre.9", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", ] [[package]] @@ -1319,21 +1664,32 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1373,9 +1729,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64 0.22.1", "bytes", @@ -1515,9 +1871,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1534,11 +1890,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "incrementalmerkletree" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30821f91f0fa8660edca547918dc59812893b497d07c1144f326f07fdd94aba9" +dependencies = [ + "either", +] + [[package]] name = "indenter" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" @@ -1553,12 +1918,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "serde", ] @@ -1571,6 +1936,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if 1.0.3", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1605,6 +1981,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1613,9 +1998,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -1623,9 +2008,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -1660,18 +2045,32 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jubjub" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8499f7a74008aafbecb2a2e608a3e13e4dd3e84df198b604451efe93f2de6e61" +dependencies = [ + "bitvec", + "bls12_381", + "ff", + "group", + "rand_core", + "subtle", +] + [[package]] name = "k256" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "ecdsa", "elliptic-curve", "once_cell", "serdect", - "sha2", + "sha2 0.10.9", "signature", ] @@ -1695,17 +2094,23 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "libc", "redox_syscall", ] @@ -1722,6 +2127,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "lock_api" version = "0.4.13" @@ -1734,9 +2145,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -1744,14 +2155,23 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if 1.0.3", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memory_units" @@ -1759,6 +2179,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" +[[package]] +name = "memuse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" + [[package]] name = "mime" version = "0.3.17" @@ -1827,7 +2253,7 @@ dependencies = [ [[package]] name = "nbtc" -version = "0.1.0" +version = "0.3.0" dependencies = [ "near-contract-standards", "near-sdk", @@ -1840,7 +2266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c49593c9e94454a2368a4c0a511bf4bf1413aff4d23f16e1d8f4e64b5215351" dependencies = [ "borsh", - "schemars", + "schemars 0.8.22", "semver", "serde", ] @@ -1871,7 +2297,7 @@ dependencies = [ "near_schemafy_lib", "proc-macro2", "quote", - "schemars", + "schemars 0.8.22", "serde_json", ] @@ -1887,9 +2313,19 @@ dependencies = [ [[package]] name = "near-account-id" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed69d94899cfdfba16182bd681ad9e6b7f888e29532b04c56da9ae05a4c5bc4" +checksum = "8542f031adc257a27ba46ad904c241a88470ee95130663a9e5c08cf8e124f4d4" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "near-account-id" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702dbca982e748975658812c7be2ca53211f454137486f98f6cf768934e2cb29" dependencies = [ "borsh", "serde", @@ -1897,23 +2333,23 @@ dependencies = [ [[package]] name = "near-chain-configs" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35aca612e6aee487a185766f51f5218e305257270136f0b911f2ecd4184c8986" +checksum = "b309d3b1f8adee89167babb4695ae1fdfe1102ef019b9f44dc1dbc5389327c42" dependencies = [ "anyhow", "bytesize", "chrono", - "derive_more", - "near-config-utils", - "near-crypto", - "near-parameters", - "near-primitives", - "near-time", + "derive_more 1.0.0", + "near-config-utils 0.30.3", + "near-crypto 0.30.3", + "near-parameters 0.30.3", + "near-primitives 0.30.3", + "near-time 0.30.3", "num-rational", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smart-default", "time", "tracing", @@ -1921,68 +2357,114 @@ dependencies = [ [[package]] name = "near-config-utils" -version = "0.30.1" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb6045fa1f9503c61665af42d1534b04a854a6b4aeecb33fd53a5acaa4635b7" +dependencies = [ + "anyhow", + "json_comments", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "near-config-utils" +version = "0.34.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94d5cbc85fdf0bf4ce0836d22243ab156b8d9491de04128eb2e1a334f3f9aec" +checksum = "da2ba8f7129472fc147b867e904e4b8f398aa79f263f54dff6283c4860446ef8" dependencies = [ "anyhow", "json_comments", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", ] [[package]] name = "near-contract-standards" -version = "5.14.0" +version = "5.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef23d0204b2c12ff54bb04c6cb83fadf8d74ea77acde09263f279b3b9c97a684" +checksum = "d59d3d4fd5d6cb11907c69b57f1c15e30acd48d775be5b5c4ccc79ffd6a35ab5" dependencies = [ "near-sdk", ] [[package]] name = "near-crypto" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8808d0d9674795fd6f5c78a4d50b80d297cedb79731525a8111c960aa110f9e" +checksum = "7c635fb7ddbd807d92e1a8a3dc57d45e92faa15eaf2a8f0fbc977f6bc8fda6ce" dependencies = [ "blake2", "borsh", "bs58 0.4.0", "curve25519-dalek", - "derive_more", + "derive_more 1.0.0", "ed25519-dalek", "hex", - "near-account-id", - "near-config-utils", - "near-schema-checker-lib", - "near-stdx", + "near-account-id 1.1.3", + "near-config-utils 0.30.3", + "near-schema-checker-lib 0.30.3", + "near-stdx 0.30.3", "primitive-types", "rand", "secp256k1 0.27.0", "serde", "serde_json", "subtle", - "thiserror 2.0.12", + "thiserror 2.0.16", +] + +[[package]] +name = "near-crypto" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c12a12485f8baafa85d5c413885b795bfa1d7d0ab7fd49b4f7fbe6cd270325b" +dependencies = [ + "blake2", + "borsh", + "bs58 0.4.0", + "curve25519-dalek", + "derive_more 2.0.1", + "ed25519-dalek", + "hex", + "near-account-id 2.6.0", + "near-config-utils 0.34.7", + "near-schema-checker-lib 0.34.7", + "near-stdx 0.34.7", + "primitive-types", + "secp256k1 0.27.0", + "serde", + "serde_json", + "subtle", + "thiserror 2.0.16", ] [[package]] name = "near-fmt" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c0e4d846b9c27b30e5f24e788fb8cc55c046f72e2048e2539dbcb04d9a71c4" +checksum = "32d6b26918e71a60b56b0fe6604198d0b29df4e0b27dc944cad7af3e1ada6976" dependencies = [ - "near-primitives-core", + "near-primitives-core 0.30.3", +] + +[[package]] +name = "near-fmt" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31b6d8fb4146cf0a7dcadf7816bf7b8efd5c081d6d2ca524bc80815b5f86812" +dependencies = [ + "near-primitives-core 0.34.7", ] [[package]] name = "near-gas" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180edcc7dc2fac41f93570d0c7b759c1b6d492f6ad093d749d644a40b4310a97" +checksum = "41ca4044222f2f392ab61d27d0aefc5106b1ece4dcd22c5c987e3c75371d2a37" dependencies = [ "borsh", - "schemars", + "schemars 0.8.22", "serde", ] @@ -1996,49 +2478,68 @@ dependencies = [ "lazy_static", "log", "near-chain-configs", - "near-crypto", + "near-crypto 0.30.3", "near-jsonrpc-primitives", - "near-primitives", + "near-primitives 0.30.3", "reqwest", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "near-jsonrpc-primitives" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ac3e779b1ad979957f05e43c92a79fbe7e1315647ab4d530e2a9a66bc62f5e" +checksum = "963e3051ebca3bb37a4a411b44d5744efdfa0b227dbcad5e03732dd0b9672b2d" dependencies = [ "arbitrary", "near-chain-configs", - "near-crypto", - "near-primitives", - "near-schema-checker-lib", + "near-crypto 0.30.3", + "near-primitives 0.30.3", + "near-schema-checker-lib 0.30.3", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", ] [[package]] name = "near-parameters" -version = "0.30.1" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e364f850512d7f1ee1eb398e1da85fd3ef95eb3cbce8db2d505eed054bbe848" +dependencies = [ + "borsh", + "enum-map", + "near-account-id 1.1.3", + "near-primitives-core 0.30.3", + "near-schema-checker-lib 0.30.3", + "num-rational", + "serde", + "serde_repr", + "serde_yaml", + "strum 0.24.1", + "thiserror 2.0.16", +] + +[[package]] +name = "near-parameters" +version = "0.34.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbb139bec6b7088d6afab0a3662725e5ee82d0ad725b67c1d45447c3d45fe55" +checksum = "e7a561606a8beb563bf166c8a9ceb7f97058b376d17ea1a9b4b65ebc9bff29ac" dependencies = [ "borsh", "enum-map", - "near-account-id", - "near-primitives-core", - "near-schema-checker-lib", + "near-account-id 2.6.0", + "near-primitives-core 0.34.7", + "near-schema-checker-lib 0.34.7", "num-rational", "serde", "serde_repr", "serde_yaml", "strum 0.24.1", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -2066,9 +2567,9 @@ dependencies = [ [[package]] name = "near-primitives" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f06d70f10eb505600ec6627f5fd64a1d1700af71098282e339c993e6319151d" +checksum = "1ca734a17b2a973e4753658dd4370f6b35e106ff6c0f9620cbe5283988597833" dependencies = [ "arbitrary", "base64 0.21.7", @@ -2076,20 +2577,20 @@ dependencies = [ "borsh", "bytes", "bytesize", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "chrono", - "derive_more", + "derive_more 1.0.0", "easy-ext", "enum-map", "hex", - "itertools", - "near-crypto", - "near-fmt", - "near-parameters", - "near-primitives-core", - "near-schema-checker-lib", - "near-stdx", - "near-time", + "itertools 0.12.1", + "near-crypto 0.30.3", + "near-fmt 0.30.3", + "near-parameters 0.30.3", + "near-primitives-core 0.30.3", + "near-schema-checker-lib 0.30.3", + "near-stdx 0.30.3", + "near-time 0.30.3", "num-rational", "ordered-float", "primitive-types", @@ -2101,30 +2602,94 @@ dependencies = [ "sha3", "smart-default", "strum 0.24.1", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", "zstd 0.13.3", ] +[[package]] +name = "near-primitives" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ccddcf4a73e19afd681faaa7e83fc4046ec71f4bbe58c58ff4ae4432f36e3aa" +dependencies = [ + "arbitrary", + "base64 0.21.7", + "bitvec", + "borsh", + "bytes", + "bytesize", + "chrono", + "derive_more 2.0.1", + "easy-ext", + "enum-map", + "hex", + "itertools 0.14.0", + "near-crypto 0.34.7", + "near-fmt 0.34.7", + "near-parameters 0.34.7", + "near-primitives-core 0.34.7", + "near-schema-checker-lib 0.34.7", + "near-stdx 0.34.7", + "near-time 0.34.7", + "num-rational", + "ordered-float", + "primitive-types", + "serde", + "serde_json", + "serde_with", + "sha3", + "smallvec", + "smart-default", + "strum 0.24.1", + "thiserror 2.0.16", + "tracing", + "zstd 0.13.3", +] + +[[package]] +name = "near-primitives-core" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953534fb0dff03f2042a12a933e31d86dd79601c2640338307bba724919e1876" +dependencies = [ + "arbitrary", + "base64 0.21.7", + "borsh", + "bs58 0.4.0", + "derive_more 1.0.0", + "enum-map", + "near-account-id 1.1.3", + "near-schema-checker-lib 0.30.3", + "num-rational", + "serde", + "serde_repr", + "sha2 0.10.9", + "thiserror 2.0.16", +] + [[package]] name = "near-primitives-core" -version = "0.30.1" +version = "0.34.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "408abb7e360ae1353d5a3cde62b4a2f0abde96c9ff4045f23cabda15c22b6ec9" +checksum = "7c93d8c5d6aecfec0aa9d60ab34408c68b13d5c1bfc0f3afeee8c99fa521cdb3" dependencies = [ "arbitrary", "base64 0.21.7", "borsh", "bs58 0.4.0", - "derive_more", + "derive_more 2.0.1", "enum-map", - "near-account-id", - "near-schema-checker-lib", + "near-account-id 2.6.0", + "near-gas", + "near-schema-checker-lib 0.34.7", + "near-token", "num-rational", "serde", "serde_repr", - "sha2", - "thiserror 2.0.12", + "serde_with", + "sha2 0.10.9", + "thiserror 2.0.16", ] [[package]] @@ -2142,41 +2707,63 @@ dependencies = [ [[package]] name = "near-schema-checker-core" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1fbfbc3c53b00aa893f8cb64abc5c12601edb8cecb878baf6f8f00e3184d3d" +checksum = "ecf3abb048646186aef4796d5bcda22c2c9246beaabaf3ea568c0cce2229257b" + +[[package]] +name = "near-schema-checker-core" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f969a965d1ea04e1f085ee4d6c7273ae1064f578711087f3beaf8d400672cc7e" + +[[package]] +name = "near-schema-checker-lib" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1416c5b236ea30152895df73213eca04c997c7bd60d83a1c18141f8705759865" +dependencies = [ + "near-schema-checker-core 0.30.3", + "near-schema-checker-macro 0.30.3", +] [[package]] name = "near-schema-checker-lib" -version = "0.30.1" +version = "0.34.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f424ce08c8d715f529a8f8dcd246f574042f0ed0b393d0aaefdf3cc693d5a9f" +checksum = "e4ae7538880de8a8d75e150dd0f4f685211ddd654ab12a339f40458df6d191dd" dependencies = [ - "near-schema-checker-core", - "near-schema-checker-macro", + "near-schema-checker-core 0.34.7", + "near-schema-checker-macro 0.34.7", ] [[package]] name = "near-schema-checker-macro" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d191936f902770069255b16c95d1fb8edd6f3c3817c9228933a20ec8466737a3" +checksum = "a60d29f7f64c2fc6d2fd25139863a6887b4d7fbcc79db8caad9c72eca67f05e9" + +[[package]] +name = "near-schema-checker-macro" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9eb7d4dc413fe39ffa7fe5591ed4c24bc8139b9de8497689178d0101ae5167" [[package]] name = "near-sdk" -version = "5.14.0" +version = "5.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1477ca4eb6d4a70a0e5740c5d34c268eedacce936ca557d3450ed5bd873fd06" +checksum = "0f3fa35758aba48e4f13528ba2f603e860dad03233d446e5c65ebd619296b607" dependencies = [ "base64 0.22.1", "borsh", "bs58 0.5.1", - "near-account-id", - "near-crypto", + "near-account-id 2.6.0", + "near-crypto 0.34.7", "near-gas", - "near-parameters", - "near-primitives", - "near-primitives-core", + "near-parameters 0.34.7", + "near-primitives 0.34.7", + "near-primitives-core 0.34.7", "near-sdk-macros", "near-sys", "near-token", @@ -2184,14 +2771,15 @@ dependencies = [ "once_cell", "serde", "serde_json", + "serde_with", "wee_alloc", ] [[package]] name = "near-sdk-macros" -version = "5.14.0" +version = "5.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29fe6d31a827e421d0d3f5c38fe3cc73f9f2a2aae41d2601d37c22d7ec1aae" +checksum = "bb141510850a842d010d706c00bcbf31beba188c706415cc9392ffd62a127e09" dependencies = [ "Inflector", "darling 0.20.11", @@ -2201,36 +2789,53 @@ dependencies = [ "serde_json", "strum 0.26.3", "strum_macros 0.26.4", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] name = "near-stdx" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f292226fd8f4c7c21cf6b1da1c17e9b484ebc1b9aeb4251d69336d28b7917ace" +checksum = "13869f432b1b457c36c9332471d868da6b0ee971e2da0b94deb376aba8d27e6b" + +[[package]] +name = "near-stdx" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5dc0456309fcb256a0609d829971fd99f343e1a7f3b72f85364e64250a4555" [[package]] name = "near-sys" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7d8e0ba9994e4d54cb4b301cd5fa9f2defcb69851148103512b9640a7e91572" +checksum = "f4ea77bb86969ff09c83faa517b2209c4876928381ed31e29c06cae2de0a216b" [[package]] name = "near-time" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806ae1785ed79e99e9183646e5fe18ecee504385350a45c600ee189d904808a9" +checksum = "d1b143d7249e64ebfd1f6da7b1c15f4a9d0ee5d9be3556771a5b4b665a2c22cb" dependencies = [ "serde", "time", ] +[[package]] +name = "near-time" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9ae070cbd84d16b948fcc335ea82db35919bf856e349f333143ff2894eeafd" +dependencies = [ + "parking_lot", + "serde", + "time", +] + [[package]] name = "near-token" -version = "0.3.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3e60aa26a74dc514b1b6408fdd06cefe2eb0ff029020956c1c6517594048fd" +checksum = "34de6b54d82d0790b2a56b677e7b4ecb7f021a7e8559f8611065c890d56cfcda" dependencies = [ "borsh", "serde", @@ -2238,9 +2843,9 @@ dependencies = [ [[package]] name = "near-vm-runner" -version = "0.30.1" +version = "0.34.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e44b5b6582676805ab61bc60e65e56eec1460c58a7c951dd662b7a5c677554" +checksum = "3c9b4794d695cf13a1a117d76a3e87b6b660f8f7862f9df1544ba8bbd96634c9" dependencies = [ "blst", "borsh", @@ -2248,23 +2853,23 @@ dependencies = [ "ed25519-dalek", "enum-map", "lru", - "near-crypto", - "near-parameters", - "near-primitives-core", - "near-schema-checker-lib", - "near-stdx", + "near-crypto 0.34.7", + "near-parameters 0.34.7", + "near-primitives-core 0.34.7", + "near-schema-checker-lib 0.34.7", + "near-stdx 0.34.7", "num-rational", + "parking_lot", "rand", "rayon", - "ripemd", + "ripemd 0.1.3", "rustix", "serde", - "serde_repr", - "sha2", + "sha2 0.10.9", "sha3", "strum 0.24.1", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", "zeropool-bn", ] @@ -2284,19 +2889,19 @@ dependencies = [ "json-patch", "libc", "near-abi-client", - "near-account-id", - "near-crypto", + "near-account-id 1.1.3", + "near-crypto 0.30.3", "near-gas", "near-jsonrpc-client", "near-jsonrpc-primitives", - "near-primitives", + "near-primitives 0.30.3", "near-sandbox-utils", "near-token", "rand", "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 1.0.69", "tokio", @@ -2332,6 +2937,12 @@ dependencies = [ "uriparse", ] +[[package]] +name = "nonempty" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" + [[package]] name = "num-bigint" version = "0.3.3" @@ -2343,6 +2954,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2365,7 +2986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg", - "num-bigint", + "num-bigint 0.3.3", "num-integer", "num-traits", "serde", @@ -2399,20 +3020,47 @@ dependencies = [ "memchr", ] +[[package]] +name = "omni-utils" +version = "0.1.0" +source = "git+https://github.com/near-one/omni-utils?rev=f9bc3ea4a72e97f02660b8f1c0b2f79a9fbde3a4#f9bc3ea4a72e97f02660b8f1c0b2f79a9fbde3a4" +dependencies = [ + "near-sdk", + "omni-utils-derive", + "serde", + "serde_json", +] + +[[package]] +name = "omni-utils-derive" +version = "0.1.0" +source = "git+https://github.com/near-one/omni-utils?rev=f9bc3ea4a72e97f02660b8f1c0b2f79a9fbde3a4#f9bc3ea4a72e97f02660b8f1c0b2f79a9fbde3a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", + "bitflags 2.9.4", + "cfg-if 1.0.3", "foreign-types", "libc", "once_cell", @@ -2428,7 +3076,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -2449,6 +3097,41 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "orchard" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ef66fcf99348242a20d582d7434da381a867df8dc155b3a980eca767c56137" +dependencies = [ + "aes", + "bitvec", + "blake2b_simd", + "core2", + "ff", + "fpe", + "getset", + "group", + "halo2_gadgets", + "halo2_poseidon", + "halo2_proofs", + "hex", + "incrementalmerkletree", + "lazy_static", + "memuse", + "nonempty", + "pasta_curves", + "rand", + "reddsa", + "serde", + "sinsemilla", + "subtle", + "tracing", + "visibility", + "zcash_note_encryption", + "zcash_spec", + "zip32", +] + [[package]] name = "ordered-float" version = "4.6.0" @@ -2461,6 +3144,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + [[package]] name = "parking_lot" version = "0.12.4" @@ -2477,11 +3169,11 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2495,6 +3187,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "lazy_static", + "rand", + "static_assertions", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -2510,17 +3217,17 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", "password-hash", - "sha2", + "sha2 0.10.9", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" @@ -2539,7 +3246,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -2570,11 +3277,22 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -2606,12 +3324,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.33" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -2642,11 +3360,33 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -2662,9 +3402,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -2706,9 +3446,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -2716,21 +3456,51 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] +[[package]] +name = "reddsa" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a5191930e84973293aa5f532b513404460cd2216c1cfb76d08748c15b40b02" +dependencies = [ + "blake2b_simd", + "byteorder", + "group", + "hex", + "jubjub", + "pasta_curves", + "rand_core", + "serde", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "redjubjub" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b0ac1bc6bb3696d2c6f52cff8fba57238b81da8c0214ee6cd146eb8fde364e" +dependencies = [ + "rand_core", + "reddsa", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] @@ -2744,11 +3514,31 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -2758,9 +3548,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -2769,15 +3559,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -2819,7 +3609,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -2830,7 +3620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if 1.0.1", + "cfg-if 1.0.3", "getrandom 0.2.16", "libc", "untrusted", @@ -2843,14 +3633,23 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "ripemd" +version = "0.2.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48cf93482ea998ad1302c42739bc73ab3adc574890c373ec89710e219357579" +dependencies = [ + "digest 0.11.0-pre.9", ] [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hex" @@ -2869,22 +3668,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "log", "once_cell", @@ -2906,9 +3705,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -2917,9 +3716,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2928,10 +3727,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "satoshi-bridge" +name = "sapling-crypto" version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d3c081c83f1dc87403d9d71a06f52301c0aa9ea4c17da2a3435bbf493ffba4" +dependencies = [ + "aes", + "bellman", + "bitvec", + "blake2b_simd", + "blake2s_simd", + "bls12_381", + "core2", + "document-features", + "ff", + "fpe", + "getset", + "group", + "hex", + "incrementalmerkletree", + "jubjub", + "lazy_static", + "memuse", + "rand", + "rand_core", + "redjubjub", + "subtle", + "tracing", + "zcash_note_encryption", + "zcash_spec", + "zip32", +] + +[[package]] +name = "satoshi-bridge" +version = "0.7.5" dependencies = [ "bitcoin", + "bs58 0.5.1", + "core2", "crypto-shared", "ed25519-dalek", "getrandom 0.2.16", @@ -2941,7 +3775,15 @@ dependencies = [ "near-plugins", "near-sdk", "near-workspaces", + "omni-utils", + "orchard", + "rand", + "sapling-crypto", "tokio", + "zcash_address", + "zcash_primitives", + "zcash_protocol", + "zcash_transparent", ] [[package]] @@ -2965,6 +3807,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -2974,7 +3840,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3005,7 +3871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" dependencies = [ "rand", - "secp256k1-sys 0.8.1", + "secp256k1-sys 0.8.2", ] [[package]] @@ -3021,9 +3887,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" dependencies = [ "cc", ] @@ -3043,7 +3909,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -3086,7 +3952,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3097,15 +3963,16 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ + "indexmap 2.11.0", "itoa", "memchr", "ryu", @@ -3120,7 +3987,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3137,15 +4004,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.11.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -3155,14 +4024,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3171,7 +4040,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.11.0", "itoa", "ryu", "serde", @@ -3194,9 +4063,9 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3205,9 +4074,20 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" +dependencies = [ + "cfg-if 1.0.3", "cpufeatures", - "digest", + "digest 0.11.0-pre.9", ] [[package]] @@ -3216,7 +4096,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -3228,9 +4108,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -3241,10 +4121,21 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core", ] +[[package]] +name = "sinsemilla" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" +dependencies = [ + "group", + "pasta_curves", + "subtle", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -3253,12 +4144,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -3274,17 +4162,17 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3365,7 +4253,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3387,9 +4275,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.102" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -3413,7 +4301,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3422,7 +4310,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation", "system-configuration-sys", ] @@ -3456,15 +4344,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3478,11 +4366,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -3493,18 +4381,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3518,12 +4406,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde", @@ -3533,15 +4420,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3559,9 +4446,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3574,20 +4461,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3598,7 +4487,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3634,9 +4523,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -3666,7 +4555,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.11.0", "toml_datetime", "winnow", ] @@ -3692,7 +4581,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes", "futures-util", "http", @@ -3729,13 +4618,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -3777,6 +4666,22 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.6", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3817,9 +4722,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -3845,6 +4750,17 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "want" version = "0.3.1" @@ -3862,46 +4778,47 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ - "cfg-if 1.0.1", + "cfg-if 1.0.3", "js-sys", "once_cell", "wasm-bindgen", @@ -3910,9 +4827,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3920,31 +4837,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -3956,14 +4873,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.2", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -4023,7 +4940,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -4034,20 +4951,20 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link", "windows-result", @@ -4078,7 +4995,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -4087,7 +5004,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", ] [[package]] @@ -4096,14 +5022,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -4112,65 +5055,110 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" [[package]] name = "writeable" @@ -4189,9 +5177,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", "rustix", @@ -4217,28 +5205,151 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", "synstructure", ] +[[package]] +name = "zcash_address" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c984ae01367a4a3d20e9d34ae4e4cc0dca004b22d9a10a51eec43f43934612e" +dependencies = [ + "bech32", + "bs58 0.5.1", + "core2", + "f4jumble", + "zcash_encoding", + "zcash_protocol", +] + +[[package]] +name = "zcash_encoding" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca38087e6524e5f51a5b0fb3fc18f36d7b84bf67b2056f494ca0c281590953d" +dependencies = [ + "core2", + "nonempty", +] + +[[package]] +name = "zcash_note_encryption" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77efec759c3798b6e4d829fcc762070d9b229b0f13338c40bf993b7b609c2272" +dependencies = [ + "chacha20", + "chacha20poly1305", + "cipher", + "rand_core", + "subtle", +] + +[[package]] +name = "zcash_primitives" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0c26140f2e6b760dcf052d22bd01f1a8773cdefb650ff5829430798a21b85b" +dependencies = [ + "bip32", + "blake2b_simd", + "block-buffer 0.11.0-rc.3", + "bs58 0.5.1", + "core2", + "crypto-common 0.2.0-rc.1", + "equihash", + "ff", + "fpe", + "getset", + "group", + "hex", + "incrementalmerkletree", + "jubjub", + "memuse", + "nonempty", + "orchard", + "rand", + "rand_core", + "redjubjub", + "ripemd 0.1.3", + "sapling-crypto", + "secp256k1 0.29.1", + "sha2 0.10.9", + "subtle", + "tracing", + "zcash_address", + "zcash_encoding", + "zcash_note_encryption", + "zcash_protocol", + "zcash_spec", + "zcash_transparent", + "zip32", +] + +[[package]] +name = "zcash_protocol" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cfe9e3fb08e6851efe3d0ced457e4cb2c305daa928f64cb0d70c040f8f8336" +dependencies = [ + "core2", + "document-features", + "hex", + "memuse", +] + +[[package]] +name = "zcash_spec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded3f58b93486aa79b85acba1001f5298f27a46489859934954d262533ee2915" +dependencies = [ + "blake2b_simd", +] + +[[package]] +name = "zcash_transparent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a7c162a8aa6f708e842503ed5157032465dadfb1d7f63adf9db2d45213a0b11" +dependencies = [ + "bip32", + "blake2b_simd", + "bs58 0.5.1", + "core2", + "document-features", + "getset", + "hex", + "ripemd 0.1.3", + "secp256k1 0.29.1", + "sha2 0.10.9", + "subtle", + "zcash_address", + "zcash_encoding", + "zcash_protocol", + "zcash_spec", + "zip32", +] + [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -4258,7 +5369,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", "synstructure", ] @@ -4279,7 +5390,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -4308,9 +5419,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -4325,7 +5436,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.106", ] [[package]] @@ -4337,17 +5448,29 @@ dependencies = [ "aes", "byteorder", "bzip2", - "constant_time_eq", + "constant_time_eq 0.1.5", "crc32fast", "crossbeam-utils", "flate2", - "hmac", + "hmac 0.12.1", "pbkdf2", "sha1", "time", "zstd 0.11.2+zstd.1.5.2", ] +[[package]] +name = "zip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ff9ea444cdbce820211f91e6aa3d3a56bde7202d3c0961b7c38f793abf5637" +dependencies = [ + "blake2b_simd", + "memuse", + "subtle", + "zcash_spec", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -4387,9 +5510,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index e331affa..da22ecba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,14 +7,15 @@ members = [ [workspace.package] edition = "2021" publish = false -repository = "https://github.com/Near-Bridge-Lab/btc-bridge" +repository = "https://github.com/Near-One/btc-bridge" [workspace.dependencies] -near-sdk = { version = "5.14.0", features = ["unstable", "unit-testing"] } -near-contract-standards = "5.14.0" +near-sdk = { version = "=5.24.1", features = ["unstable", "unit-testing"] } +near-contract-standards = "=5.24.1" hex="0.4.3" bitcoin = { version = "0.32.0", default-features = false, features = ["serde"] } near-plugins = { git = "https://github.com/aurora-is-near/near-plugins", tag = "v0.4.1" } +omni-utils = { git = "https://github.com/near-one/omni-utils", rev = "f9bc3ea4a72e97f02660b8f1c0b2f79a9fbde3a4" } [profile.release] codegen-units = 1 @@ -22,4 +23,4 @@ opt-level = "z" lto = true debug = false panic = "abort" -overflow-checks = true \ No newline at end of file +overflow-checks = true diff --git a/Makefile b/Makefile index bf88113f..f8dd2a50 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,48 @@ +MAKEFILE_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +#INT_OPTIONS = -D warnings -D clippy::pedantic -A clippy::must_use_candidate -A clippy::used_underscore_binding -A clippy::needless_range_loop //TODO: enable it later +BRIDGE_MANIFEST := $(MAKEFILE_DIR)/contracts/satoshi-bridge/Cargo.toml + RFLAGS="-C link-arg=-s" -build: lint satoshi-bridge nbtc mock-chain-signatures mock-btc-light-client mock-dapp +FEATURES = bitcoin zcash + +release: $(addprefix build-,$(FEATURES)) + $(call build_release_wasm,nbtc,nbtc) + +build-local: $(addprefix build-local-,$(FEATURES)) nbtc mock-chain-signatures mock-btc-light-client mock-dapp -lint: +lint: $(addprefix clippy-,$(FEATURES)) $(addprefix fmt-,$(FEATURES)) @cargo fmt --all - @cargo clippy --fix --allow-dirty --allow-staged + @cargo clippy -- $(LINT_OPTIONS) -satoshi-bridge: contracts/satoshi-bridge - $(call local_build_wasm,satoshi-bridge,satoshi_bridge) +test: build-local $(addprefix test-,$(FEATURES)) + +$(foreach feature,$(FEATURES), \ + $(eval build-$(feature): ; \ + cargo near build reproducible-wasm --variant "$(feature)" --manifest-path $(BRIDGE_MANIFEST) && \ + mkdir -p res && mv ./target/near/satoshi_bridge/satoshi_bridge.wasm ./res/$(feature)_bridge_release.wasm \ + ) \ +) + +$(foreach feature,$(FEATURES), \ + $(eval build-local-$(feature): ; \ + cargo near build non-reproducible-wasm --features "$(feature)" --manifest-path $(BRIDGE_MANIFEST) --no-abi && \ + mkdir -p res && mv ./target/near/satoshi_bridge/satoshi_bridge.wasm ./res/$(feature)_bridge.wasm \ + ) \ +) + +$(foreach feature,$(FEATURES), \ + $(eval clippy-$(feature): ; cargo clippy --no-default-features --features "$(feature)" --manifest-path $(BRIDGE_MANIFEST) -- $(LINT_OPTIONS)) \ +) + +$(foreach feature,$(FEATURES), \ + $(eval fmt-$(feature): ; cargo fmt --all --check --manifest-path $(BRIDGE_MANIFEST)) \ +) + +$(foreach feature,$(FEATURES), \ + $(eval test-$(feature): ; cargo test --no-default-features --features "$(feature)" --manifest-path $(BRIDGE_MANIFEST)) \ +) -nbtc: contracts/nbtc - $(call local_build_wasm,nbtc,nbtc) mock-dapp: contracts/mock-dapp $(call local_build_wasm,mock-dapp,mock_dapp) @@ -21,18 +53,9 @@ mock-chain-signatures: contracts/mock-chain-signatures mock-btc-light-client: contracts/mock-btc-light-client $(call local_build_wasm,mock-btc-light-client,mock_btc_light_client) -count: - @tokei ./contracts/satoshi-bridge/src/ --files --exclude unit - @tokei ./contracts/nbtc/src/ --files - -release: - $(call build_release_wasm,satoshi-bridge,satoshi_bridge) - $(call build_release_wasm,nbtc,nbtc) - -clean: - cargo clean - rm -rf res/ - +nbtc: contracts/nbtc + $(call local_build_wasm,nbtc,nbtc) + define local_build_wasm $(eval PACKAGE_NAME := $(1)) $(eval WASM_NAME := $(2)) @@ -51,4 +74,18 @@ define build_release_wasm @rustup target add wasm32-unknown-unknown @cargo near build reproducible-wasm --manifest-path ./contracts/${PACKAGE_NAME}/Cargo.toml @cp target/near/${WASM_NAME}/$(WASM_NAME).wasm ./res/$(WASM_NAME)_release.wasm -endef \ No newline at end of file +endef + +define build_release_zcash_wasm + @mkdir -p res + @rustup target add wasm32-unknown-unknown + @cargo near build reproducible-wasm --manifest-path ./contracts/satoshi-bridge/Cargo.toml --variant zcash + @cp target/near/satoshi_bridge/satoshi_bridge.wasm ./res/zcash_connector_release.wasm +endef + +define local_build_zcash_wasm + @mkdir -p res + @rustup target add wasm32-unknown-unknown + @cargo near build non-reproducible-wasm --manifest-path ./contracts/satoshi-bridge/Cargo.toml --locked --no-abi --no-default-features --features zcash + @cp target/near/satoshi_bridge/satoshi_bridge.wasm ./res/zcash.wasm +endef diff --git a/contracts/mock-btc-light-client/src/lib.rs b/contracts/mock-btc-light-client/src/lib.rs index 6b03fc5e..c7984e0e 100644 --- a/contracts/mock-btc-light-client/src/lib.rs +++ b/contracts/mock-btc-light-client/src/lib.rs @@ -81,4 +81,9 @@ impl Contract { pub fn verify_transaction_inclusion(&self, #[serializer(borsh)] args: ProofArgs) -> bool { true } + + pub fn get_last_block_height(&self) -> u32 { + // Return a reasonable mock block height for Zcash testnet + 1000 + } } diff --git a/contracts/mock-dapp/src/account.rs b/contracts/mock-dapp/src/account.rs index 6a16a191..3adbde3d 100644 --- a/contracts/mock-dapp/src/account.rs +++ b/contracts/mock-dapp/src/account.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{near, AccountId}; #[near(serializers = [borsh, json])] pub struct Account { diff --git a/contracts/mock-dapp/src/fungible_token.rs b/contracts/mock-dapp/src/fungible_token.rs index 02fd9737..3c85e128 100644 --- a/contracts/mock-dapp/src/fungible_token.rs +++ b/contracts/mock-dapp/src/fungible_token.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{env, near, AccountId, Contract, ContractExt, PromiseOrValue, U128}; use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; use near_sdk::{log, require, serde_json}; diff --git a/contracts/mock-dapp/src/lib.rs b/contracts/mock-dapp/src/lib.rs index d7621dee..0c201aa6 100644 --- a/contracts/mock-dapp/src/lib.rs +++ b/contracts/mock-dapp/src/lib.rs @@ -7,7 +7,7 @@ mod account; mod fungible_token; mod storage; -use account::*; +use account::Account; #[derive(PanicOnDefault)] #[near(contract_state)] diff --git a/contracts/mock-dapp/src/storage.rs b/contracts/mock-dapp/src/storage.rs index 9d7f235c..0b9670a4 100644 --- a/contracts/mock-dapp/src/storage.rs +++ b/contracts/mock-dapp/src/storage.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{env, near, Account, AccountId, Contract, ContractExt, NearToken, Promise}; use near_contract_standards::storage_management::{ StorageBalance, StorageBalanceBounds, StorageManagement, @@ -17,7 +17,9 @@ impl StorageManagement for Contract { let registration_only = registration_only.unwrap_or(false); if let Some(account) = self.accounts.get_mut(&account_id) { if registration_only && !amount.is_zero() { - Promise::new(env::predecessor_account_id()).transfer(amount); + Promise::new(env::predecessor_account_id()) + .transfer(amount) + .detach(); } else { account.deposit += amount.as_yoctonear(); } @@ -31,7 +33,8 @@ impl StorageManagement for Contract { let refund = amount.as_yoctonear() - min_balance.as_yoctonear(); if refund > 0 { Promise::new(env::predecessor_account_id()) - .transfer(NearToken::from_yoctonear(refund)); + .transfer(NearToken::from_yoctonear(refund)) + .detach(); } min_balance.as_yoctonear() } else { diff --git a/contracts/nbtc/Cargo.toml b/contracts/nbtc/Cargo.toml index 8f7de575..84ce633b 100644 --- a/contracts/nbtc/Cargo.toml +++ b/contracts/nbtc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nbtc" -version = "0.1.0" +version = "0.3.0" edition.workspace = true publish.workspace = true repository.workspace = true diff --git a/contracts/nbtc/src/lib.rs b/contracts/nbtc/src/lib.rs index 1ac578cb..5e502f13 100644 --- a/contracts/nbtc/src/lib.rs +++ b/contracts/nbtc/src/lib.rs @@ -26,8 +26,6 @@ pub struct Contract { metadata: LazyOption, } -const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg%20width%3D%2232%22%20height%3D%2232%22%20viewBox%3D%220%200%2032%2032%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20clip-path%3D%22url(%23clip0_2351_779)%22%3E%3Cpath%20d%3D%22M16%2032C24.8366%2032%2032%2024.8366%2032%2016C32%207.16344%2024.8366%200%2016%200C7.16344%200%200%207.16344%200%2016C0%2024.8366%207.16344%2032%2016%2032Z%22%20fill%3D%22%2300E99F%22%2F%3E%3Cpath%20d%3D%22M16.0006%2028.2858C22.7858%2028.2858%2028.2863%2022.7853%2028.2863%2016.0001C28.2863%209.21486%2022.7858%203.71436%2016.0006%203.71436C9.21535%203.71436%203.71484%209.21486%203.71484%2016.0001C3.71484%2022.7853%209.21535%2028.2858%2016.0006%2028.2858Z%22%20stroke%3D%22black%22%2F%3E%3Cpath%20d%3D%22M27.1412%2016C27.1412%2022.1541%2022.1524%2027.1429%2015.9983%2027.1429C9.84427%2027.1429%204.85547%2022.1541%204.85547%2016C4.85547%209.84598%209.84427%204.85718%2015.9983%204.85718C22.1524%204.85718%2027.1412%209.84598%2027.1412%2016Z%22%20stroke%3D%22black%22%20stroke-width%3D%220.5%22%2F%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M16.2167%2011.1743C15.9198%2011.1643%2015.6095%2011.1622%2015.2868%2011.1668V9.32056H13.8907V11.2217C13.1583%2011.2659%2012.3792%2011.3332%2011.5625%2011.4149V12.811H12.9586V18.8607H11.7952V20.4895H13.8893V22.5836H15.2854V20.4895H16.2161V22.5836H17.3795V20.4895C18.4654%2020.4119%2020.6836%2019.7915%2020.8698%2017.93C21.0559%2016.0686%2019.7064%2015.6032%2019.0083%2015.6032C19.5512%2015.3705%2020.544%2014.5328%2020.1717%2013.0436C19.9215%2012.043%2019.0072%2011.5204%2017.6128%2011.2984V9.32164H16.2167V11.1743ZM18.0737%2013.9723C18.0737%2012.8554%2016.2122%2012.7313%2015.2815%2012.8088V15.1356C16.2122%2015.2132%2018.0737%2015.0891%2018.0737%2013.9723ZM15.2826%2016.5322V18.8591C16.2133%2018.9366%2018.3075%2018.859%2018.3075%2017.6956C18.3075%2016.2994%2016.2133%2016.4547%2015.2826%2016.5322Z%22%20fill%3D%22black%22%2F%3E%3C%2Fg%3E%3Cdefs%3E%3CclipPath%20id%3D%22clip0_2351_779%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22white%22%2F%3E%3C%2FclipPath%3E%3C%2Fdefs%3E%3C%2Fsvg%3E"; - const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas::from_tgas(5); const GAS_FOR_FT_TRANSFER_CALL: Gas = Gas::from_tgas(30); @@ -53,9 +51,16 @@ pub struct PostAction { #[near] impl Contract { #[init] - pub fn new(controller: AccountId, bridge_id: AccountId) -> Self { + pub fn new( + controller: AccountId, + bridge_id: AccountId, + name: String, + symbol: String, + icon: Option, + decimals: u8, + ) -> Self { require!(!env::state_exists(), "Already initialized"); - Self { + let mut contract = Self { controller, bridge_id, token: FungibleToken::new(StorageKey::FungibleToken), @@ -63,15 +68,21 @@ impl Contract { StorageKey::Metadata, Some(&FungibleTokenMetadata { spec: FT_METADATA_SPEC.to_string(), - name: "Near WTC".to_string(), - symbol: "NBTC".to_string(), - icon: Some(DATA_IMAGE_SVG_NEAR_ICON.to_string()), + name, + symbol, + icon, reference: None, reference_hash: None, - decimals: 8, + decimals, }), ), - } + }; + + contract + .token + .internal_register_account(&contract.bridge_id); + + contract } #[payable] @@ -81,6 +92,29 @@ impl Contract { self.controller = controller; } + #[payable] + pub fn safe_mint( + &mut self, + account_id: AccountId, + amount: U128, + msg: Option, + ) -> PromiseOrValue { + self.assert_bridge(); + + if self.token.accounts.get(&account_id).is_none() { + return PromiseOrValue::Value(U128(0)); + } + + if let Some(msg) = msg { + self.token.internal_deposit(&self.bridge_id, amount.into()); + + self.ft_transfer_call(account_id, amount, None, msg) + } else { + self.token.internal_deposit(&account_id, amount.into()); + PromiseOrValue::Value(amount) + } + } + pub fn mint( &mut self, mint_account_id: AccountId, @@ -99,7 +133,9 @@ impl Contract { self.mint_inner(&relayer_account_id, relayer_fee); } if let Some(post_actions) = post_actions { - Self::ext(env::current_account_id()).handle_post_actions(mint_account_id, post_actions); + Self::ext(env::current_account_id()) + .handle_post_actions(mint_account_id, post_actions) + .detach(); } } @@ -137,7 +173,7 @@ impl Contract { impl FungibleTokenCore for Contract { #[payable] fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { - self.token.ft_transfer(receiver_id, amount, memo) + self.token.ft_transfer(receiver_id, amount, memo); } #[payable] @@ -340,15 +376,12 @@ impl Contract { Self::ext(env::current_account_id()) .with_static_gas(gas) .handle_post_action(sender_id.clone(), receiver_id, amount, memo, msg) + .detach(); } else { - Self::ext(env::current_account_id()).handle_post_action( - sender_id.clone(), - receiver_id, - amount, - memo, - msg, - ) - }; + Self::ext(env::current_account_id()) + .handle_post_action(sender_id.clone(), receiver_id, amount, memo, msg) + .detach(); + } } } @@ -379,6 +412,7 @@ impl Contract { ext_ft_resolver::ext(env::current_account_id()) .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) .ft_resolve_transfer(sender_id, receiver_id, amount.into()), - ); + ) + .detach(); } } diff --git a/contracts/satoshi-bridge/Cargo.toml b/contracts/satoshi-bridge/Cargo.toml index 97f38c85..3acc25a3 100644 --- a/contracts/satoshi-bridge/Cargo.toml +++ b/contracts/satoshi-bridge/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "satoshi-bridge" -version = "0.5.0" +version = "0.7.5" edition.workspace = true publish.workspace = true repository.workspace = true @@ -21,15 +21,29 @@ container_build_command = [ "--no-abi", ] +[package.metadata.near.reproducible_build.variant.bitcoin] +container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "--locked", "--no-abi", "--no-default-features", "--features", "bitcoin"] + +[package.metadata.near.reproducible_build.variant.zcash] +container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "--locked", "--no-abi", "--no-default-features", "--features", "zcash"] + [dependencies] near-sdk.workspace = true near-contract-standards.workspace = true bitcoin.workspace = true hex.workspace = true near-plugins.workspace = true +omni-utils.workspace = true k256 = "0.13.1" -ed25519-dalek="2.1.0" -crypto-shared = { git = "https://github.com/near/mpc_old", rev = "0afee9004a1b1c3386940e60c28cff7cf1b5978c"} +ed25519-dalek = "2.1.0" +crypto-shared = { git = "https://github.com/near/mpc_old", rev = "0afee9004a1b1c3386940e60c28cff7cf1b5978c" } +zcash_primitives = { version = "0.24.0", default-features = false, features = ["circuits", "transparent-inputs"], optional = true } +zcash_transparent = { version = "0.4.0", features = ["transparent-inputs"], optional = true } +orchard = { version = "0.11.0", default-features = false, optional = true } +sapling-crypto = { version = "0.5.0", default-features = false, optional = true } +zcash_protocol = { version = "0.6.1" } +core2 = { version = "0.3", optional = true } +zcash_address = { version = "0.9.0" } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.12", features = ["custom"] } @@ -37,3 +51,11 @@ getrandom = { version = "0.2.12", features = ["custom"] } [dev-dependencies] near-workspaces = { version = "0.20", features = ["unstable"] } tokio = { version = "1.12.0", features = ["full"] } +rand = "0.8" +hex = "0.4" +bs58 = "0.5" + +[features] +default = [] +zcash = ["zcash_primitives", "zcash_transparent", "orchard", "core2", "sapling-crypto"] +bitcoin = [] diff --git a/contracts/satoshi-bridge/src/account.rs b/contracts/satoshi-bridge/src/account.rs index d795da3d..6fc2cef8 100644 --- a/contracts/satoshi-bridge/src/account.rs +++ b/contracts/satoshi-bridge/src/account.rs @@ -1,4 +1,5 @@ -use crate::*; +use crate::{near, u128_dec_format, AccountId, Contract}; +use near_sdk::env; use std::collections::HashSet; #[near(serializers = [borsh, json])] @@ -78,7 +79,7 @@ impl Contract { } pub fn internal_get_account(&self, account_id: &AccountId) -> Option<&Account> { - self.data().accounts.get(account_id).map(|o| o.into()) + self.data().accounts.get(account_id).map(Into::into) } pub fn internal_unwrap_account(&self, account_id: &AccountId) -> &Account { @@ -86,7 +87,9 @@ impl Contract { .accounts .get(account_id) .map(|o| o.into()) - .expect("ACCOUNT NOT REGISTERED") + .unwrap_or_else(|| { + env::panic_str(&format!("ERR_ACCOUNT_NOT_REGISTERED: {}", account_id)) + }) } pub fn internal_unwrap_mut_account(&mut self, account_id: &AccountId) -> &mut Account { @@ -94,7 +97,9 @@ impl Contract { .accounts .get_mut(account_id) .map(|o| o.into()) - .expect("ACCOUNT NOT REGISTERED") + .unwrap_or_else(|| { + env::panic_str(&format!("ERR_ACCOUNT_NOT_REGISTERED: {}", account_id)) + }) } pub fn internal_unwrap_or_create_mut_account( diff --git a/contracts/satoshi-bridge/src/api/bridge.rs b/contracts/satoshi-bridge/src/api/bridge.rs index 683807a3..1e0dc9f0 100644 --- a/contracts/satoshi-bridge/src/api/bridge.rs +++ b/contracts/satoshi-bridge/src/api/bridge.rs @@ -1,6 +1,8 @@ +use crate::psbt_wrapper::PsbtWrapper; use crate::*; use near_plugins::{access_control_any, pause}; +#[trusted_relayer] #[near] impl Contract { /// Verify that the user has transferred BTC asset to the protocol's designated BTC deposit account, and mint NBTC to the user's NEAR account. @@ -17,6 +19,7 @@ impl Contract { /// # Returns /// /// bool - Whether nBTC minting was successful. + #[trusted_relayer] #[pause(except(roles(Role::DAO)))] pub fn verify_deposit( &mut self, @@ -27,18 +30,25 @@ impl Contract { tx_index: u64, merkle_proof: Vec, ) -> Promise { + require!( + deposit_msg.safe_deposit.is_none(), + "safe_deposit not supported in verify_deposit" + ); let path = get_deposit_path(&deposit_msg); - let transaction = bytes_to_btc_transaction(&tx_bytes); - let deposit_amount = transaction.output[vout].value.to_sat() as u128; + let transaction = WrappedTransaction::decode(&tx_bytes, &self.internal_config().chain) + .expect("Deserialization tx_bytes failed"); + let deposit_amount = u128::from(transaction.output()[vout].value.to_sat()); require!(deposit_amount > 0, "Invalid deposit_amount"); require!( - transaction.lock_time == LockTime::ZERO, + transaction.lock_time() == LockTime::ZERO, "Tx with a non-zero lock_time are not supported." ); - let deposit_address = self.generate_btc_p2wpkh_address(&path); - let deposit_address_script_pubkey = deposit_address.script_pubkey(); + let deposit_address = self.generate_utxo_chain_address(&path); + let deposit_address_script_pubkey = deposit_address + .script_pubkey() + .expect("Invalid deposit address"); require!( - deposit_address_script_pubkey == transaction.output[vout].script_pubkey, + deposit_address_script_pubkey == transaction.output()[vout].script_pubkey, "Invalid deposit tx_bytes" ); @@ -46,10 +56,13 @@ impl Contract { path, tx_bytes, vout, - balance: transaction.output[vout].value.to_sat(), + balance: transaction.output()[vout].value.to_sat(), }; let tx_id = transaction.compute_txid().to_string(); - let utxo_storage_key = generate_utxo_storage_key(tx_id.clone(), vout as u32); + let utxo_storage_key = generate_utxo_storage_key( + tx_id.clone(), + u32::try_from(vout).unwrap_or_else(|_| env::panic_str("vout overflow")), + ); self.internal_verify_deposit( deposit_amount, tx_block_blockhash, @@ -64,6 +77,84 @@ impl Contract { ) } + /// Safe version of verify_deposit, only supports minting nBTC with safe_deposit message and revert the deposit on failed XCC calls. + /// It doesn't charge deposit fee, and doesn't pay the token storage for the user + /// + /// # Arguments + /// + /// * `deposit_msg` - Information used to generate the deposit address path. + /// * `tx_bytes` - Successfully confirmed BTC transaction bytes + /// * `vout` - The index of the output where the user sent BTC to the deposit address + /// * `tx_block_blockhash` - The block hash where the transaction is located. + /// * `tx_index` - The index of the transaction in the block. + /// * `merkle_proof` - Merkle proof of the transaction. + /// + /// # Returns + /// + /// bool - Whether nBTC minting was successful. + #[payable] + #[trusted_relayer] + #[pause(except(roles(Role::DAO)))] + pub fn safe_verify_deposit( + &mut self, + deposit_msg: DepositMsg, + tx_bytes: Vec, + vout: usize, + tx_block_blockhash: String, + tx_index: u64, + merkle_proof: Vec, + ) -> Promise { + require!( + env::attached_deposit() >= self.required_balance_for_safe_deposit(), + "Insufficient deposit for storage" + ); + + let path = get_deposit_path(&deposit_msg); + let safe_deposit_msg = deposit_msg + .safe_deposit + .unwrap_or_else(|| env::panic_str("safe_deposit is required in safe_verify_deposit")); + + let transaction = WrappedTransaction::decode(&tx_bytes, &self.internal_config().chain) + .expect("Deserialization tx_bytes failed"); + let deposit_amount = transaction.output()[vout].value.to_sat().into(); + require!(deposit_amount > 0, "Invalid deposit_amount"); + require!( + transaction.lock_time() == LockTime::ZERO, + "Tx with a non-zero lock_time are not supported." + ); + let deposit_address = self.generate_utxo_chain_address(&path); + let deposit_address_script_pubkey = deposit_address + .script_pubkey() + .expect("Invalid deposit address"); + require!( + deposit_address_script_pubkey == transaction.output()[vout].script_pubkey, + "Invalid deposit tx_bytes" + ); + + let utxo = UTXO { + path, + tx_bytes, + vout, + balance: transaction.output()[vout].value.to_sat(), + }; + let tx_id = transaction.compute_txid().to_string(); + let utxo_storage_key = generate_utxo_storage_key(tx_id.clone(), vout.try_into().unwrap()); + + self.internal_safe_verify_deposit( + deposit_amount, + tx_block_blockhash, + tx_index, + merkle_proof, + PendingUTXOInfo { + tx_id, + utxo_storage_key, + utxo, + }, + deposit_msg.recipient_id, + safe_deposit_msg, + ) + } + /// Verify that the user’s withdrawal has been successful, and then burn the corresponding amount of tokens. /// /// # Arguments @@ -76,6 +167,7 @@ impl Contract { /// # Returns /// /// bool - Whether nBTC burning was successful. + #[trusted_relayer] #[pause(except(roles(Role::DAO)))] pub fn verify_withdraw( &mut self, @@ -112,7 +204,12 @@ impl Contract { /// * `original_btc_pending_verify_id` - Pending verify ID of the original transaction. /// * `output` - Modified output. #[pause(except(roles(Role::DAO)))] - pub fn withdraw_rbf(&mut self, original_btc_pending_verify_id: String, output: Vec) { + pub fn withdraw_rbf( + &mut self, + original_btc_pending_verify_id: String, + output: Vec, + chain_specific_data: Option, + ) { let account_id = env::predecessor_account_id(); require!( self.internal_unwrap_account(&account_id) @@ -120,15 +217,13 @@ impl Contract { .is_none(), "Previous btc tx has not been signed" ); - let btc_pending_id = - self.internal_withdraw_rbf(&account_id, original_btc_pending_verify_id, output); - self.internal_unwrap_mut_account(&account_id) - .btc_pending_sign_id = Some(btc_pending_id.clone()); - Event::GenerateBtcPendingInfo { - account_id: &account_id, - btc_pending_id: &btc_pending_id, - } - .emit(); + + self.withdraw_rbf_chain_specific( + account_id, + original_btc_pending_verify_id, + output, + chain_specific_data, + ); } /// If the user's Withdraw is not verified within a certain time, the protocol can actively cancel the Withdraw through RBF, with the gas fee borne by the user. @@ -152,14 +247,13 @@ impl Contract { .is_none(), "Assisted user previous btc tx has not been signed" ); - let btc_pending_id = self.internal_cancel_withdraw(original_btc_pending_verify_id, output); - self.internal_unwrap_mut_account(&user_account_id) - .btc_pending_sign_id = Some(btc_pending_id.clone()); - Event::GenerateBtcPendingInfo { - account_id: &user_account_id, - btc_pending_id: &btc_pending_id, - } - .emit(); + + self.cancel_withdraw_chain_specific( + user_account_id, + original_btc_pending_verify_id, + output, + None, + ); } /// Verify that the active utxo management has been successful, and then burn the corresponding amount of tokens. @@ -174,6 +268,7 @@ impl Contract { /// # Returns /// /// bool - Whether nBTC burning was successful. + #[trusted_relayer] #[pause(except(roles(Role::DAO)))] pub fn verify_active_utxo_management( &mut self, @@ -215,60 +310,7 @@ impl Contract { pub fn active_utxo_management(&mut self, input: Vec, output: Vec) { assert_one_yocto(); let account_id = env::predecessor_account_id(); - let account = self.internal_unwrap_account(&account_id); - require!( - account.btc_pending_sign_id.is_none(), - "Previous btc tx has not been signed" - ); - let (psbt, utxo_storage_keys, vutxos) = self.generate_psbt_and_vutxos(input, output); - let (actual_received_amount, gas_fee) = - self.check_active_management_psbt_valid(&psbt, &vutxos); - require!( - gas_fee <= self.data().cur_available_protocol_fee, - "Insufficient protocol_fee" - ); - self.data_mut().cur_available_protocol_fee -= gas_fee; - self.data_mut().cur_reserved_protocol_fee += gas_fee; - - let need_signature_num = psbt.unsigned_tx.input.len(); - let psbt_hex = psbt.serialize_hex(); - let btc_pending_id = psbt.extract_tx().unwrap().compute_txid().to_string(); - let btc_pending_info = BTCPendingInfo { - account_id: account_id.clone(), - btc_pending_id: btc_pending_id.clone(), - transfer_amount: 0, - actual_received_amount, - withdraw_fee: 0, - gas_fee, - burn_amount: gas_fee, - psbt_hex, - vutxos, - signatures: vec![None; need_signature_num], - tx_bytes_with_sign: None, - create_time_sec: nano_to_sec(env::block_timestamp()), - last_sign_time_sec: 0, - state: PendingInfoState::ActiveUtxoManagementOriginal(OriginalState { - stage: PendingInfoStage::PendingSign, - max_gas_fee: gas_fee, - last_rbf_time_sec: None, - cancel_rbf_reserved: None, - }), - }; - require!( - self.data_mut() - .btc_pending_infos - .insert(btc_pending_id.clone(), btc_pending_info.into()) - .is_none(), - "pending info already exist" - ); - self.internal_unwrap_mut_account(&account_id) - .btc_pending_sign_id = Some(btc_pending_id.clone()); - Event::UtxoRemoved { utxo_storage_keys }.emit(); - Event::GenerateBtcPendingInfo { - account_id: &account_id, - btc_pending_id: &btc_pending_id, - } - .emit(); + self.active_utxo_management_chain_specific(account_id, input, output); } /// The initiator of active UTXO management accelerates the transaction by increasing the gas fee. @@ -293,18 +335,12 @@ impl Contract { .is_none(), "Previous btc tx has not been signed" ); - let btc_pending_id = self.internal_active_utxo_management_rbf( - &account_id, + self.active_utxo_management_rbf_chain_specific( + account_id, original_btc_pending_verify_id, output, + None, ); - self.internal_unwrap_mut_account(&account_id) - .btc_pending_sign_id = Some(btc_pending_id.clone()); - Event::GenerateBtcPendingInfo { - account_id: &account_id, - btc_pending_id: &btc_pending_id, - } - .emit(); } /// Active UTXO management transactions that have not been verified for a long time are allowed to be canceled through RBF. @@ -332,15 +368,12 @@ impl Contract { .is_none(), "Assisted user previous btc tx has not been signed" ); - let btc_pending_id = - self.internal_cancel_active_utxo_management(original_btc_pending_verify_id, output); - self.internal_unwrap_mut_account(&user_account_id) - .btc_pending_sign_id = Some(btc_pending_id.clone()); - Event::GenerateBtcPendingInfo { - account_id: &user_account_id, - btc_pending_id: &btc_pending_id, - } - .emit(); + self.cancel_active_utxo_management_chain_specific( + user_account_id, + original_btc_pending_verify_id, + output, + None, + ); } /// Since there can be many RBFs, removing all RBF pending info at once after verifying the transaction on-chain might not have enough gas. @@ -377,7 +410,7 @@ impl Contract { pub fn get_user_deposit_address(&self, deposit_msg: DepositMsg) -> String { let path = get_deposit_path(&deposit_msg); - let deposit_address = self.generate_btc_p2wpkh_address(&path).to_string(); + let deposit_address = self.generate_utxo_chain_address(&path).to_string(); Event::LogDepositAddress { deposit_msg, path, @@ -392,3 +425,67 @@ impl Contract { config.change_address.clone() } } + +impl Contract { + pub fn create_active_utxo_management_pending_info( + &mut self, + account_id: AccountId, + mut psbt: PsbtWrapper, + ) { + let account = self.internal_unwrap_account(&account_id); + require!( + account.btc_pending_sign_id.is_none(), + "Previous btc tx has not been signed" + ); + + let (utxo_storage_keys, vutxos) = self.generate_vutxos(&mut psbt); + let (actual_received_amount, gas_fee) = + self.check_active_management_psbt_valid(&psbt, &vutxos); + require!( + gas_fee <= self.data().cur_available_protocol_fee, + "Insufficient protocol_fee" + ); + self.data_mut().cur_available_protocol_fee -= gas_fee; + self.data_mut().cur_reserved_protocol_fee += gas_fee; + + let need_signature_num = psbt.get_input_num(); + let psbt_hex = psbt.serialize(); + let btc_pending_id = psbt.get_pending_id(); + let btc_pending_info = BTCPendingInfo { + account_id: account_id.clone(), + btc_pending_id: btc_pending_id.clone(), + transfer_amount: 0, + actual_received_amount, + withdraw_fee: 0, + gas_fee, + burn_amount: gas_fee, + psbt_hex, + vutxos, + signatures: vec![None; need_signature_num], + tx_bytes_with_sign: None, + create_time_sec: nano_to_sec(env::block_timestamp()), + last_sign_time_sec: 0, + state: PendingInfoState::ActiveUtxoManagementOriginal(OriginalState { + stage: PendingInfoStage::PendingSign, + max_gas_fee: gas_fee, + last_rbf_time_sec: None, + cancel_rbf_reserved: None, + }), + }; + require!( + self.data_mut() + .btc_pending_infos + .insert(btc_pending_id.clone(), btc_pending_info.into()) + .is_none(), + "pending info already exist" + ); + self.internal_unwrap_mut_account(&account_id) + .btc_pending_sign_id = Some(btc_pending_id.clone()); + Event::UtxoRemoved { utxo_storage_keys }.emit(); + Event::GenerateBtcPendingInfo { + account_id: &account_id, + btc_pending_id: &btc_pending_id, + } + .emit(); + } +} diff --git a/contracts/satoshi-bridge/src/api/chain_signatures.rs b/contracts/satoshi-bridge/src/api/chain_signatures.rs index 54460373..f546edc4 100644 --- a/contracts/satoshi-bridge/src/api/chain_signatures.rs +++ b/contracts/satoshi-bridge/src/api/chain_signatures.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + near, require, AccessControllable, Contract, ContractExt, Pausable, PromiseOrValue, Role, +}; + use near_plugins::pause; #[near] diff --git a/contracts/satoshi-bridge/src/api/management.rs b/contracts/satoshi-bridge/src/api/management.rs index f68f68a9..41a5b3d2 100644 --- a/contracts/satoshi-bridge/src/api/management.rs +++ b/contracts/satoshi-bridge/src/api/management.rs @@ -1,4 +1,8 @@ -use crate::*; +use crate::{ + assert_one_yocto, env, near, require, AccessControllable, Account, AccountId, BridgeFee, + Contract, ContractExt, HashSet, Promise, Role, U128, U64, +}; + use near_plugins::access_control_any; #[near] @@ -17,7 +21,7 @@ impl Contract { pub fn withdraw_protocol_fee(&mut self, amount: Option) -> Promise { assert_one_yocto(); let total_protocol_fee = self.data().cur_available_protocol_fee; - let amount = amount.map(|v| v.0).unwrap_or(total_protocol_fee); + let amount = amount.map_or(total_protocol_fee, |v| v.0); require!(amount > 0 && amount <= total_protocol_fee, "Invalid amount"); self.data_mut().cur_available_protocol_fee -= amount; self.data_mut().acc_claimed_protocol_fee += amount; @@ -450,7 +454,7 @@ impl Contract { pub fn set_unhealthy_utxo_amount(&mut self, unhealthy_utxo_amount: U64) { assert_one_yocto(); require!( - unhealthy_utxo_amount.0 as u128 > self.internal_config().min_change_amount, + u128::from(unhealthy_utxo_amount.0) > self.internal_config().min_change_amount, "Invalid unhealthy_utxo_amount" ); self.internal_mut_config().unhealthy_utxo_amount = unhealthy_utxo_amount.0; diff --git a/contracts/satoshi-bridge/src/api/token_receiver.rs b/contracts/satoshi-bridge/src/api/token_receiver.rs index 28432a08..acbd3c3c 100644 --- a/contracts/satoshi-bridge/src/api/token_receiver.rs +++ b/contracts/satoshi-bridge/src/api/token_receiver.rs @@ -1,14 +1,20 @@ -use crate::*; +use crate::{psbt_wrapper::PsbtWrapper, *}; use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; use near_plugins::pause; +pub const GAS_FOR_FT_ON_TRANSFER_CALL_BACK: Gas = Gas::from_tgas(100); + #[near(serializers = [json])] pub enum TokenReceiverMessage { DepositProtocolFee, + // Here is the withdraw message structure that will be sent from user or dApp to the btc/zcash connector Withdraw { target_btc_address: String, input: Vec, output: Vec, + max_gas_fee: Option, + chain_specific_data: Option, + external_id: Option, }, } @@ -47,71 +53,100 @@ impl FungibleTokenReceiver for Contract { target_btc_address, input, output, - } => { - let (psbt, utxo_storage_keys, vutxos) = - self.generate_psbt_and_vutxos(input, output); - require!( - self.internal_unwrap_or_create_mut_account(&sender_id) - .btc_pending_sign_id - .is_none(), - "Previous btc tx has not been signed" - ); - let target_address_script_pubkey = - string_to_btc_address(&target_btc_address).script_pubkey(); + max_gas_fee, + chain_specific_data, + external_id, + } => self.ft_on_transfer_withdraw_chain_specific( + sender_id.clone(), + amount, + target_btc_address, + input, + output, + max_gas_fee, + chain_specific_data, + external_id.map(|id| format!("{sender_id}:{id}")), + ), + } + } +} + +impl Contract { + pub(crate) fn create_btc_pending_info( + &mut self, + sender_id: AccountId, + amount: u128, + target_btc_address: String, + mut psbt: PsbtWrapper, + max_gas_fee: Option, + external_id: Option, + ) { + let (utxo_storage_keys, vutxos) = self.generate_vutxos(&mut psbt); + require!( + self.internal_unwrap_or_create_mut_account(&sender_id) + .btc_pending_sign_id + .is_none(), + "Previous btc tx has not been signed" + ); - let withdraw_change_address_script_pubkey = - self.internal_config().get_change_address().script_pubkey(); - let withdraw_fee = self.internal_config().withdraw_bridge_fee.get_fee(amount); - let (actual_received_amount, gas_fee) = self.check_withdraw_psbt_valid( - &target_address_script_pubkey, - &withdraw_change_address_script_pubkey, - &psbt, - &vutxos, - amount, - withdraw_fee, - ); + let withdraw_change_address_script_pubkey = + self.internal_config().get_change_script_pubkey(); + let withdraw_fee = self.internal_config().withdraw_bridge_fee.get_fee(amount); + let (actual_received_amount, gas_fee) = self.check_withdraw_psbt_valid( + target_btc_address.clone(), + &withdraw_change_address_script_pubkey, + &psbt, + &vutxos, + amount, + withdraw_fee, + max_gas_fee, + ); - let need_signature_num = psbt.unsigned_tx.input.len(); - let psbt_hex = psbt.serialize_hex(); - let btc_pending_id = psbt.extract_tx().unwrap().compute_txid().to_string(); - let btc_pending_info = BTCPendingInfo { - account_id: sender_id.clone(), - btc_pending_id: btc_pending_id.clone(), - transfer_amount: amount, - actual_received_amount, - withdraw_fee, - gas_fee, - burn_amount: actual_received_amount + gas_fee, - psbt_hex, - vutxos, - signatures: vec![None; need_signature_num], - tx_bytes_with_sign: None, - create_time_sec: nano_to_sec(env::block_timestamp()), - last_sign_time_sec: 0, - state: PendingInfoState::WithdrawOriginal(OriginalState { - stage: PendingInfoStage::PendingSign, - max_gas_fee: gas_fee, - last_rbf_time_sec: None, - cancel_rbf_reserved: None, - }), - }; - require!( - self.data_mut() - .btc_pending_infos - .insert(btc_pending_id.clone(), btc_pending_info.into()) - .is_none(), - "pending info already exist" - ); - self.internal_unwrap_mut_account(&sender_id) - .btc_pending_sign_id = Some(btc_pending_id.clone()); - Event::UtxoRemoved { utxo_storage_keys }.emit(); - Event::GenerateBtcPendingInfo { - account_id: &sender_id, - btc_pending_id: &btc_pending_id, - } - .emit(); - PromiseOrValue::Value(U128(0)) - } + let need_signature_num = psbt.get_input_num(); + let psbt_hex = psbt.serialize(); + let btc_pending_id = psbt.get_pending_id(); + let btc_pending_info = BTCPendingInfo { + account_id: sender_id.clone(), + btc_pending_id: btc_pending_id.clone(), + transfer_amount: amount, + actual_received_amount, + withdraw_fee, + gas_fee, + burn_amount: actual_received_amount + gas_fee, + psbt_hex, + vutxos, + signatures: vec![None; need_signature_num], + tx_bytes_with_sign: None, + create_time_sec: nano_to_sec(env::block_timestamp()), + last_sign_time_sec: 0, + state: PendingInfoState::WithdrawOriginal(OriginalState { + stage: PendingInfoStage::PendingSign, + max_gas_fee: gas_fee, + last_rbf_time_sec: None, + cancel_rbf_reserved: None, + }), + }; + require!( + self.data_mut() + .btc_pending_infos + .insert(btc_pending_id.clone(), btc_pending_info.into()) + .is_none(), + "pending info already exist" + ); + + if let Some(external_id) = &external_id { + self.data_mut() + .btc_pending_infos_by_external_id + .insert(external_id.clone(), btc_pending_id.clone()); + } + + self.internal_unwrap_mut_account(&sender_id) + .btc_pending_sign_id = Some(btc_pending_id.clone()); + Event::UtxoRemoved { utxo_storage_keys }.emit(); + Event::GenerateBtcPendingInfo { + account_id: &sender_id, + btc_pending_id: &btc_pending_id, + external_id, } + .emit(); } } diff --git a/contracts/satoshi-bridge/src/api/view.rs b/contracts/satoshi-bridge/src/api/view.rs index 7e02dae0..b98f3cbe 100644 --- a/contracts/satoshi-bridge/src/api/view.rs +++ b/contracts/satoshi-bridge/src/api/view.rs @@ -1,4 +1,10 @@ -use crate::*; +use crate::{ + env, near, u128_dec_format, AccessControllable, Account, AccountId, BTCPendingInfo, Config, + Contract, ContractExt, HashMap, HashSet, NearToken, Pausable, Role, U128, UTXO, +}; + +const REQUIRED_BALANCE_FOR_DEPOSIT: NearToken = + NearToken::from_yoctonear(1_200_000_000_000_000_000_000); #[near(serializers = [json])] #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] @@ -31,10 +37,14 @@ impl Contract { pub fn get_metadata(&self) -> Metadata { let root_state = self.data(); Metadata { - super_admins: self.acl_get_super_admins(0, usize::MAX as u64), - daos: self.acl_get_grantees(Role::DAO.into(), 0, usize::MAX as u64), - operators: self.acl_get_grantees(Role::Operator.into(), 0, usize::MAX as u64), - pause_managers: self.acl_get_grantees(Role::PauseManager.into(), 0, usize::MAX as u64), + super_admins: self.acl_get_super_admins(0, u64::from(u32::MAX)), + daos: self.acl_get_grantees(Role::DAO.into(), 0, u64::from(u32::MAX)), + operators: self.acl_get_grantees(Role::Operator.into(), 0, u64::from(u32::MAX)), + pause_managers: self.acl_get_grantees( + Role::PauseManager.into(), + 0, + u64::from(u32::MAX), + ), pa_all_paused: self.pa_all_paused(), relayer_white_list: root_state.relayer_white_list.iter().cloned().collect(), extra_msg_relayer_white_list: root_state @@ -81,7 +91,8 @@ impl Contract { from_index: Option, limit: Option, ) -> HashMap { - let len = self.data().accounts.len() as usize; + let len = usize::try_from(self.data().accounts.len()) + .unwrap_or_else(|_| env::panic_str("Too many accounts")); let skip_n = from_index.unwrap_or(0); let take_n = limit.unwrap_or(len - skip_n); self.data() @@ -98,7 +109,8 @@ impl Contract { from_index: Option, limit: Option, ) -> HashMap { - let len = self.data().lost_found.len() as usize; + let len = usize::try_from(self.data().lost_found.len()) + .unwrap_or_else(|_| env::panic_str("Too many lost_found accounts")); let skip_n = from_index.unwrap_or(0); let take_n = limit.unwrap_or(len - skip_n); self.data() @@ -127,7 +139,8 @@ impl Contract { from_index: Option, limit: Option, ) -> HashMap { - let len = self.data().utxos.len() as usize; + let len = usize::try_from(self.data().utxos.len()) + .unwrap_or_else(|_| env::panic_str("Too many utxos")); let skip_n = from_index.unwrap_or(0); let take_n = limit.unwrap_or(len - skip_n); self.data() @@ -142,7 +155,7 @@ impl Contract { pub fn list_utxos(&self, utxo_storage_keys: Vec) -> HashMap> { utxo_storage_keys .into_iter() - .map(|key| (key.clone(), self.data().utxos.get(&key).map(|v| v.into()))) + .map(|key| (key.clone(), self.data().utxos.get(&key).map(Into::into))) .collect() } @@ -151,7 +164,8 @@ impl Contract { from_index: Option, limit: Option, ) -> HashMap { - let len = self.data().unavailable_utxos.len() as usize; + let len = usize::try_from(self.data().unavailable_utxos.len()) + .unwrap_or_else(|_| env::panic_str("Too many unavailable_utxos")); let skip_n = from_index.unwrap_or(0); let take_n = limit.unwrap_or(len - skip_n); self.data() @@ -172,7 +186,7 @@ impl Contract { .map(|key| { ( key.clone(), - self.data().unavailable_utxos.get(&key).map(|v| v.into()), + self.data().unavailable_utxos.get(&key).map(Into::into), ) }) .collect() @@ -183,7 +197,8 @@ impl Contract { from_index: Option, limit: Option, ) -> HashMap { - let len = self.data().btc_pending_infos.len() as usize; + let len = usize::try_from(self.data().btc_pending_infos.len()) + .unwrap_or_else(|_| env::panic_str("Too many btc_pending_infos")); let skip_n = from_index.unwrap_or(0); let take_n = limit.unwrap_or(len - skip_n); self.data() @@ -220,7 +235,8 @@ impl Contract { from_index: Option, limit: Option, ) -> HashMap> { - let len = self.data().rbf_txs.len() as usize; + let len = usize::try_from(self.data().rbf_txs.len()) + .unwrap_or_else(|_| env::panic_str("Too many rbf_txs")); let skip_n = from_index.unwrap_or(0); let take_n = limit.unwrap_or(len - skip_n); self.data() @@ -252,7 +268,8 @@ impl Contract { from_index: Option, limit: Option, ) -> HashMap> { - let len = self.data().post_action_msg_templates.len() as usize; + let len = usize::try_from(self.data().post_action_msg_templates.len()) + .unwrap_or_else(|_| env::panic_str("Too many post_action_msg_templates")); let skip_n = from_index.unwrap_or(0); let take_n = limit.unwrap_or(len - skip_n); self.data() @@ -263,4 +280,8 @@ impl Contract { .map(|(k, v)| (k.clone(), v.clone())) .collect() } + + pub fn required_balance_for_safe_deposit(&self) -> NearToken { + REQUIRED_BALANCE_FOR_DEPOSIT + } } diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs b/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs new file mode 100644 index 00000000..af084b8b --- /dev/null +++ b/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs @@ -0,0 +1,120 @@ +use crate::bitcoin_utils::types::ChainSpecificData; +use crate::env; +use crate::psbt_wrapper::PsbtWrapper; +use crate::{BTCPendingInfo, Contract, Event}; +use bitcoin::{OutPoint, TxOut}; +use near_sdk::json_types::U128; +use near_sdk::{require, AccountId, PromiseOrValue}; + +macro_rules! define_rbf_method { + ($method:ident, $internal_fn:ident) => { + pub(crate) fn $method( + &mut self, + account_id: AccountId, + original_btc_pending_verify_id: String, + output: Vec, + _chain_specific_data: Option, + ) { + let predecessor_account_id = env::predecessor_account_id(); + let original_tx_btc_pending_info = + self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id); + + let new_psbt = self.generate_psbt_from_original_psbt_and_new_output( + original_tx_btc_pending_info, + output, + ); + + let btc_pending_id = self.$internal_fn( + &account_id, + original_btc_pending_verify_id, + new_psbt, + predecessor_account_id, + ); + + self.internal_unwrap_mut_account(&account_id) + .btc_pending_sign_id = Some(btc_pending_id.clone()); + + Event::GenerateBtcPendingInfo { + account_id: &account_id, + btc_pending_id: &btc_pending_id, + external_id: None, + } + .emit(); + } + }; +} + +impl Contract { + pub(crate) fn check_psbt_chain_specific( + &self, + _psbt: &PsbtWrapper, + _gas_fee: u128, + _target_btc_address: String, + ) { + } + + pub(crate) fn check_withdraw_chain_specific( + original_tx_btc_pending_info: &BTCPendingInfo, + gas_fee: u128, + ) { + // Ensure that the RBF transaction pays more gas than the previous transaction. + let max_gas_fee = original_tx_btc_pending_info.get_max_gas_fee(); + let additional_gas_amount = gas_fee.saturating_sub(max_gas_fee); + require!(additional_gas_amount > 0, "No gas increase."); + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn ft_on_transfer_withdraw_chain_specific( + &mut self, + sender_id: AccountId, + amount: u128, + target_btc_address: String, + input: Vec, + output: Vec, + max_gas_fee: Option, + _chain_specific_data: Option, + external_id: Option, + ) -> PromiseOrValue { + self.create_btc_pending_info( + sender_id, + amount, + target_btc_address, + PsbtWrapper::new(input, output), + max_gas_fee, + external_id, + ); + PromiseOrValue::Value(U128(0)) + } + + define_rbf_method!(withdraw_rbf_chain_specific, internal_withdraw_rbf); + define_rbf_method!(cancel_withdraw_chain_specific, internal_cancel_withdraw); + define_rbf_method!( + cancel_active_utxo_management_chain_specific, + internal_cancel_active_utxo_management + ); + define_rbf_method!( + active_utxo_management_rbf_chain_specific, + internal_active_utxo_management_rbf + ); + + pub(crate) fn active_utxo_management_chain_specific( + &mut self, + account_id: AccountId, + input: Vec, + output: Vec, + ) { + self.create_active_utxo_management_pending_info( + account_id, + PsbtWrapper::new(input, output), + ); + } + + pub(crate) fn generate_psbt_from_original_psbt_and_new_output( + &self, + original_tx_btc_pending_info: &BTCPendingInfo, + output: Vec, + ) -> PsbtWrapper { + let original_psbt = original_tx_btc_pending_info.get_psbt(); + PsbtWrapper::from_original_psbt(original_psbt, output) + } +} diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/mod.rs b/contracts/satoshi-bridge/src/bitcoin_utils/mod.rs new file mode 100644 index 00000000..18ae7bc9 --- /dev/null +++ b/contracts/satoshi-bridge/src/bitcoin_utils/mod.rs @@ -0,0 +1,4 @@ +pub mod contract_methods; +pub mod psbt_wrapper; +pub mod transaction; +pub mod types; diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs b/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs new file mode 100644 index 00000000..85c35774 --- /dev/null +++ b/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs @@ -0,0 +1,169 @@ +use crate::{generate_utxo_storage_key, SignatureResponse}; + +use bitcoin::absolute::LockTime; +use bitcoin::consensus::serialize; +use bitcoin::hashes::Hash; +use bitcoin::psbt::Psbt; +use bitcoin::sighash::SighashCache; +use bitcoin::transaction::Version; +use bitcoin::Transaction as BtcTransaction; +use bitcoin::{OutPoint, TxIn, TxOut, Witness}; +use near_sdk::require; + +pub struct PsbtWrapper { + psbt: Psbt, +} +impl PsbtWrapper { + pub fn new(input: Vec, output: Vec) -> Self { + require!(!input.is_empty(), "empty input"); + require!(!output.is_empty(), "empty output"); + + let sequence = bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME; + + let transaction = BtcTransaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: input + .into_iter() + .map(|previous_output| TxIn { + previous_output, + sequence, + ..Default::default() + }) + .collect(), + output, + }; + let psbt = Psbt::from_unsigned_tx(transaction).expect("Failed to generate PSBT"); + + Self { psbt } + } + + pub fn from_original_psbt( + original_psbt: crate::psbt_wrapper::PsbtWrapper, + output: Vec, + ) -> Self { + let sequence = bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME; + + let transaction = BtcTransaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: original_psbt + .psbt + .unsigned_tx + .input + .into_iter() + .map(|original_psbt_input| TxIn { + previous_output: original_psbt_input.previous_output, + sequence, + ..Default::default() + }) + .collect(), + output, + }; + let mut psbt = Psbt::from_unsigned_tx(transaction).expect("Failed to generate PSBT"); + original_psbt + .psbt + .inputs + .iter() + .enumerate() + .for_each(|(i, v)| { + psbt.inputs[i].witness_utxo.clone_from(&v.witness_utxo); + }); + Self { psbt } + } + + pub fn set_input_utxo(&mut self, input_utxo: Vec) { + input_utxo + .iter() + .enumerate() + .for_each(|(i, v)| self.psbt.inputs[i].witness_utxo = Some(v.clone())); + } + + pub fn get_output(&self) -> &Vec { + &self.psbt.unsigned_tx.output + } + + pub fn get_input_num(&self) -> usize { + self.psbt.unsigned_tx.input.len() + } + + pub fn get_output_num(&self) -> usize { + self.psbt.unsigned_tx.output.len() + } + + pub fn get_utxo_storage_keys(&self) -> Vec { + self.psbt + .unsigned_tx + .input + .clone() + .into_iter() + .map(|out_point| { + generate_utxo_storage_key( + out_point.previous_output.txid.to_string(), + out_point.previous_output.vout, + ) + }) + .collect() + } + + pub fn add_extra_outputs(&self, _actual_received_amounts: &mut [u128]) -> u128 { + 0 + } + pub fn serialize(&self) -> String { + self.psbt.serialize_hex() + } + + pub fn deserialize(psbt_hex: &String) -> Self { + let psbt_bytes = hex::decode(psbt_hex).expect("ERR_INVALID_PSBT_HEX: failed to decode hex"); + Self { + psbt: Psbt::deserialize(&psbt_bytes).expect("ERR_INVALID_PSBT: failed to parse PSBT"), + } + } + + pub fn extract_tx_bytes_with_sign(&self) -> Vec { + serialize(&self.psbt.clone().extract_tx().expect("extract_tx failed")) + } + + pub fn get_pending_id(&self) -> String { + self.psbt + .clone() + .extract_tx() + .expect("ERR_EXTRACT_TX: failed to extract transaction from PSBT") + .compute_txid() + .to_string() + } + + #[allow(unused_variables)] + pub fn get_hash_to_sign(&self, vin: usize, public_keys: &[bitcoin::PublicKey]) -> [u8; 32] { + let tx = self.psbt.unsigned_tx.clone(); + let mut cache = SighashCache::new(tx); + let witness_utxo = self.psbt.inputs[vin] + .witness_utxo + .as_ref() + .expect("ERR_MISSING_WITNESS_UTXO: input missing witness UTXO data"); + cache + .p2wpkh_signature_hash( + vin, + &witness_utxo.script_pubkey, + witness_utxo.value, + bitcoin::EcdsaSighashType::All, + ) + .expect("ERR_SIGHASH: failed to compute signature hash") + .to_raw_hash() + .to_byte_array() + } + + pub fn save_signature( + &mut self, + sign_index: usize, + signature: SignatureResponse, + public_key: bitcoin::secp256k1::PublicKey, + ) { + self.psbt.inputs[sign_index].final_script_witness = + Some(Witness::p2wpkh(&signature.to_btc_signature(), &public_key)); + } + + pub fn get_recipient_address(&self) -> Option { + None + } +} diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/transaction.rs b/contracts/satoshi-bridge/src/bitcoin_utils/transaction.rs new file mode 100644 index 00000000..15349d2d --- /dev/null +++ b/contracts/satoshi-bridge/src/bitcoin_utils/transaction.rs @@ -0,0 +1,41 @@ +use crate::network; +use bitcoin::consensus::{Decodable, Encodable}; +use bitcoin::{absolute, Transaction as BtcTransaction, TxOut, Txid}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Transaction { + pub inner_tx: BtcTransaction, +} + +impl Transaction { + pub fn compute_txid(&self) -> Txid { + self.inner_tx.compute_txid() + } + + pub fn output(&self) -> Vec { + self.inner_tx.output.clone() + } + + pub fn lock_time(&self) -> absolute::LockTime { + self.inner_tx.lock_time + } + + pub fn encode(&self) -> Result, bitcoin::io::Error> { + let mut buf = Vec::new(); + self.inner_tx.consensus_encode(&mut buf)?; + Ok(buf) + } + + pub fn decode( + data: &[u8], + _chain: &network::Chain, + ) -> Result { + let mut cursor = bitcoin::io::Cursor::new(data); + let tx = BtcTransaction::consensus_decode(&mut cursor)?; + Ok(Self { inner_tx: tx }) + } + + pub fn tx_bytes_with_sign(tx: bitcoin::Transaction) -> Result, bitcoin::io::Error> { + Transaction { inner_tx: tx }.encode() + } +} diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/types.rs b/contracts/satoshi-bridge/src/bitcoin_utils/types.rs new file mode 100644 index 00000000..137cd845 --- /dev/null +++ b/contracts/satoshi-bridge/src/bitcoin_utils/types.rs @@ -0,0 +1,4 @@ +use near_sdk::near; + +#[near(serializers = [json])] +pub struct ChainSpecificData {} diff --git a/contracts/satoshi-bridge/src/btc_light_client/active_utxo_management.rs b/contracts/satoshi-bridge/src/btc_light_client/active_utxo_management.rs index 628d7307..385470ea 100644 --- a/contracts/satoshi-bridge/src/btc_light_client/active_utxo_management.rs +++ b/contracts/satoshi-bridge/src/btc_light_client/active_utxo_management.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + env, near, require, serde_json, BTCPendingInfo, Contract, ContractExt, Gas, Promise, + PromiseOrValue, MAX_BOOL_RESULT, +}; pub const GAS_FOR_VERIFY_ACTIVE_UTXO_MANAGEMENT_CALL_BACK: Gas = Gas::from_tgas(50); @@ -36,8 +39,8 @@ impl Contract { &mut self, tx_id: String, ) -> PromiseOrValue { - let result_bytes = - promise_result_as_success().expect("Call verify_transaction_inclusion failed"); + let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT) + .expect("Call verify_transaction_inclusion failed"); let is_valid = serde_json::from_slice::(&result_bytes) .expect("verify_transaction_inclusion return not bool"); require!(is_valid, "verify_transaction_inclusion return false"); diff --git a/contracts/satoshi-bridge/src/btc_light_client/deposit.rs b/contracts/satoshi-bridge/src/btc_light_client/deposit.rs index d96cf12d..6f9a40a3 100644 --- a/contracts/satoshi-bridge/src/btc_light_client/deposit.rs +++ b/contracts/satoshi-bridge/src/btc_light_client/deposit.rs @@ -1,10 +1,19 @@ -use crate::*; +use near_sdk::serde_json::Value; + +use crate::{ + burn::GAS_FOR_BURN_CALL, + env, ext_nbtc, + mint::{GAS_FOR_MINT_CALL, GAS_FOR_MINT_CALL_BACK}, + near, require, serde_json, AccountId, Contract, ContractExt, DepositMsg, Event, Gas, NearToken, + PendingUTXOInfo, PostAction, Promise, PromiseOrValue, SafeDepositMsg, MAX_BOOL_RESULT, + MAX_FT_TRANSFER_CALL_RESULT, U128, +}; pub const GAS_FOR_VERIFY_DEPOSIT_CALL_BACK: Gas = Gas::from_tgas(190); pub const GAS_FOR_UNAVAILABLE_UTXO_CALL_BACK: Gas = Gas::from_tgas(20); impl Contract { - pub fn internal_verify_deposit( + pub(crate) fn internal_verify_deposit( &mut self, deposit_amount: u128, tx_block_blockhash: String, @@ -57,6 +66,48 @@ impl Contract { ) } } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn internal_safe_verify_deposit( + &mut self, + deposit_amount: u128, + tx_block_blockhash: String, + tx_index: u64, + merkle_proof: Vec, + pending_utxo_info: PendingUTXOInfo, + recipient_id: AccountId, + deposit_msg: SafeDepositMsg, + ) -> Promise { + let config = self.internal_config(); + let confirmations = self.get_confirmations(config, deposit_amount); + let promise = self.verify_transaction_inclusion_promise( + config.btc_light_client_account_id.clone(), + pending_utxo_info.tx_id.clone(), + tx_block_blockhash, + tx_index, + merkle_proof, + confirmations, + ); + + if deposit_amount < config.min_deposit_amount { + promise.then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_UNAVAILABLE_UTXO_CALL_BACK) + .unavailable_utxo_callback(recipient_id, pending_utxo_info), + ) + } else { + promise.then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_VERIFY_DEPOSIT_CALL_BACK) + .verify_safe_deposit_callback( + recipient_id, + deposit_amount.into(), + deposit_msg.msg, + pending_utxo_info, + ), + ) + } + } } #[near] @@ -67,8 +118,8 @@ impl Contract { recipient_id: AccountId, pending_utxo_info: PendingUTXOInfo, ) -> PromiseOrValue { - let result_bytes = - promise_result_as_success().expect("Call verify_transaction_inclusion failed"); + let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT) + .expect("Call verify_transaction_inclusion failed"); let is_valid = serde_json::from_slice::(&result_bytes) .expect("verify_transaction_inclusion return not bool"); require!(is_valid, "verify_transaction_inclusion return false"); @@ -78,7 +129,7 @@ impl Contract { .insert(pending_utxo_info.utxo_storage_key.clone()), "Already deposit utxo" ); - let deposit_amount = pending_utxo_info.utxo.balance as u128; + let deposit_amount = u128::from(pending_utxo_info.utxo.balance); self.internal_set_unavailable_utxo( &pending_utxo_info.utxo_storage_key, pending_utxo_info.utxo, @@ -102,8 +153,8 @@ impl Contract { pending_utxo_info: PendingUTXOInfo, post_actions: Option>, ) -> PromiseOrValue { - let result_bytes = - promise_result_as_success().expect("Call verify_transaction_inclusion failed"); + let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT) + .expect("Call verify_transaction_inclusion failed"); let is_valid = serde_json::from_slice::(&result_bytes) .expect("verify_transaction_inclusion return not bool"); require!(is_valid, "verify_transaction_inclusion return false"); @@ -123,4 +174,208 @@ impl Contract { ) .into() } + + #[private] + pub fn verify_safe_deposit_callback( + &mut self, + recipient_id: AccountId, + mint_amount: U128, + msg: String, + pending_utxo_info: PendingUTXOInfo, + ) -> PromiseOrValue { + let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT) + .expect("Call verify_transaction_inclusion failed"); + let is_valid = serde_json::from_slice::(&result_bytes) + .expect("verify_transaction_inclusion return not bool"); + require!(is_valid, "verify_transaction_inclusion return false"); + require!( + self.data_mut() + .verified_deposit_utxo + .insert(pending_utxo_info.utxo_storage_key.clone()), + "Already deposit utxo" + ); + + let msg = (!msg.is_empty()) + .then(|| inject_utxo_id_in_msg(msg, &pending_utxo_info.utxo_storage_key)); + + ext_nbtc::ext(self.internal_config().nbtc_account_id.clone()) + .with_static_gas(GAS_FOR_MINT_CALL) + .with_attached_deposit(NearToken::from_yoctonear(1)) + .safe_mint(recipient_id.clone(), mint_amount, msg) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_MINT_CALL_BACK) + .safe_mint_callback(recipient_id.clone(), mint_amount, pending_utxo_info), + ) + .into() + } + + #[private] + pub fn safe_mint_callback( + &mut self, + recipient_id: AccountId, + mint_amount: U128, + pending_utxo_info: PendingUTXOInfo, + ) -> bool { + let is_success = !is_refund_required(); + let relayer_account_id = env::signer_account_id(); + + if is_success { + Event::UtxoAdded { + utxo_storage_keys: vec![pending_utxo_info.utxo_storage_key.clone()], + } + .emit(); + self.internal_set_utxo(&pending_utxo_info.utxo_storage_key, pending_utxo_info.utxo); + } else { + self.data_mut() + .verified_deposit_utxo + .remove(&pending_utxo_info.utxo_storage_key); + + ext_nbtc::ext(self.internal_config().nbtc_account_id.clone()) + .with_static_gas(GAS_FOR_BURN_CALL) + .burn( + env::current_account_id(), + mint_amount, + relayer_account_id, + U128(0), + ) + .detach(); + + Promise::new(env::signer_account_id()) + .transfer(self.required_balance_for_safe_deposit()) + .detach(); + } + + Event::VerifyDepositDetails { + recipient_id: &recipient_id, + mint_amount, + protocol_fee: U128(0), + relayer_account_id: env::signer_account_id(), + relayer_fee: U128(0), + success: is_success, + } + .emit(); + is_success + } +} + +fn is_refund_required() -> bool { + match env::promise_result_checked(0, MAX_FT_TRANSFER_CALL_RESULT) { + Ok(value) => { + if let Ok(amount) = near_sdk::serde_json::from_slice::(&value) { + // Normal case: refund if the used token amount is zero + // The amount can be zero if the `ft_on_transfer` in the receiver contract returns an amount instead of `0`, or if it panics. + amount.0 == 0 + } else { + // Unexpected case: don't refund + false + } + } + // Unexpected case: don't refund + Err(_) => false, + } +} + +fn inject_utxo_id_in_msg(msg: String, utxo_id: &str) -> String { + fn inject(value: &mut Value, utxo_id: &str) { + match value { + Value::Object(map) => { + for (k, v) in map.iter_mut() { + if k == "utxo_id" { + *v = Value::String(utxo_id.to_string()); + } else { + inject(v, utxo_id); + } + } + } + Value::Array(arr) => { + for v in arr.iter_mut() { + inject(v, utxo_id); + } + } + _ => {} + } + } + + if let Ok(mut json) = serde_json::from_str::(&msg) { + inject(&mut json, utxo_id); + serde_json::to_string(&json).unwrap() + } else { + msg + } +} + +#[cfg(test)] +mod tests { + use crate::btc_light_client::deposit::inject_utxo_id_in_msg; + use near_sdk::{near, serde_json}; + + #[near(serializers=[json])] + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct UtxoFinTransferMsg { + pub utxo_id: String, + pub recipient: String, + pub relayer_fee: String, + pub msg: String, + } + + #[near(serializers=[json])] + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum BridgeOnTransferMsg { + UtxoFinTransfer(UtxoFinTransferMsg), + } + + #[test] + fn test_duplicated_utxo_id_injection() { + let duplicated_msg = + r#"{"utxo_id":"first","utxo_id":"second","recipient":"some_recipient","relayer_fee":"1000","msg":"OS"}"# + .to_string(); + + let injected_msg = inject_utxo_id_in_msg(duplicated_msg, "correct_utxo_id"); + let parsed_msg: UtxoFinTransferMsg = serde_json::from_str(&injected_msg).unwrap(); + let expected = UtxoFinTransferMsg { + utxo_id: "correct_utxo_id".to_string(), + recipient: "some_recipient".to_string(), + relayer_fee: "1000".to_string(), + msg: "OS".to_string(), + }; + + assert_eq!(parsed_msg, expected); + } + + #[test] + fn test_utxo_id_injection() { + let nested_msg = + r#"{"UtxoFinTransfer":{"msg":"OS","recipient":"some_recipient","relayer_fee":"1000","utxo_id":"{{UTXO_TX_ID}}"}}"# + .to_string(); + + let injected_msg = inject_utxo_id_in_msg(nested_msg, "correct_utxo_id"); + let parsed_msg: BridgeOnTransferMsg = serde_json::from_str(&injected_msg).unwrap(); + let expected = BridgeOnTransferMsg::UtxoFinTransfer(UtxoFinTransferMsg { + utxo_id: "correct_utxo_id".to_string(), + recipient: "some_recipient".to_string(), + relayer_fee: "1000".to_string(), + msg: "OS".to_string(), + }); + + assert_eq!(parsed_msg, expected); + } + + #[test] + fn test_already_set_utxo_id_injection() { + let nested_msg = + r#"{"UtxoFinTransfer":{"msg":"OS","recipient":"{{UTXO_TX_ID}}","relayer_fee":"1000","utxo_id":"invalid_utxo_id"}}"# + .to_string(); + + let injected_msg = inject_utxo_id_in_msg(nested_msg, "correct_utxo_id"); + let parsed_msg: BridgeOnTransferMsg = serde_json::from_str(&injected_msg).unwrap(); + let expected = BridgeOnTransferMsg::UtxoFinTransfer(UtxoFinTransferMsg { + utxo_id: "correct_utxo_id".to_string(), + recipient: "{{UTXO_TX_ID}}".to_string(), + relayer_fee: "1000".to_string(), + msg: "OS".to_string(), + }); + + assert_eq!(parsed_msg, expected); + } } diff --git a/contracts/satoshi-bridge/src/btc_light_client/mod.rs b/contracts/satoshi-bridge/src/btc_light_client/mod.rs index b74cc6f4..da524661 100644 --- a/contracts/satoshi-bridge/src/btc_light_client/mod.rs +++ b/contracts/satoshi-bridge/src/btc_light_client/mod.rs @@ -5,16 +5,21 @@ use near_sdk::serde::{ }; use std::{fmt, str::FromStr}; -use crate::*; +use crate::{env, ext_contract, near, AccountId, Contract, Gas, Promise}; pub mod active_utxo_management; pub mod deposit; pub mod withdraw; pub const GAS_FOR_VERIFY_TRANSACTION_INCLUSION: Gas = Gas::from_tgas(10); - +pub const GAS_FOR_GET_LAST_BLOCK_HEIGHT: Gas = Gas::from_tgas(3); #[near(serializers = [borsh])] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct H256(pub [u8; 32]); +#[near(serializers = [borsh, json])] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct U256(u128, u128); + impl FromStr for H256 { type Err = hex::FromHexError; @@ -53,7 +58,7 @@ impl ProofArgs { .into_iter() .map(|v| { v.parse() - .unwrap_or_else(|_| panic!("Invalid merkle_proof: {:?}", v)) + .unwrap_or_else(|_| env::panic_str("Invalid merkle_proof: {v:?}")) }) .collect(), confirmations, @@ -106,6 +111,7 @@ impl Serialize for H256 { #[ext_contract(ext_btc_light_client)] pub trait BtcLightClient { fn verify_transaction_inclusion(&self, #[serializer(borsh)] args: ProofArgs) -> bool; + fn get_last_block_height(&self) -> u32; } impl Contract { @@ -128,4 +134,11 @@ impl Contract { confirmations, )) } + + pub fn get_last_block_height_promise(&self) -> Promise { + let config = self.internal_config(); + ext_btc_light_client::ext(config.btc_light_client_account_id.clone()) + .with_static_gas(GAS_FOR_GET_LAST_BLOCK_HEIGHT) + .get_last_block_height() + } } diff --git a/contracts/satoshi-bridge/src/btc_light_client/withdraw.rs b/contracts/satoshi-bridge/src/btc_light_client/withdraw.rs index 5c234d52..794a068c 100644 --- a/contracts/satoshi-bridge/src/btc_light_client/withdraw.rs +++ b/contracts/satoshi-bridge/src/btc_light_client/withdraw.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + env, near, require, serde_json, BTCPendingInfo, Contract, ContractExt, Gas, Promise, + PromiseOrValue, MAX_BOOL_RESULT, +}; pub const GAS_FOR_VERIFY_WITHDRAW_CALL_BACK: Gas = Gas::from_tgas(50); pub const GAS_FOR_VERIFY_CANCEL_WITHDRAW_CALL_BACK: Gas = Gas::from_tgas(50); @@ -34,8 +37,8 @@ impl Contract { impl Contract { #[private] pub fn internal_verify_withdraw_callback(&mut self, tx_id: String) -> PromiseOrValue { - let result_bytes = - promise_result_as_success().expect("Call verify_transaction_inclusion failed"); + let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT) + .expect("Call verify_transaction_inclusion failed"); let is_valid = serde_json::from_slice::(&result_bytes) .expect("verify_transaction_inclusion return not bool"); require!(is_valid, "verify_transaction_inclusion return false"); diff --git a/contracts/satoshi-bridge/src/btc_pending_info.rs b/contracts/satoshi-bridge/src/btc_pending_info.rs index dc1eaa98..cdf15ddc 100644 --- a/contracts/satoshi-bridge/src/btc_pending_info.rs +++ b/contracts/satoshi-bridge/src/btc_pending_info.rs @@ -1,6 +1,9 @@ use std::borrow::{Borrow, BorrowMut}; -use crate::*; +use crate::{ + env, nano_to_sec, near, network, psbt_wrapper::PsbtWrapper, require, u128_dec_format, + AccountId, Contract, SignatureResponse, WrappedTransaction, U128, VUTXO, +}; #[near(serializers = [borsh, json])] #[derive(Clone, PartialEq, Eq)] @@ -160,21 +163,21 @@ impl BTCPendingInfo { PendingInfoState::ActiveUtxoManagementRbf(state) => state.assert_pending_verify(), PendingInfoState::ActiveUtxoManagementCancelRbf(state) => state.assert_pending_verify(), _ => env::panic_str("Not active utxo management related tx"), - }; + } } pub fn assert_active_utxo_management_original_pending_verify_tx(&self) { match self.state.borrow() { PendingInfoState::ActiveUtxoManagementOriginal(state) => state.assert_pending_verify(), _ => env::panic_str("Not active utxo management original tx"), - }; + } } pub fn assert_withdraw_original_pending_verify_tx(&self) { match self.state.borrow() { PendingInfoState::WithdrawOriginal(state) => state.assert_pending_verify(), _ => env::panic_str("Not withdraw original tx"), - }; + } } pub fn get_max_gas_fee(&self) -> u128 { @@ -202,22 +205,22 @@ impl BTCPendingInfo { pub fn to_pending_verify_stage(&mut self) { match self.state.borrow_mut() { PendingInfoState::WithdrawOriginal(state) => { - state.stage = PendingInfoStage::PendingVerify + state.stage = PendingInfoStage::PendingVerify; } PendingInfoState::WithdrawUserRbf(state) => { - state.stage = PendingInfoStage::PendingVerify + state.stage = PendingInfoStage::PendingVerify; } PendingInfoState::WithdrawCancelRbf(state) => { - state.stage = PendingInfoStage::PendingVerify + state.stage = PendingInfoStage::PendingVerify; } PendingInfoState::ActiveUtxoManagementOriginal(state) => { - state.stage = PendingInfoStage::PendingVerify + state.stage = PendingInfoStage::PendingVerify; } PendingInfoState::ActiveUtxoManagementRbf(state) => { - state.stage = PendingInfoStage::PendingVerify + state.stage = PendingInfoStage::PendingVerify; } PendingInfoState::ActiveUtxoManagementCancelRbf(state) => { - state.stage = PendingInfoStage::PendingVerify + state.stage = PendingInfoStage::PendingVerify; } } } @@ -225,20 +228,20 @@ impl BTCPendingInfo { pub fn to_pending_burn_stage(&mut self) { match self.state.borrow_mut() { PendingInfoState::WithdrawOriginal(state) => { - state.stage = PendingInfoStage::PendingBurn + state.stage = PendingInfoStage::PendingBurn; } PendingInfoState::WithdrawUserRbf(state) => state.stage = PendingInfoStage::PendingBurn, PendingInfoState::WithdrawCancelRbf(state) => { - state.stage = PendingInfoStage::PendingBurn + state.stage = PendingInfoStage::PendingBurn; } PendingInfoState::ActiveUtxoManagementOriginal(state) => { - state.stage = PendingInfoStage::PendingBurn + state.stage = PendingInfoStage::PendingBurn; } PendingInfoState::ActiveUtxoManagementRbf(state) => { - state.stage = PendingInfoStage::PendingBurn + state.stage = PendingInfoStage::PendingBurn; } PendingInfoState::ActiveUtxoManagementCancelRbf(state) => { - state.stage = PendingInfoStage::PendingBurn + state.stage = PendingInfoStage::PendingBurn; } } } @@ -272,19 +275,21 @@ impl BTCPendingInfo { } pub fn is_all_signed(&self) -> bool { - self.signatures.iter().all(|v| v.is_some()) + self.signatures.iter().all(Option::is_some) } - pub fn get_psbt(&self) -> Psbt { - to_psbt(&self.psbt_hex) + pub fn get_psbt(&self) -> PsbtWrapper { + PsbtWrapper::deserialize(&self.psbt_hex) } - pub fn get_transaction(&self) -> BtcTransaction { - bytes_to_btc_transaction( + pub fn get_transaction(&self, chain: &network::Chain) -> WrappedTransaction { + WrappedTransaction::decode( self.tx_bytes_with_sign .as_ref() .expect("Missing tx_bytes_with_sign"), + chain, ) + .expect("Deserialization tx_bytes failed") } } @@ -343,14 +348,14 @@ impl Contract { self.data() .btc_pending_infos .get(btc_pending_id) - .map(|o| o.into()) + .map(Into::into) } pub fn internal_unwrap_btc_pending_info(&self, btc_pending_id: &String) -> &BTCPendingInfo { self.data() .btc_pending_infos .get(btc_pending_id) - .map(|o| o.into()) + .map(Into::into) .expect("BTC pending info not exist") } @@ -361,7 +366,7 @@ impl Contract { self.data_mut() .btc_pending_infos .get_mut(btc_pending_id) - .map(|o| o.into()) + .map(Into::into) .expect("BTC pending info not exist") } @@ -391,17 +396,84 @@ pub fn generate_btc_pending_sign_id(payload_preimages: &[Vec]) -> String { &payload_preimages .iter() .flatten() - .cloned() + .copied() .collect::>(), ); hex::encode(hash_bytes) } -pub fn bytes_to_btc_transaction(tx_bytes: &[u8]) -> BtcTransaction { - deserialize(tx_bytes).expect("Deserialization tx_bytes failed") -} +#[cfg(test)] +#[cfg(feature = "zcash")] +mod tests { + use crate::network::{Address, Chain}; + use crate::{get_deposit_path, network, DepositMsg, WrappedTransaction}; + use bitcoin::PublicKey as BtcPublicKey; + use k256::elliptic_curve::sec1::ToEncodedPoint; + use near_sdk::PublicKey; + use std::str::FromStr; + + pub fn generate_public_key(path: &str) -> Vec { + let mpc_pk = crypto_shared::near_public_key_to_affine_point( + PublicKey::from_str("secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3").unwrap(), + ); + let epsilon = crypto_shared::derive_epsilon( + &"zcash_connector-20250714-143829.testnet".parse().unwrap(), + path, + ); + let user_pk = crypto_shared::derive_key(mpc_pk, epsilon); + let user_pk_encoded_point = user_pk.to_encoded_point(false); + user_pk_encoded_point.as_bytes().to_vec() + } + + pub fn generate_btc_public_key(path: &str) -> BtcPublicKey { + let public_key_bytes = generate_public_key(path); + let uncompressed_btc_public_key = + BtcPublicKey::from_slice(&public_key_bytes).expect("Invalid public key bytes"); + uncompressed_btc_public_key + .inner + .to_string() + .parse() + .unwrap() + } + + pub fn generate_utxo_chain_address(path: &str) -> Address { + let btc_public_key = generate_btc_public_key(path); + Address::from_pubkey(Chain::ZcashTestnet, btc_public_key).unwrap() + } + + #[test] + #[cfg(feature = "zcash")] + fn test_zcash_tx_bytes() { + let tx_zcash_hex = "050000800a27a7265510e7c80000000085443500000160ae0a00000000001976a914f97c1d1cb17b5657d889cf7503a69e53c9da081988ac000002af1f4b1e9f842c3f878efee2b9ab4403500c5fc1ab3eb14442ad3463880cc387c8769e71b1a858a7aa268cfc17b6c4e414e69e2cc257e2f289d4780516d4a82149fdc0abd4061824c43a7c17f94a7bc0736eeaa20429dffca2180c8094ab779acbc03ab3bdf2c30bd31a859c96032ffda141549d189045ec43ac1b9dc6f15c2875afe1f1fa3ff2bb4b03599506062fe118a097008de7d5af99f09a78819bb93fe143e8c888eeab1ed2421660ca46d416bf6b9da01e54663bf18ad79dfe6bcd46587b32fb47efb0b49e1a99f6ec6812fdaa9d178eb21f6487adcdac88553580c61137b7b7169029cb26bb8c09869ba866ff8185d4d31e2d6c431ed86338fea3db03faf5b164d851d00e4dbaf608b8415a31196d3fd33246490252c8f6bb7bc3dfe0d9400bab0ce0015a9e23cf730e5aa5211d9906aef91c4591c4d2b2e3cc913809990f1bd0fdc3b828ea5fd416e1459a12364b502f65d9eda45615850942991ed3f7aa4b12700c6493bc89e969b2320d11d0b5deea814c2d9f2c20fd2635f7eb1172a9009ed31fa3e281a7632b68a023fbf232e890023505fa08f806c3f71b31f76ac480795e01d8249758c30b18f9be9d126b077a338bc1aa8e2a4d1ebea6f48717cad84488be5b7f7a1a323f4016c0d38c2bba099420ad2bab8a68068831264de585c8f2d62dbe8bab7b692a6047793774d40b9b3311a824b670ea64fa80389a1923f49e5485c850665c53d160c2bb4c230576099057f9167270678a82e80ab4e05c28e1ecd57b8145ad3b11bbca8c160478f44086c5f2d43417e78d2aa19ec136f9d6e34306da957990a125e45df52dccf8efce76df8b5ec2fe3cc17b855c00e18ef62f84de0b1d1085973967222f1e7c8230943cf7a51c4c50ce343117b278c81af6241ddca5af47e2afef683fc66aa194d90fcefb13cb58a478e9765e323d8a0da292c5792455dc9434e53292b8b8cec2da1c1b2c39f00aff5081aa21adfbdea6f7fd153794bc94701a7aae3fa5bc94060452ca1c1d965fe4e37f60315f87d091c79caa9dfe33261c377d837ef273436615d295be073385bc4374bf743aeb4df84622407afee6e63a8c9389ece0e50c8ebf031b0a61dd9615c5ef92d99e3b01345afd1cfa4216f10da8ccfc2d56314c5c0f49c69f33a19c073e655f606b55ea19b91bdcc9a7d4c054e9e3bbd5efac4ddd213b666ea6f1a0612e765a38a94fc1edd5f6c6dd5dba50db01a1172436ef86e30fb8ffd055670f9fa83f28d5a757da33feb35ff4ada98bbee715f2b9883f109420943462e51fee9ad4282ee6876820c74d32b30a1bcaa5b22ddc73dc62e28ee516afb866a0305dc7e8b3ca3cf380187b33076932fbbdea8339f59c6360adefa086d85019fdcf441cb724a0f890775e6fd4e00ed950bf618e5f258e1c485f9c3b512e616126eba07b3d4ae08046a446a52c8107a5a62ef8ecb4662dfaf53b05e8e71bcc537a3e32e225920a4b989432ad47b5c31897919bc989c6f48cac1579c1f525b481c0bcc9e019bae4d9eeeef18e4e33888f1cc00df3c06408ab6b4064b40496dce7c905b8c19335025debe5f54ad535d7395e3b4f75989983ed76791230ab52bfe98e562175d4f0cb1933a0929aed49a7a0e1e76638dab0e3703f5577ba6e3088f44e590633cf13d9b679ca7f7561e9906bddd6a05244be421003b3a1a54693cbef7ce2632040b4be9072f7a632fa3a9afbfdb25414bda9ee82866c439d0fbb148e294e9bcc8759aae703d37f9d492c3afddf5b1f6fe93a8b059e7d6c5ab53667262656746e405515fe4a7ddc156b8e8f3c6a9e345fdf49e6ff654b6111022198324655a522b58c6ef9a14b78b739802ec8b87b5cb1ad986aa769ba2958ee98bcc00f7e86e806f2d344e22ec25d57abefa84d3de3440c5fb779784c5b0d240fe9cea509d76744a8a06ff0f9caed6095f0f56c89451cfef6eec39470016c9b99cd7736821b9b426d4b3731faca55e903756e9506712e5c0cc63ea040e6ba5396797e42edecef69dedebb725acb6678fc9ccba24873659a8b7a253dfece816d5f1028197ca09574488c4405b2b3129d2dde643ba6852878eb337baf9f7a2388e6c29a9ee281f3fe1c96647c5bcb485e60953d895e4016cf2a679425450de1c7ffe99cc4dcc141dde4c480b445544bda0afb18c0465c75de100b24547ec90c978939ae32a6d14b5a61380382aa75652963aa9d140a66ff0016bb50fcd11749a24601224ae4f444ffb5d80a618f7843a6b5a88a74bf2149a8a3e08be36194e76902b98a1134123fae6474056b860b474762125d1803f8e80a000000000032861203d8fd375ba5aa472daea41c5c9d2a015551d67437e267bbec1fe2e109fd601c6e99d1bdf653d9b2595d9f9898341989654e99c643e98bc7cd196dd5d946630009a3c02403849254ca01f6afcff0c28a622fd397980e2fa8f35b49ace0ece09b75499feaf26293d18f917ecde6bc9f930614661ce1d32d76d0e863e520fb4583174b43c77c3e6859b53f02447159dcb02069d7b5eabf504c96829c5cc50d742043ddacf37eb14759b345f3851d4aa397c0b4fe99bf0ff748e2a538ace4275e22a063da15d86ff72572c480c4fe1760318b141c440732d72947da4e27578eef24762543315eb0e4d8c487ca449ec2a1e60b5205722f0d16e5583831bb7fbf9fa40a0cd5a6a9ec36d8f68d779c36fbd09122f59f4f9f4179c5d45a2301a5fcf292600e3e930c4c9819a659bce76799df2c7dd78163a59fe4aea354aeed56a2ea15d18c9bb5001c1774930eca2ed94f1010785e3c92bfb2633c80ea42a3373d301657e21fb540d6e6dd869c79f6b846ee225762ebac40d0949e56d4f85f09e4ad981e7af031a0b5e8fac04f0a6fe02acac9a499903fcdad4ec8c997f1cd88cd2120a53f2726ed09b8d05ded97b637401bb74f5a1cf408c60a112b7f4bcfe18685baa22eda0e16f02f423c8678e7cbbcc99f132c1ba2c0d6c05ba83d43d7b5b5ab0cb00c1685fff14e5e8592edae5a276186ca43481e55a340761ff4a29f0b7def0a6f23ea96fdecbbfa53bc3270d0acb38fa352143ba2632565d623070c9b72ed9e7e0d43e547d7864fcf507b110c3a36f4a6745e916d945bc3803f6847ca774a8fd9d2104d9436efad03ac5daac19cd7cb1f70c542e971b817d5ef3566e2ccac26c48f7d49dc2176ba1c24263c61f70411e76ab1e72714c8cd0ddbc8294392d78504d22cca19b1a532f899cbbe079599caf35478719d2af9f1fb767cdc5d7b031e14cab5d5f7205aa1b33c01cbfa1347eea2d8b7309bfb8f54cbc5764d87f5249885ab87918ed38a48a4a8440f17b9b799f07ff4f86b0ef5af6a9850f2cfe8282427873d94c7f5d93b7edbec8cdcdd6044ba08db4ae876236d16b6fccce61130b1f32d446d8d3e74de5d412be069dea6bfa3f55f15184acbde502cd55b5ab653146e933424bc85dd4af01c603a205fd2343aa7fed6867051e7905394bbb3699694ec6a6fd8b68f41ebf2060e8ea54e45e9c294c9121408128b920d980f2bf915b3781f1bec74afbf30958b20833806fc3d7982f982154bcfaa9d88a0f87a0df2a63dca46c9d4bed436cdf50e73967b4405c3cbdcb3ec2c1c0f872d1884cbb08992a2faaa829d590ed750a7783c36ef19c9a76a4bb4a5bd1b84f2eec26ad62f0d8279e42cf0e017785fc90aa0c72e734abd9614856e0ae86a49a0f4574fae547032741c741345f372a5c823b952dc95b10c13f142ef084d72661f33395c4ec53792f4d3b5cbcd67e1603c366aa8dd70dc3a7b3bbc81c0069eb7c69ece8dd744988c6754178bbb0ed8dc72cc621cedee47a202db52cec49cab2fe198325c2fd30b17bde4358eb25d7d573171b862c747a33c11df2e7bb5781e79dca58e181b8c2b116bd8383b15aa78c1732c8e6155fc3f095fb77e1dbd2ab9dfef26a936d489158e1d3dde3eb1d3f727548138d9faf5f55ced3b8e014051e58bd99f752d58e839999df3f60f903b768d2a1ff097769664acd034e6a0f3596985db17da232f29863267b10e0ed34c71ded167fb2b4feeec465ef6af0d17d248d3cc0e577e027c51a7d90eec4a90783f37b5e48cd1c474a590e790a548ff204bc775ac61c66777f0230d7f107d2ffedc6ef2b92dda1d7e79f40d5b85a2eaf623f543165260b8e67fb0b58ca46dca0c4fc048f8593c6223d0652f57bfa1e33d7923644afd4fb0c1e4028ced5c9e9296780570644225b20631d1c53fb904e932e7620256e06777bff2a89d7dff2c7fecb5a2d48ab2b9abdd8021938efe8e78f5ba13452100d87aee8d298a7e1a80b844dc6457be7c28f6715a7de70773bd3aa380d8969e105ccad6f22dce0ae344f0a57a4fd63dc9ecb4aa2eb17c9283cc701123ad448b994e3d5e8523329d31dda9af4dde215f7c35033f527074c1d746e0b4ff05d93996d0afb9892dfd7bb6ba7097bd8d4b234b6172b6948f7fc80e66c7bae3bb211b68676fa3d21152eb38f27cfbaa3b718b3a7462eb0d2ec7550b6046c52b70ee54d5035bdc4f8f93bf87cb2bc0a7b899d8d51c55527efea11917b0d09791961b564624f978a6881b5540b3792f07f93c180a856d7553f91079814590f1d76c2dc0089036a61d38c81a8b24e6a34da7483a1cc2174bcf133a651afc145b8702bd6a20ba888f54ac50da3d44a9cb1b7c2248cb5da3d41e60cd1ceeb5892f0cf72b25bc3e2559d98c068c02d26ed1d04e2f9bf89569675821f1feb4e4445125be19a354ad6f870a3b57fd0e69d04b90d622b338e22d447b3b5647c15d4282e8801f9f6dbab5f1cf297f1ab7230d04433a5c16bf06830f3160b04dce7b2a3ae4edc4dd63fbb6484133b6ad1f580456a7f6cb6b7128f4f52e6e066a8476ee181d9298f69e07e8a153023dc5f2f581d6caad44b63be0f87433a9d8aee7a68135bb683a1bc9fe169ea41bb8f9be123e8723cf3ba0abbfbd2d0ff15dbd374ce3a0e6a2b81e557584095530f1d05bcb2cffb079ed056ff9b4d67cfbce4af3a3d52f1c300621d7045b56010b8d01e539396079ab966126391263b722dab43f002516d4cbd890171d5f01852a3b5c4613086f9a1fe25389100f3e442a20a106cd9c73ebd5be2dfdc691a8d729bd56508be1d5edc02a2518758084f05d42059c1e352af53274baa8ba317a931c8452bbef8f04939343433afae01b9489fc10d5651c0e491265e0fa65f114503bddf462b216ac6e26000f44497e577377ee6e2d3b3af7c34654dc89adbcac132ee78c9d5166b5702ed5c5222b98e1c5176686e3d19a86c3b2eb1f95b1d2279f3ef11c865637f76dcc241a8e71e8c833c949746c348e403d463bd87c6c53eae605508480e108c5387975b6e98285c3035535c0efd3a3fc21957431f82717abfe03e71cd9a077f61cef2b28e7e6480b769b9c0f1a466b8201ce37fe51b13574c73da11cb5aea806a1438059b2ab42f912812f7b4ae4363a16688eebe733730da70c1854d8ad2ffdadbed118e91017aa516a49f19e9a68850f79c6f17d470198422ee1cdb6c1f5e900941f6a5a6757bb2f72c1183b7e2d6a6b4510e6a46969ed3e1a2e813513583d65c2f539844c4bd0e97a53b6ca45fc2d7156579366ab6ff9542888a56728404d014eb4019994dda53aee376e57719f081f939f85392e27fbe72a5cf65510d148270d1ed22c2f46863e325133d6a722fb55c5a900f7308e874317595ef4cafcd40fa844338344c0dc4e8e05218ad3565a50ab9143b63b333e360d1fbf3fc9fe341bd9403bc3013f3440039d923b2b6adc9e2435e8b9ffb78fce1b88109e13c948984ddc8924f5da1e77380f5a2f7ad81d31d047e6be45a774ca3082353dc9ef14c421b8129042d8ca11cb77806f3f93fdd6f537ae731f137728120b5750d69ca674545cf09d81f29b0f53e73373383befbb7ddaa39ceb45a84a0c268687fe307a3ad501e9bb0d19a5b223d67a8a32d6e3c2c3e090c06c92834b09634ece2fcf520a8337e4b28da57410b7084fdeb1f9b55b308b17164cb72e9503600ce2ca6c70bc32e4269bd87cdf22c1e45a6cda1bf658c98dedd878186fab3978cbe48184e02a05b39c4c286fe20158e581d3e2b5e4e5e2d438439125fe2e05d3f2fea69d0934c7e6eaac4e793ac5a7feaa4760ce7575677ca9ad9d1c8c803ba96e937d7cec429a770f68270f1813fd74dcde6956714650cf54b3ea24b8771389ecd3ee1ac0c4eaac2341918c819ed65af8671cd3bb58c0bb6c1a27db24f21686373dfe2274699c54c16c4986fb22d9a8caae2222be12d235c78a50a2489610cdf97c7761e0fad69c966ff8f1a41cd2307d1f4adbb094aa34eac0cbf8b6bf0aa60aef8314c37d3217bd36b7726c51bab1aa8705215b0d1fe973f59fcb8c9513e3310c32b88ae4156a292fa1d02acf6204555fd6b692f80346a100b3c2232b158a94d7558facb635b69dc5c87c7cf2d2495ade53fcdb50ae0163d4798749d62ecedef3919193ea4c0034403489d09c2f76e7f251dfacd60f9bb824579e6e983cfc448d8afb75088891e8bc5fa0854de7457c5810106751a7f412b12a995b8c29a4b6734a0477a373be1674007d969465f27e95c9a0a3c292a3b51272584ec802471323ef4b8e0b39cb34da4b7ae870728464dd73c5a7c5bfcf65e8afc800572be59b9f1b6f30a42215f75b56172a2ec29320288a666623fade667b4d84b16632c5972ee16fbd69fe81d194ce510cb48f4d47081ace37ee8b1531a90f9831da160cda269151dd7dbaeeeb7942c513c10a19ac9f38995f2cf49c1d3c03c909ca3a564458cc4e483790e9be82047fbf84084a97ad6c452c29e794b9046b69b93223281737979c23047f95c41d29dc552ad85c3db2e7da9003b9105e030dca309229e4e89a10fcb0f5456bc06283ccb3938a8e0d5958ce693911f3fcfb7e34d41219d1c7bea4683396e6ac547b62bf80f0a84e5381825baaab9fdf346a6a939d6538e535a028e1a0f9e545e11441f8bb717d67943de3f1ff11f212c0af480b6d6927b9a2631176b63536d489c1a8a4ba7892a84b393e5e8793458a12ca5f795651021ffdb236344adec8bf6012d22260727a44777b5685575724525ce1b82fd83b2689944cffc8388ae4527d7eaa6b7708ac6f5163c2d62fcc13f9830bafcfd3eb1e9bebe7ac1b1a2d38b2c0c387e0156a9fd2be9d8685bf7b4442960045bfa0571817c966921b723d5d783a7245b6f98c8bb5abc08c1938da45d66ca89cc8bf243c860e264cf5906a6d9545dbe3778c2552929b2d8989af3a92e5df28872f63aa0ba09355d01f2b5a0f2d7d4fc20454d9e4a9340f21589cf2b9ab88dc19fd6774205b3988b719ec5d74957952c0efa64fd425aaab5367b61d9634344a0f32f7a01d0e61db9ae1e240078293239712d52f741b8e1aef58c3a4fa9c0d40a154ec0e23a7f12fb438632d64282171fe705cbcca36d5c331b4cc446b6f215833d941bd3d0cb2c8b63d579388c59096983c93da17a55c9f58e0da42f4a29e88be7eeeaa1898b624352f9e93dc8ee1ef0297cbfe0a38d19d89bf572f26cf80dda465e7741067b75a0f6f5e2079753a1b106a67efb204a121a78c7017487860360b90ad1103dc9afcd1f5faed05acafcf5b776efc91b894b5a8895eea5217609b5935f669049513305aa10a9f62de1f824f550ddee657f1b0d7a338b19d18e4b3e26aec17217d0f58a5aa6e39a835f3443f5caf522988294d756b036bed298c4693a30c933867c4c92989c122e0d3da3c3358b219ce8e9b2e99fe2c6346b9cb581b86f85c2e33d2d34b405aca83ac915608732b7b258965a55beee0f9ea166f558ad9d06d07d4137a030772520dbddd3a057e6d2ac4d3079a34731c56de2f8291b59f05330912caf5ac13c25a1dc2131f90e17998f14f62cc70831f43afb4183ee498e77c0566d42997468fa83a53899df2b1b67a0fe11454033ce03f7ebc5002218c2f4e266da31796bb06c1f6aa1c3f879d5d21f9f81062bddb28c805050fe63a55789f1fc50dd67a11bbbfd870b53cc296d719cf403663303e3503547e323f10f74bc707648a3318a1c6abd0428c24df43fa0a208c67ad855dafb0fa115669664515dd2782a32098ef68850b7c2533d55af6560d248154148335dd84b2de3eaab3554c265ab235361b161ba52d79db25e24190ebe26795f78e4c1a97cf40284ad278f216dc27ed90c85eaa2b9cda7708c73fa45bd3792559207b2812b47da176282df71ca4e25dde19e9758e93b8dff9b4d6f6bb81abff143ad08c952e426a151db1f8326c37a54a3dee38de00dce18987c216dcb821fb76f06aaf596a1937b314a23535313017e5a3bd02bf3e6cfaa1e2643f0a26318abb70eed77fc9896e32d060693537069191a96db3b51b4978ed0826cb9f7a017396b99e6545b4c40c21e9c3f8215a584aa0c780ee42689f51a605631b9d7a9397734b9077d4fd5f00bcc1ecf13380598a8025195cb9cce565af0012f7d8063d9337088759fcd20101d6ffa8a3390b006d4583e3291b79e6cdf0813bcf88d9955f98d2371aa2b4d91dd555129714823cd9d7ac58695dbac826057b8a76818891c37395973987ce6a2be088eed92fdff7bd9852737f84461e304a96b8a258ac201fbadf618954278ff9912e8a2e2be415f7ae6af565cfc8724c8f4d0e1df9046d6bcd49bc3f77daaab84f24ad3530d703c16466ab20c208e3aa9722cc73da0d5107644f6fd0f4bc9bf40149584e239e543f24ce5765d23483a337e0ebf7ff6e1e00112108a5a50e838892b86a92226cd615ed8a5920c73f63d2129c853dfb9a54c1a7e5210850d5ae7c49243b423a5471d381acfe94300feba23b166859517f56f91cca5dbee19e15e87cf9aa4028b42ceea7efc56b1a6f828176ca63bd27aead72bbefa5312c7a4a6fec59dd1f291b0a58f650f3b6ce5f437519edcbfc7b58cdf83da36d3baf5762ed625b52411380acfac0109437710443ea4dd5042013d1b77c27c330521ada326fbe94a28003d7e270728d06eea4d652317d81d2845384c5f774b8906e2644764c3dc67acd3836ea045f046f5644b79a972d42864a0367d3e6d26112c88485d63ed3906fa336bba0a8300ab56ead40af49d04dc32fac383eb4a876534634197372b0f9001e0b9d01e6ee17484f3276907314b510f86b4b22be1cb84fa5dcdb67e7874b531e12ff6896e5041eba791f741151362c03ca66685b4e85f8cd38595e9d4855655426b23dbda6a50ec4b43091cdf2a6adc32797cb3af7e73404f40e158a06505feb155aa6e96dd4973aab68c0bdc2d9ae6dbd84b67d2742802d983eea7ed679328626cf4c477bcdef597da667d294cdb82a0a7e7fc6717839d1da8a8aec5e44e7123a1246f7fbddac2f5c48e69cefcf5eb2a628caa77b91d28be700985d261cca3c2048c35417c34204e035f07017c55176cd4bddb0b04d3f98a756f39856baa43e11e8777e4a9d1d79a5ac8c9413e913e56a0814314cf09ff1a51a02f4a8e51b800c998ed11b996a7828aa2fa1d384245fe097954a5ef75c6c19125f01027c1872240e3c585a3b9769d2c8d47a91a5721ae5503981b2fc02a7f0e6c203c195facd1544640eea2c354976f79acb57ae479c235522f30267c2bcf0eb186b8b0570620e38072041309f3ee79382af7fd1e087c987005210a594ac452cf4f8cd1befd81a844e7ed2d13963ef77be237210056e6251712ef6bf3cf686ef7ec62bcec5fd314a2f80e036167946645cd1d40399d4c78aa45e78cae99f9e7845a17e1162ad2dfc26426d830a1f5c0846cbb01d85cb2b5488727df8f4e77af49943bbdc85f412c1986315bfeb7f4b2761463f4e3992080ec64bc47485f5388c0c147a77998d344a51d60be16438a6986c65028e9a390691c9aef240f7f0514c0e1e9b3f2849006df8949ee45a5c5b2996fd1c497a7260a6cd5fce873e4d4185ca5d79da2ed7089cc1caadd75c9796595bfc3f6010b1817044e9b8123277441021827fe1c59c32c46359a2511dd1e4b751cfde29cbae57828a815764c127cc56d219020536fd3c41be7fecee24c5e7529117d3b6c70c751732fcb67f1b39e2a5e901288a6db820306b938ab21a0b808aa8f65e3c0b39905fd782f840beb3ff377dcbb46d5f75328823394fb6b8ff809fbd2eafe5be484c8abf5e30a84c70cef6bc175a343a2d14fbf531fadf8dadece387959a3a433a98316a81aa3ca1242a2d19556fed88d912e6749c8a941f20163165d0a1c1aa81d46012025a31156db63f8a1e3573bcdd1d5bd67413f9432ab50b741addfbec64b4f14cfd59d95a5b60f5b9167951676614f14b00472f4b4c8cfe65da0da7ac73810049a9c3609cddaf50f089df3986df1a51830004586351673cc9a9ebccd1edf71b0597b6369a34187bcd4aed53b994051a2b1c26f2ad6425f7cb22a6630739da814db1069f929a884173eeab9ec6f818fedd22eb828161978ab7b2a3f7396f8d23536ca2cfa4e93efd1224d7efa5c139cea99d89cf19c75aa3b305b783f58df099ed0ea64466031d764e3494deff2e3929f94dcd62b4eaf989d926fdc5bcedb7251b0af356a340c5e2564dfce9534723aef055882a88c0237455b2a26f6eadfe627ca776ecf208225730aed3f25d621a3c8ffb500ec5e2c523928ee49905486cc7cbc915b2519abadad770bc7e6e2f1712d07766b4d73a0f1b751de3bc2eb41b9b879c755af9e1b102569fbcb3d71a0058232dd51c1b9aa70dd23e82a9d5227df6404b9817460b636e3ef720eb045b370fd7b88c399710f78741dede33ee4d1a19c28f4ba82302fef4a7b167346ec03fab6e8eb489dfe5e2a85f9dc9da42b047283bdc1d1082ca2649a6d2fe889e9639e9c0f74f59aba9180aa9f9ef3d2ae4834307f2239794cb372f67d8e17046841ce83a8713bbe43b8a7bfb508421a3d6d49631407663e4eb909fdf39fcc166ec13346a4259cfc07bd4c2fe00861c9eebc78076d9f5bb8757f2fad939b493f0d81f373cc09e74f1db62fff487251ac0474babb4a42ba36b63bb77e107f4dc45fd33f40c00f0c0b043b480f15fa37c9397a3ade86d168e33c18c8d7d77e8f179d02986f402bc28f15f3715b17d2f932bebda8e0abb2176e4339a41c15b4fcd109f328ef71eff50ba94e591c12c4c197b006fa684f88725551920a8a419cc745b9a1b3594d80af9f6e5a92531a858ad72b5f0a0380bb3b3afab7249ac66769a5a083766ad5f00a21bd095874bb06e8183debb71f328eab7d538ed47601abd2cb4e0110229c450cbc0a3dacf77ca1be7d4c60f526564ffdbb13a0dfb03a3167be9091c282121905f21470f9b811bd730383ea1a28be7d048fea316d328e82e87917c0fa581b0c5880af1bcda5ecde40d116050a9bb2d1c5bbcac03cd65fff023558e0a99ef8f8e427c6b20885adb650cd19c3c7588298469ffe94212196e7cd2ca420f690f9158bbaf204f47e3c5ab0a37234946b7e482457fb6dfc188b864760b590c918d619e987faca0768aa466c274b3a5e1f16a37686487d147ba6ab1ba2b8d9be1fb26fe4f5baa94117d1f081da9adb3a193825630343a32fac6678b4a90b02f15669de1c26112fecb97b2c1d5d2fdad12aa4bb54a4b8c5c17c6970c0aef5a34c4a5760b8bc9071270de9ba3a015db8f8ed5e899b68a150720cefba8ea62bf30437a5a3e88eb76cd486924e5a22d8f93e0f281412a91e713dbcdf954fc6e8083eab4fa2e062ad815bb00166ad9fa66a6966f277dae5a5d920632659f727cd803692cdf439b5bd91dfb5102ca002ee0c045c83d2c001d1671c8375c1c72b9e02b6cecc1851f150056c7a0c280b711b68a376072d7ef7b42b140a6cc72677d789c8b3a72b93eb246669ceb58d993cc1b9042270625301b2cc84efb2b87a688b922bc83780d33db8c017258c49a0645156814bbfb910a158110bbe33b101932e985a4d39637487ed1f59cbd1419e6001f6be2065e981369388da39d917a2524bfa4261babd5dbc06c932639e5937c7f8e6ff88515f10115b1494ca4b74ca38be6be72bd0329ed8db64d22f612fa91eef917e528afce83866ce743784ece16ef98016a1fb86b358df345eecadc7a2dde8301f73c3633456cc55c0f606afd7680a98cf6cca852a43a0ba9b525f52fe9713f470a62e3902683f0757eee89bf16258a28a59e028c48378918550842acaf298c19956deaab130a625b771a92ba2d7ffe36750308e61f0b5ae4e2de3d4da9d4f53fb0e60524532574ac28fae9b2712c00884e68a6b561f75ca4bca149bcac141fd3be1ef0008356874eaf92c1f3abe032135f55569e8eafa1c1ad617b3d26a337e5daaf041f5c419f2e6c5985b686790c95ec70f70baa822b70e47036c51d10e80f2e3bc76f9cdbccdb649efd57612ba537f519beaf6cca54585e71542e1d355588255a95709f53c8e1025da0ddb2050ea0b8cbe6b32ad297867b2e362077aa1012c0344333cfb5ba48cda7469e78984eae21c4454786676a89f2ee997a8ec1e173f8422f3a98602882218ffdd1d3476c19f550b84e72993dbd2b4aac4bc804ddb5b61f5e3887f6d6db594bfe845296d50ee33e3f35b677dc781bde42be674442413f51447f8bc3886bb41d8504a9fed33db491bcc0275dede8e0fd1d9ce23bd83424ed8a93d3b91406830924d2a796a03b8a75ec16d80cbc4348a4b5baa26a8287414f74de464c1718d8bc6c4f3c15b31259f5e00f7a96539e173a0390bbbdbe84c5262a8e39a2fb44c02bdfadc4c392270863fbbf3855bb62273a2268b5c168b1bafb55ba4a086243efeeaddc98c6a907cd2f36b62e83656b05c44d943e9f0c8855c386fe09a65e5f9d6eddeaf0436e1e730d6a94feb3ac4a42f06517a11b25eeb97a8562f81ca354455adf40415ed901"; + let tx_zcash_bytes = hex::decode(tx_zcash_hex).unwrap(); + + let btc_tx = + WrappedTransaction::decode(&tx_zcash_bytes, &network::Chain::ZcashMainnet).unwrap(); + println!("ZCash tx: {:?}", btc_tx); + + let output_script_pubkey = btc_tx.output()[0].script_pubkey.clone(); + let deposit_msg = DepositMsg { + recipient_id: "omni_user_account-20250625-153431.testnet".parse().unwrap(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }; + + let path = get_deposit_path(&deposit_msg); + println!("{:?}", path); + let deposit_address = generate_utxo_chain_address(&path); + let expected_script_pubkey = deposit_address.script_pubkey().unwrap(); -pub fn to_psbt(psbt_hex: &String) -> Psbt { - let psbt_bytes = hex::decode(psbt_hex).unwrap(); - Psbt::deserialize(&psbt_bytes).expect("ERR_INVALID_PSBT_HEX") + println!("Deposit address: {:?}", deposit_address); + println!("Deposit address: {:?}", deposit_address.to_string()); + println!( + "Expected script pubkey: {}", + expected_script_pubkey.to_hex_string() + ); + println!( + "Output script pubkey: {}", + output_script_pubkey.to_hex_string() + ); + assert_eq!(expected_script_pubkey, output_script_pubkey); + } } diff --git a/contracts/satoshi-bridge/src/chain_signature.rs b/contracts/satoshi-bridge/src/chain_signature.rs index 10ec76f7..daff5491 100644 --- a/contracts/satoshi-bridge/src/chain_signature.rs +++ b/contracts/satoshi-bridge/src/chain_signature.rs @@ -1,7 +1,8 @@ -use bitcoin::Witness; -use bitcoin::{ecdsa::Signature, hashes::Hash, sighash::SighashCache}; - -use crate::*; +use crate::{ + env, ext_contract, nano_to_sec, near, require, serde_json, AccountId, Contract, ContractExt, + Event, Gas, Promise, PublicKey, MAX_PUBLIC_KEY_RESULT, MAX_SIGNATURE_RESULT, +}; +use bitcoin::ecdsa::Signature; pub const GAS_FOR_SIGN_CALL: Gas = Gas::from_tgas(50); pub const GAS_FOR_SIGN_BTC_TRANSACTION_CALL_BACK: Gas = Gas::from_tgas(30); @@ -78,12 +79,22 @@ impl Contract { sign_index: usize, key_version: u32, ) -> Promise { + let pending_info = self.internal_unwrap_btc_pending_info(&btc_pending_sign_id); + + let public_keys: Vec<_> = pending_info + .vutxos + .iter() + .map(|vutxo| self.generate_btc_public_key(&vutxo.get_path())) + .collect(); + let btc_pending_info = self.internal_unwrap_btc_pending_info(&btc_pending_sign_id); require!( btc_pending_info.signatures[sign_index].is_none(), "Already signed" ); - let payload = get_hash_to_sign(&btc_pending_info.get_psbt(), sign_index); + let payload = btc_pending_info + .get_psbt() + .get_hash_to_sign(sign_index, &public_keys); let path = btc_pending_info.vutxos[sign_index].get_path(); self.sign_promise(SignRequest { payload, @@ -106,12 +117,12 @@ impl Contract { impl Contract { #[private] pub fn sync_root_public_key_callback(&mut self) -> bool { - if let Some(result_bytes) = promise_result_as_success() { + if let Ok(result_bytes) = env::promise_result_checked(0, MAX_PUBLIC_KEY_RESULT) { let root_public_key = serde_json::from_slice::(&result_bytes).expect("Invalid PublicKey"); self.internal_mut_config().chain_signatures_root_public_key = Some(root_public_key); let change_address = self - .generate_btc_p2wpkh_address(env::current_account_id().as_str()) + .generate_utxo_chain_address(env::current_account_id().as_str()) .to_string(); self.internal_mut_config().change_address = Some(change_address); true @@ -127,9 +138,10 @@ impl Contract { btc_pending_sign_id: String, sign_index: usize, ) -> bool { - if let Some(result_bytes) = promise_result_as_success() { + if let Ok(result_bytes) = env::promise_result_checked(0, MAX_SIGNATURE_RESULT) { let signature = serde_json::from_slice::(&result_bytes) .expect("Invalid signature"); + let public_key = self .generate_btc_public_key( &self @@ -153,18 +165,32 @@ impl Contract { } .emit(); let mut psbt = btc_pending_info.get_psbt(); - psbt.inputs[sign_index].final_script_witness = - Some(Witness::p2wpkh(&signature.to_btc_signature(), &public_key)); - btc_pending_info.psbt_hex = psbt.serialize_hex(); + psbt.save_signature(sign_index, signature, public_key); + + btc_pending_info.psbt_hex = psbt.serialize(); if btc_pending_info.is_all_signed() { - let transaction = psbt.extract_tx().expect("extract_tx failed"); - let tx_bytes_with_sign = serialize(&transaction); + let tx_bytes_with_sign = psbt.extract_tx_bytes_with_sign(); + + // For ZCash chains, use base64 encoding to save space (1.33x vs 2x overhead for hex) + // ZCash transactions with Orchard bundles are larger and benefit from compact encoding + // For Bitcoin chains, keep hex encoding for backward compatibility + + #[cfg(feature = "zcash")] + let tx_bytes_base64 = { + use near_sdk::base64::{engine::general_purpose::STANDARD, Engine}; + STANDARD.encode(&tx_bytes_with_sign) + }; + Event::SignedBtcTransaction { account_id: &account_id, tx_id: btc_pending_sign_id.clone(), + #[cfg(not(feature = "zcash"))] tx_bytes: &tx_bytes_with_sign, + #[cfg(feature = "zcash")] + tx_bytes_base64, } .emit(); + btc_pending_info.tx_bytes_with_sign = Some(tx_bytes_with_sign); btc_pending_info.to_pending_verify_stage(); @@ -185,24 +211,3 @@ impl Contract { } } } - -pub fn get_hash_to_sign(psbt: &Psbt, vin: usize) -> [u8; 32] { - let tx = psbt.unsigned_tx.clone(); - let mut cache = SighashCache::new(tx); - cache - .p2wpkh_signature_hash( - vin, - &psbt.inputs[vin] - .witness_utxo - .as_ref() - .unwrap() - .script_pubkey, - psbt.inputs[vin].witness_utxo.as_ref().unwrap().value, - bitcoin::EcdsaSighashType::All, - ) - .unwrap() - .to_raw_hash() - .to_byte_array() - // let payload = psbt.sighash_ecdsa(vin, &mut cache).unwrap(); - // *payload.0.as_ref() -} diff --git a/contracts/satoshi-bridge/src/config.rs b/contracts/satoshi-bridge/src/config.rs index a1926b2b..16141891 100644 --- a/contracts/satoshi-bridge/src/config.rs +++ b/contracts/satoshi-bridge/src/config.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + env, near, network, network::Address, require, u128_dec_format, AccountId, Contract, HashMap, + PublicKey, ScriptBuf, +}; pub const MAX_RATIO: u32 = 10000; @@ -39,6 +42,8 @@ impl BridgeFee { #[derive(Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] pub struct Config { + // The chain id: BitconMainnet/BitcoinTestnet/ZcashMainnet/ZcashTestnet etc + pub chain: network::Chain, // The account id of btc light client contract pub btc_light_client_account_id: AccountId, // The account id of nbtc contract @@ -103,11 +108,13 @@ pub struct Config { pub max_btc_tx_pending_sec: u32, // UTXOs less than or equal to this amount are allowed to be merged through active management. pub unhealthy_utxo_amount: u64, + #[cfg(feature = "zcash")] + pub expiry_height_gap: u32, } impl Config { pub fn assert_valid(&self) { - let confirmations_valid_range = 2..=10; + let confirmations_valid_range = 2..=100; require!( self.confirmations_strategy .values() @@ -134,8 +141,25 @@ impl Config { ); } - pub fn get_change_address(&self) -> Address { - string_to_btc_address(self.change_address.as_ref().unwrap()) + pub fn get_change_script_pubkey(&self) -> ScriptBuf { + self.string_to_script_pubkey( + self.change_address + .as_ref() + .expect("ERR_CONFIG: change_address not configured"), + ) + } + + pub fn string_to_script_pubkey(&self, address_string: &str) -> ScriptBuf { + let chain = self.get_utxo_network(); + + Address::parse(address_string, chain) + .unwrap_or_else(|e| env::panic_str(&format!("{address_string}: {e}"))) + .script_pubkey() + .expect("Failed to get script pubkey") + } + + pub fn get_utxo_network(&self) -> network::Chain { + self.chain.clone() } pub fn get_confirmations(&self, satoshi_amount: u128) -> u64 { @@ -152,28 +176,34 @@ impl Config { keys.sort_unstable(); for key in &keys { if *key > satoshi_amount { - return self - .confirmations_strategy - .get(&key.to_string()) - .cloned() - .unwrap() as u64; + return u64::from(*self.confirmations_strategy.get(&key.to_string()).unwrap()); } } let max_key = keys.last().unwrap(); - self.confirmations_strategy - .get(&max_key.to_string()) - .cloned() - .unwrap() as u64 + u64::from( + *self + .confirmations_strategy + .get(&max_key.to_string()) + .unwrap(), + ) } } impl Contract { pub fn internal_mut_config(&mut self) -> &mut Config { - self.data_mut().config.get_mut().as_mut().unwrap() + self.data_mut() + .config + .get_mut() + .as_mut() + .expect("ERR_CONFIG: contract not initialized") } pub fn internal_config(&self) -> &Config { - self.data().config.get().as_ref().unwrap() + self.data() + .config + .get() + .as_ref() + .expect("ERR_CONFIG: contract not initialized") } pub fn get_confirmations(&self, config: &Config, satoshi_amount: u128) -> u64 { @@ -185,7 +215,7 @@ impl Contract { { config.get_confirmations(satoshi_amount) } else { - config.get_confirmations(satoshi_amount) + config.confirmations_delta as u64 + config.get_confirmations(satoshi_amount) + u64::from(config.confirmations_delta) } } @@ -197,7 +227,8 @@ impl Contract { { config.get_confirmations(satoshi_amount) } else { - config.get_confirmations(satoshi_amount) + config.extra_msg_confirmations_delta as u64 + config.get_confirmations(satoshi_amount) + + u64::from(config.extra_msg_confirmations_delta) } } } diff --git a/contracts/satoshi-bridge/src/deposit_msg.rs b/contracts/satoshi-bridge/src/deposit_msg.rs index f319b3fb..629e6f2b 100644 --- a/contracts/satoshi-bridge/src/deposit_msg.rs +++ b/contracts/satoshi-bridge/src/deposit_msg.rs @@ -1,4 +1,6 @@ -use crate::*; +use crate::{ + env, is_structure_equal, near, serde_json, AccountId, Contract, Event, Gas, Value, U128, +}; const MAX_POST_ACTIONS_NUM: usize = 2; const MAX_TOTAL_POST_ACTIONS_GAS: Gas = Gas::from_tgas(130); @@ -16,6 +18,17 @@ pub struct DepositMsg { // Used to support other dApps extending based on verify_deposit. #[serde(skip_serializing_if = "Option::is_none")] pub extra_msg: Option, + // Replacment for the legacy post_actions to support safer cross-contract calls. + // If this field is present, the legacy post_actions field must be None + #[serde(skip_serializing_if = "Option::is_none")] + pub safe_deposit: Option, +} + +#[near(serializers = [json])] +#[derive(Clone)] +pub struct SafeDepositMsg { + pub msg: String, + // TODO: add relayer fee support in the future. } #[near(serializers = [json])] @@ -117,8 +130,7 @@ impl Contract { Event::InvalidPostAction { index: Some(index), err_msg: format!( - "The amount({}) of gas exceeds the limit of {}.", - gas, MAX_PER_POST_ACTIONS_GAS + "The amount({gas}) of gas exceeds the limit of {MAX_PER_POST_ACTIONS_GAS}." ), } .emit(); @@ -128,8 +140,7 @@ impl Contract { Event::InvalidPostAction { index: Some(index), err_msg: format!( - "The gas amount({}) does not meet the minimum requirement of {}.", - gas, MIN_PER_POST_ACTIONS_GAS + "The gas amount({gas}) does not meet the minimum requirement of {MIN_PER_POST_ACTIONS_GAS}." ), } .emit(); @@ -143,8 +154,7 @@ impl Contract { Event::InvalidPostAction { index: None, err_msg: format!( - "The total amount({}) of gas exceeds the limit of {}.", - total_gas, MAX_TOTAL_POST_ACTIONS_GAS + "The total amount({total_gas}) of gas exceeds the limit of {MAX_TOTAL_POST_ACTIONS_GAS}." ), } .emit(); @@ -154,8 +164,7 @@ impl Contract { Event::InvalidPostAction { index: None, err_msg: format!( - "The total amount({}) of nBTC used in post_actions exceeds the mint amount ({}).", - total_amount, actual_mintable_amount + "The total amount({total_amount}) of nBTC used in post_actions exceeds the mint amount ({actual_mintable_amount})." ), } .emit(); diff --git a/contracts/satoshi-bridge/src/event.rs b/contracts/satoshi-bridge/src/event.rs index 6dbd4c77..41b1b93e 100644 --- a/contracts/satoshi-bridge/src/event.rs +++ b/contracts/satoshi-bridge/src/event.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{json, log, AccountId, DepositMsg, SignatureResponse, U128}; use near_sdk::serde::Serialize; const EVENT_STANDARD: &str = "bridge"; @@ -43,6 +43,7 @@ pub enum Event<'a> { GenerateBtcPendingInfo { account_id: &'a AccountId, btc_pending_id: &'a String, + external_id: Option, }, BtcInputSignature { account_id: &'a AccountId, @@ -53,7 +54,10 @@ pub enum Event<'a> { SignedBtcTransaction { account_id: &'a AccountId, tx_id: String, + #[cfg(not(feature = "zcash"))] tx_bytes: &'a Vec, + #[cfg(feature = "zcash")] + tx_bytes_base64: String, }, WithdrawBtcDetail { cost_nbtc: U128, diff --git a/contracts/satoshi-bridge/src/json_utils.rs b/contracts/satoshi-bridge/src/json_utils.rs index 083febbd..b4f7dcfa 100644 --- a/contracts/satoshi-bridge/src/json_utils.rs +++ b/contracts/satoshi-bridge/src/json_utils.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::Value; /// Recursively checks whether the structure of `input` matches the structure of `template`. /// Values can differ, but keys and value types must conform to the `template`. @@ -15,15 +15,9 @@ pub fn is_structure_equal(template: &Value, input: &Value) -> bool { match (template, input) { (Value::Object(t_obj), Value::Object(i_obj)) => { for (key, t_val) in t_obj { - match i_obj.get(key) { - Some(i_val) => { - if !is_structure_equal(t_val, i_val) { - return false; - } - } - None => { - // Input is allowed to omit fields defined in the template; these are treated as optional fields. - continue; + if let Some(i_val) = i_obj.get(key) { + if !is_structure_equal(t_val, i_val) { + return false; } } } diff --git a/contracts/satoshi-bridge/src/kdf.rs b/contracts/satoshi-bridge/src/kdf.rs index 44f7e7c9..e827c223 100644 --- a/contracts/satoshi-bridge/src/kdf.rs +++ b/contracts/satoshi-bridge/src/kdf.rs @@ -1,9 +1,18 @@ -use crate::*; - -use std::str::FromStr; +use crate::{env, BtcPublicKey, Contract}; +use crate::network::Address; use k256::elliptic_curve::sec1::ToEncodedPoint; +impl Contract { + pub fn get_public_key_by_path(&self, path: String) -> String { + let public_key_bytes = self.generate_public_key(&path); + let uncompressed_btc_public_key = + BtcPublicKey::from_slice(&public_key_bytes).expect("Invalid public key bytes"); + + uncompressed_btc_public_key.inner.to_string() + } +} + impl Contract { pub fn generate_public_key(&self, path: &str) -> Vec { let mpc_pk = crypto_shared::near_public_key_to_affine_point( @@ -12,7 +21,12 @@ impl Contract { .clone() .expect("Missing chain_signatures_root_public_key"), ); - let epsilon = crypto_shared::derive_epsilon(&env::current_account_id(), path); + let epsilon = crypto_shared::derive_epsilon( + // NOTE: conversion to string with parsing later on is needed to convert to the proper + // version of `AccountId` + &env::current_account_id().as_str().parse().unwrap(), + path, + ); let user_pk = crypto_shared::derive_key(mpc_pk, epsilon); let user_pk_encoded_point = user_pk.to_encoded_point(false); user_pk_encoded_point.as_bytes().to_vec() @@ -29,23 +43,9 @@ impl Contract { .unwrap() } - pub fn generate_btc_p2wpkh_address(&self, path: &str) -> Address { + pub fn generate_utxo_chain_address(&self, path: &str) -> Address { let btc_public_key = self.generate_btc_public_key(path); - Address::p2wpkh(&btc_public_key.try_into().unwrap(), btc_network()) - } -} - -pub fn string_to_btc_address(address_string: &str) -> Address { - Address::from_str(address_string) - .unwrap_or_else(|_| panic!("{address_string} not a valid btc address")) - .require_network(btc_network()) - .unwrap_or_else(|_| panic!("{address_string} network error")) -} - -pub fn btc_network() -> Network { - if env::current_account_id().to_string().ends_with(".near") { - Network::Bitcoin - } else { - Network::Testnet + Address::from_pubkey(self.internal_config().chain.clone(), btc_public_key) + .expect("Invalid public key") } } diff --git a/contracts/satoshi-bridge/src/legacy.rs b/contracts/satoshi-bridge/src/legacy.rs index ffd80074..3b0f33e2 100644 --- a/contracts/satoshi-bridge/src/legacy.rs +++ b/contracts/satoshi-bridge/src/legacy.rs @@ -1,4 +1,9 @@ -use crate::*; +use near_sdk::store::LookupMap; + +use crate::{ + env, near, AccountId, BridgeFee, Config, ContractData, HashMap, HashSet, IterableMap, + IterableSet, LazyOption, LookupSet, PublicKey, StorageKey, VAccount, VBTCPendingInfo, VUTXO, +}; #[near(serializers = [borsh])] pub struct ContractDataV0 { @@ -150,7 +155,14 @@ impl From for Config { max_btc_tx_pending_sec, } = c; + let chain = if env::current_account_id().as_str().ends_with(".testnet") { + crate::network::Chain::BitcoinTestnet + } else { + crate::network::Chain::BitcoinMainnet + }; + Self { + chain, btc_light_client_account_id, nbtc_account_id, chain_signatures_account_id, @@ -178,6 +190,148 @@ impl From for Config { rbf_num_limit, max_btc_tx_pending_sec, unhealthy_utxo_amount: 1000, + #[cfg(feature = "zcash")] + expiry_height_gap: 1000, + } + } +} + +#[near(serializers = [borsh])] +#[derive(Clone)] +pub struct ConfigV1 { + // The account id of btc light client contract + pub btc_light_client_account_id: AccountId, + // The account id of nbtc contract + pub nbtc_account_id: AccountId, + // The account id of chain signatures contract + pub chain_signatures_account_id: AccountId, + // The root public key of chain signatures contract + pub chain_signatures_root_public_key: Option, + // The change address of BTC transaction + pub change_address: Option, + // Satoshi upper limit for amount checks -> confirmations + pub confirmations_strategy: HashMap, + // The number of confirmations that need to be increased when a relayer not on the whitelist performs a verify. + pub confirmations_delta: u8, + // The number of confirmations that need to be increased when a relayer not on the extra msg whitelist performs a verify. + pub extra_msg_confirmations_delta: u8, + // Used to calculate the deposit fee. + pub deposit_bridge_fee: BridgeFee, + // Used to calculate the withdraw fee. + pub withdraw_bridge_fee: BridgeFee, + // The min amount must be met during verify_deposit, otherwise NBTC will not be minted for the user. + pub min_deposit_amount: u128, + // The minimum amount allowed for the user to withdraw. + pub min_withdraw_amount: u128, + // The minimum value requirement that change address must satisfy in BTC transaction. + pub min_change_amount: u128, + // Used to limit the maximum value of change in specific situations. + pub max_change_amount: u128, + // The min gas fee applicable for Bitcoin transactions + pub min_btc_gas_fee: u128, + // The max gas fee applicable for Bitcoin transactions + pub max_btc_gas_fee: u128, + // The maximum number of inputs that can be used for a Withdraw. + pub max_withdrawal_input_number: u8, + // The maximum amount of change allowed during a Withdraw. + pub max_change_number: u8, + // The maximum number of inputs allowed during active UTXO management. + pub max_active_utxo_management_input_number: u8, + // The maximum number of outputs allowed during active UTXO management. + pub max_active_utxo_management_output_number: u8, + // When the number of UTXOs in the protocol is less than this configuration, UTXO management can be actively initiated. + // The number of inputs in the managed PSBT must be less than the number of outputs. + pub active_management_lower_limit: u32, + // When the number of UTXOs in the protocol is greater than this configuration, UTXO management can be actively initiated. + // The number of inputs in the managed PSBT must be greater than the number of outputs. + pub active_management_upper_limit: u32, + // When the number of UTXOs in the protocol is less than this configuration, passive UTXO management will be triggered, + // requiring that the number of inputs must be less than the number of changes. + pub passive_management_lower_limit: u32, + // When the number of UTXOs in the protocol is greater than this configuration, passive UTXO management will be triggered, + // requiring that the number of inputs must be greater than the number of changes. + pub passive_management_upper_limit: u32, + // The maximum number of transactions allowed to initiate RBF + pub rbf_num_limit: u8, + // If the transaction exceeds this configuration and has not been verified, the protocol will be allowed to cancel the transaction. + pub max_btc_tx_pending_sec: u32, + // UTXOs less than or equal to this amount are allowed to be merged through active management. + pub unhealthy_utxo_amount: u64, + #[cfg(feature = "zcash")] + pub expiry_height_gap: u32, +} + +impl From for Config { + fn from(c: ConfigV1) -> Self { + let ConfigV1 { + btc_light_client_account_id, + nbtc_account_id, + chain_signatures_account_id, + chain_signatures_root_public_key, + change_address, + confirmations_strategy, + confirmations_delta, + extra_msg_confirmations_delta, + deposit_bridge_fee, + withdraw_bridge_fee, + min_deposit_amount, + min_withdraw_amount, + min_change_amount, + max_change_amount, + min_btc_gas_fee, + max_btc_gas_fee, + max_withdrawal_input_number, + max_change_number, + max_active_utxo_management_input_number, + max_active_utxo_management_output_number, + active_management_lower_limit, + active_management_upper_limit, + passive_management_lower_limit, + passive_management_upper_limit, + rbf_num_limit, + max_btc_tx_pending_sec, + unhealthy_utxo_amount, + #[cfg(feature = "zcash")] + expiry_height_gap, + } = c; + + let chain = if env::current_account_id().as_str().ends_with(".testnet") { + crate::network::Chain::BitcoinTestnet + } else { + crate::network::Chain::BitcoinMainnet + }; + + Self { + chain, + btc_light_client_account_id, + nbtc_account_id, + chain_signatures_account_id, + chain_signatures_root_public_key, + change_address, + confirmations_strategy, + confirmations_delta, + extra_msg_confirmations_delta, + deposit_bridge_fee, + withdraw_bridge_fee, + min_deposit_amount, + min_withdraw_amount, + min_change_amount, + max_change_amount, + min_btc_gas_fee, + max_btc_gas_fee, + max_withdrawal_input_number, + max_change_number, + max_active_utxo_management_input_number, + max_active_utxo_management_output_number, + active_management_lower_limit, + active_management_upper_limit, + passive_management_lower_limit, + passive_management_upper_limit, + rbf_num_limit, + max_btc_tx_pending_sec, + unhealthy_utxo_amount, + #[cfg(feature = "zcash")] + expiry_height_gap, } } } @@ -244,3 +398,145 @@ impl From for ContractData { } } } + +#[near(serializers = [borsh])] +pub struct ContractDataV2 { + #[cfg(feature = "zcash")] + pub config: LazyOption, + #[cfg(not(feature = "zcash"))] + pub config: LazyOption, + pub accounts: IterableMap, + pub utxos: IterableMap, + pub unavailable_utxos: IterableMap, + pub verified_deposit_utxo: LookupSet, + pub btc_pending_infos: IterableMap, + pub rbf_txs: IterableMap>, + pub relayer_white_list: IterableSet, + pub extra_msg_relayer_white_list: IterableSet, + pub post_action_receiver_id_white_list: IterableSet, + pub post_action_msg_templates: IterableMap>, + pub lost_found: IterableMap, + pub acc_collected_protocol_fee: u128, + pub cur_available_protocol_fee: u128, + pub acc_claimed_protocol_fee: u128, + pub cur_reserved_protocol_fee: u128, + pub acc_protocol_fee_for_gas: u128, +} + +impl From for ContractData { + fn from(c: ContractDataV2) -> Self { + let ContractDataV2 { + config, + accounts, + utxos, + unavailable_utxos, + verified_deposit_utxo, + btc_pending_infos, + rbf_txs, + relayer_white_list, + extra_msg_relayer_white_list, + post_action_receiver_id_white_list, + post_action_msg_templates, + lost_found, + acc_collected_protocol_fee, + cur_available_protocol_fee, + acc_claimed_protocol_fee, + cur_reserved_protocol_fee, + acc_protocol_fee_for_gas, + } = c; + + Self { + #[cfg(feature = "zcash")] + config, + #[cfg(not(feature = "zcash"))] + config: LazyOption::new( + StorageKey::Config, + Some(config.get().clone().unwrap().into()), + ), + accounts, + utxos, + unavailable_utxos, + verified_deposit_utxo, + btc_pending_infos, + rbf_txs, + relayer_white_list, + extra_msg_relayer_white_list, + post_action_receiver_id_white_list, + post_action_msg_templates, + lost_found, + acc_collected_protocol_fee, + cur_available_protocol_fee, + acc_claimed_protocol_fee, + cur_reserved_protocol_fee, + acc_protocol_fee_for_gas, + } + } +} + +#[near(serializers = [borsh])] +pub struct ContractDataV3 { + pub config: LazyOption, + pub accounts: IterableMap, + pub utxos: IterableMap, + pub unavailable_utxos: IterableMap, + pub verified_deposit_utxo: LookupSet, + pub btc_pending_infos: IterableMap, + pub rbf_txs: IterableMap>, + pub relayer_white_list: IterableSet, + pub extra_msg_relayer_white_list: IterableSet, + pub post_action_receiver_id_white_list: IterableSet, + pub post_action_msg_templates: IterableMap>, + pub lost_found: IterableMap, + pub acc_collected_protocol_fee: u128, + pub cur_available_protocol_fee: u128, + pub acc_claimed_protocol_fee: u128, + pub cur_reserved_protocol_fee: u128, + pub acc_protocol_fee_for_gas: u128, +} + +impl From for ContractData { + fn from(c: ContractDataV3) -> Self { + let ContractDataV3 { + config, + accounts, + utxos, + unavailable_utxos, + verified_deposit_utxo, + btc_pending_infos, + rbf_txs, + relayer_white_list, + extra_msg_relayer_white_list, + post_action_receiver_id_white_list, + post_action_msg_templates, + lost_found, + acc_collected_protocol_fee, + cur_available_protocol_fee, + acc_claimed_protocol_fee, + cur_reserved_protocol_fee, + acc_protocol_fee_for_gas, + } = c; + + Self { + config, + accounts, + utxos, + unavailable_utxos, + verified_deposit_utxo, + btc_pending_infos, + btc_pending_infos_by_external_id: LookupMap::new( + StorageKey::BTCPendingInfosByExternalId, + ), + rbf_txs, + relayer_white_list, + extra_msg_relayer_white_list, + post_action_receiver_id_white_list, + post_action_msg_templates, + lost_found, + acc_collected_protocol_fee, + cur_available_protocol_fee, + acc_claimed_protocol_fee, + cur_reserved_protocol_fee, + acc_protocol_fee_for_gas, + } + } +} diff --git a/contracts/satoshi-bridge/src/lib.rs b/contracts/satoshi-bridge/src/lib.rs index 5511afb6..270ff284 100644 --- a/contracts/satoshi-bridge/src/lib.rs +++ b/contracts/satoshi-bridge/src/lib.rs @@ -4,25 +4,25 @@ use near_sdk::{ borsh::{BorshDeserialize, BorshSerialize}, env, ext_contract, is_promise_success, json_types::{U128, U64}, - log, near, promise_result_as_success, require, + log, near, require, serde::{Deserialize, Serialize}, serde_json::{self, json, Value}, - store::{IterableMap, IterableSet, LazyOption, LookupSet}, + store::{IterableMap, IterableSet, LazyOption, LookupMap, LookupSet}, AccountId, BorshStorageKey, Gas, NearToken, PanicOnDefault, Promise, PromiseOrValue, PublicKey, Timestamp, }; +use omni_utils::macros::trusted_relayer; use std::collections::{HashMap, HashSet}; -use bitcoin::{ - absolute::LockTime, - consensus::{deserialize, serialize}, - transaction::Version, - Address, Amount, Network, OutPoint, Psbt, PublicKey as BtcPublicKey, ScriptBuf, - Transaction as BtcTransaction, TxIn, TxOut, -}; +use bitcoin::{absolute::LockTime, Amount, OutPoint, PublicKey as BtcPublicKey, ScriptBuf, TxOut}; pub mod account; pub mod api; +#[cfg(not(feature = "zcash"))] +pub mod bitcoin_utils; +#[cfg(feature = "zcash")] +pub mod zcash_utils; + pub mod btc_light_client; pub mod btc_pending_info; pub mod chain_signature; @@ -33,6 +33,7 @@ pub mod json_utils; pub mod kdf; pub mod legacy; pub mod nbtc; +pub mod network; pub mod psbt; pub mod rbf; pub mod token_transfer; @@ -50,13 +51,29 @@ pub use crate::config::*; pub use crate::deposit_msg::*; pub use crate::event::*; pub use crate::json_utils::*; -pub use crate::kdf::*; pub use crate::legacy::*; pub use crate::nbtc::*; pub use crate::rbf::*; pub use crate::token_transfer::*; pub use crate::utils::*; pub use crate::utxo::*; + +#[cfg(not(feature = "zcash"))] +pub use crate::bitcoin_utils::psbt_wrapper; +#[cfg(not(feature = "zcash"))] +pub use crate::bitcoin_utils::transaction::Transaction as WrappedTransaction; +#[cfg(not(feature = "zcash"))] +use crate::bitcoin_utils::types::ChainSpecificData; + +#[cfg(feature = "zcash")] +pub use crate::zcash_utils::contract_methods::*; +#[cfg(feature = "zcash")] +pub use crate::zcash_utils::psbt_wrapper; +#[cfg(feature = "zcash")] +pub use crate::zcash_utils::transaction::Transaction as WrappedTransaction; +#[cfg(feature = "zcash")] +use crate::zcash_utils::types::ChainSpecificData; + #[cfg(test)] pub use unit::*; @@ -75,6 +92,7 @@ enum StorageKey { LostFound, PostActionMsgTemplates, ExtraMsgRelayerWhiteList, + BTCPendingInfosByExternalId, } #[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)] @@ -85,6 +103,8 @@ pub enum Role { PauseManager, UpgradableCodeStager, UpgradableCodeDeployer, + UnrestrictedRelayer, + RelayerManager, } #[near(serializers = [borsh])] @@ -95,6 +115,7 @@ pub struct ContractData { pub unavailable_utxos: IterableMap, pub verified_deposit_utxo: LookupSet, pub btc_pending_infos: IterableMap, + pub btc_pending_infos_by_external_id: LookupMap, pub rbf_txs: IterableMap>, pub relayer_white_list: IterableSet, pub extra_msg_relayer_white_list: IterableSet, @@ -112,6 +133,8 @@ pub struct ContractData { pub enum VersionedContractData { V0(ContractDataV0), V1(ContractDataV1), + V2(ContractDataV2), + V3(ContractDataV3), Current(ContractData), } @@ -130,6 +153,11 @@ pub struct Contract { data: VersionedContractData, } +#[trusted_relayer( + bypass_roles(Role::DAO, Role::UnrestrictedRelayer), + manager_roles(Role::DAO, Role::RelayerManager), + config_roles(Role::DAO) +)] #[near] impl Contract { #[init] @@ -151,6 +179,9 @@ impl Contract { unavailable_utxos: IterableMap::new(StorageKey::UnavailableUTXOs), verified_deposit_utxo: LookupSet::new(StorageKey::VerifiedDepositUtxos), btc_pending_infos: IterableMap::new(StorageKey::BTCPendingInfos), + btc_pending_infos_by_external_id: LookupMap::new( + StorageKey::BTCPendingInfosByExternalId, + ), rbf_txs: IterableMap::new(StorageKey::RbfTxs), relayer_white_list: IterableSet::new(StorageKey::RelayerWhiteList), extra_msg_relayer_white_list: IterableSet::new( diff --git a/contracts/satoshi-bridge/src/nbtc/burn.rs b/contracts/satoshi-bridge/src/nbtc/burn.rs index 7af6b3aa..e288a70a 100644 --- a/contracts/satoshi-bridge/src/nbtc/burn.rs +++ b/contracts/satoshi-bridge/src/nbtc/burn.rs @@ -1,6 +1,9 @@ -use crate::*; +use crate::{ + env, ext_nbtc, generate_utxo_storage_key, is_promise_success, near, Contract, ContractExt, + Event, Gas, Promise, WrappedTransaction, U128, UTXO, +}; -pub const GAS_FOR_BURN_CALL: Gas = Gas::from_tgas(10); +pub const GAS_FOR_BURN_CALL: Gas = Gas::from_tgas(5); pub const GAS_FOR_WITHDRAW_BURN_CALL_BACK: Gas = Gas::from_tgas(20); pub const GAS_FOR_ACTIVE_UTXO_MANAGEMENT_BURN_CALL_BACK: Gas = Gas::from_tgas(20); @@ -74,11 +77,11 @@ impl Contract { }; if is_success { let tx_bytes = btc_pending_info.tx_bytes_with_sign.as_ref().unwrap(); - let transaction = bytes_to_btc_transaction(tx_bytes); - let withdraw_change_address = config.get_change_address(); - let withdraw_change_script_pubkey = withdraw_change_address.script_pubkey(); + let transaction = WrappedTransaction::decode(tx_bytes, &self.internal_config().chain) + .expect("Deserialization tx_bytes failed"); + let withdraw_change_script_pubkey = config.get_change_script_pubkey(); let mut utxo_storage_keys = vec![]; - for (index, output) in transaction.output.into_iter().enumerate() { + for (index, output) in transaction.output().into_iter().enumerate() { if withdraw_change_script_pubkey == output.script_pubkey { let utxo = UTXO { path: env::current_account_id().to_string(), @@ -86,7 +89,10 @@ impl Contract { vout: index, balance: output.value.to_sat(), }; - let utxo_storage_key = generate_utxo_storage_key(tx_id.clone(), index as u32); + let utxo_storage_key = generate_utxo_storage_key( + tx_id.clone(), + u32::try_from(index).unwrap_or_else(|_| env::panic_str("Index overflow")), + ); self.internal_set_utxo(&utxo_storage_key, utxo); utxo_storage_keys.push(utxo_storage_key); } @@ -129,7 +135,8 @@ impl Contract { self.data_mut().cur_available_protocol_fee += protocol_fee.0; } if refund > 0 { - self.internal_transfer_nbtc(&btc_pending_info.account_id, refund); + self.internal_transfer_nbtc(&btc_pending_info.account_id, refund) + .detach(); } self.internal_remove_btc_pending_info(&tx_id); Event::UtxoAdded { utxo_storage_keys }.emit(); @@ -152,12 +159,12 @@ impl Contract { }; if is_success { let tx_bytes = btc_pending_info.tx_bytes_with_sign.as_ref().unwrap(); - let transaction = bytes_to_btc_transaction(tx_bytes); + let transaction = WrappedTransaction::decode(tx_bytes, &self.internal_config().chain) + .expect("Deserialization tx_bytes failed"); let config = self.internal_config(); - let withdraw_change_address = config.get_change_address(); - let withdraw_change_script_pubkey = withdraw_change_address.script_pubkey(); + let withdraw_change_script_pubkey = config.get_change_script_pubkey(); let mut utxo_storage_keys = vec![]; - for (index, output) in transaction.output.into_iter().enumerate() { + for (index, output) in transaction.output().into_iter().enumerate() { if withdraw_change_script_pubkey == output.script_pubkey { let utxo = UTXO { path: env::current_account_id().to_string(), @@ -165,7 +172,10 @@ impl Contract { vout: index, balance: output.value.to_sat(), }; - let utxo_storage_key = generate_utxo_storage_key(tx_id.clone(), index as u32); + let utxo_storage_key = generate_utxo_storage_key( + tx_id.clone(), + u32::try_from(index).unwrap_or_else(|_| env::panic_str("Index overflow")), + ); self.internal_set_utxo(&utxo_storage_key, utxo); utxo_storage_keys.push(utxo_storage_key); } diff --git a/contracts/satoshi-bridge/src/nbtc/mint.rs b/contracts/satoshi-bridge/src/nbtc/mint.rs index b22fc693..dfb5fb22 100644 --- a/contracts/satoshi-bridge/src/nbtc/mint.rs +++ b/contracts/satoshi-bridge/src/nbtc/mint.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + env, ext_nbtc, is_promise_success, near, Account, AccountId, Contract, ContractExt, Event, Gas, + PendingUTXOInfo, PostAction, Promise, U128, +}; pub const GAS_FOR_MINT_CALL: Gas = Gas::from_tgas(150); pub const GAS_FOR_MINT_CALL_BACK: Gas = Gas::from_tgas(10); @@ -49,11 +52,7 @@ impl Contract { pending_utxo_info: PendingUTXOInfo, ) -> bool { let is_success = is_promise_success(); - if !is_success { - self.data_mut() - .verified_deposit_utxo - .remove(&pending_utxo_info.utxo_storage_key); - } else { + if is_success { if !self.check_account_exists(&recipient_id) { self.internal_set_account(&recipient_id, Account::new(&recipient_id)); } @@ -66,6 +65,10 @@ impl Contract { } .emit(); self.internal_set_utxo(&pending_utxo_info.utxo_storage_key, pending_utxo_info.utxo); + } else { + self.data_mut() + .verified_deposit_utxo + .remove(&pending_utxo_info.utxo_storage_key); } Event::VerifyDepositDetails { recipient_id: &recipient_id, diff --git a/contracts/satoshi-bridge/src/nbtc/mod.rs b/contracts/satoshi-bridge/src/nbtc/mod.rs index 9d1ab681..903298ac 100644 --- a/contracts/satoshi-bridge/src/nbtc/mod.rs +++ b/contracts/satoshi-bridge/src/nbtc/mod.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{ext_contract, AccountId, PostAction, U128}; pub mod burn; pub mod mint; @@ -21,4 +21,5 @@ pub trait NBtc { relayer_account_id: AccountId, relayer_fee: U128, ); + fn safe_mint(&mut self, account_id: AccountId, amount: U128, msg: Option); } diff --git a/contracts/satoshi-bridge/src/network.rs b/contracts/satoshi-bridge/src/network.rs new file mode 100644 index 00000000..bf00f177 --- /dev/null +++ b/contracts/satoshi-bridge/src/network.rs @@ -0,0 +1,462 @@ +use bitcoin::bech32::Hrp; +use bitcoin::hashes::Hash; +use bitcoin::{base58, bech32, PubkeyHash, ScriptHash, WitnessProgram, WitnessVersion}; +use near_sdk::near; +use std::fmt; +use zcash_address::unified::{Container, Receiver}; +use zcash_address::{ConversionError, ToAddress, ZcashAddress}; +#[cfg(feature = "zcash")] +use zcash_protocol::consensus::BranchId; + +/// Size of Orchard raw address bytes (diversifier + pk_d). +pub const ORCHARD_RAW_ADDRESS_SIZE: usize = 43; + +/// Type alias for Orchard raw address bytes to avoid magic numbers. +pub type OrchardRawAddress = [u8; ORCHARD_RAW_ADDRESS_SIZE]; + +#[near(serializers = [borsh, json])] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Chain { + BitcoinMainnet, + BitcoinTestnet, + LitecoinMainnet, + LitecoinTestnet, + ZcashMainnet, + ZcashTestnet, + DogecoinMainnet, + DogecoinTestnet, +} +#[cfg(feature = "zcash")] +pub struct BranchIdUpdateBlockHeight { + pub nu6_1_update: u32, +} + +#[cfg(feature = "zcash")] +impl BranchIdUpdateBlockHeight { + pub fn new(chain: &Chain) -> Self { + match chain { + Chain::ZcashMainnet => Self { + nu6_1_update: 3146400, + }, + Chain::ZcashTestnet => Self { + nu6_1_update: 3536500, + }, + _ => unreachable!(), + } + } +} +impl Chain { + #[cfg(feature = "zcash")] + pub fn get_branch_id(&self, block_height: u32) -> BranchId { + let block_height_update = BranchIdUpdateBlockHeight::new(self); + if block_height_update.nu6_1_update != 0 && block_height >= block_height_update.nu6_1_update + { + return BranchId::Nu6_1; + } + + BranchId::Nu6 + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Address { + P2pkh { + hash: PubkeyHash, + chain: Chain, + }, + P2sh { + hash: ScriptHash, + chain: Chain, + }, + Segwit { + program: WitnessProgram, + chain: Chain, + }, + Unified { + address: zcash_address::unified::Address, + chain: Chain, + }, +} + +impl zcash_address::TryFromAddress for Address { + type Error = String; + fn try_from_transparent_p2pkh( + net: zcash_protocol::consensus::NetworkType, + data: [u8; 20], + ) -> Result> { + let chain = match net { + zcash_protocol::consensus::NetworkType::Main => Chain::ZcashMainnet, + zcash_protocol::consensus::NetworkType::Test => Chain::ZcashTestnet, + zcash_protocol::consensus::NetworkType::Regtest => { + return Err("Regtest network not supported".to_string().into()); + } + }; + + Ok(Self::P2pkh { + hash: PubkeyHash::from_slice(&data[..]).map_err(|e| { + ConversionError::::from(format!("Invalid pubkey hash: {e}")) + })?, + chain, + }) + } + + fn try_from_unified( + net: zcash_protocol::consensus::NetworkType, + data: zcash_address::unified::Address, + ) -> Result> { + let chain = match net { + zcash_protocol::consensus::NetworkType::Main => Chain::ZcashMainnet, + zcash_protocol::consensus::NetworkType::Test => Chain::ZcashTestnet, + zcash_protocol::consensus::NetworkType::Regtest => { + return Err("Regtest network not supported".to_string().into()); + } + }; + + Ok(Self::Unified { + address: data, + chain, + }) + } +} + +impl Address { + /// Parse an address string + chain into `AddressInner` + pub fn parse(address: &str, chain: Chain) -> Result { + if chain == Chain::ZcashMainnet || chain == Chain::ZcashTestnet { + let addr = ZcashAddress::try_from_encoded(address) + .map_err(|e| format!("Error on parsing ZCash Address: {e}"))?; + + let network = match chain { + Chain::ZcashMainnet => zcash_protocol::consensus::NetworkType::Main, + Chain::ZcashTestnet => zcash_protocol::consensus::NetworkType::Test, + _ => unreachable!(), + }; + + return addr + .convert_if_network::(network) + .map_err(|e| e.to_string()); + } + + if let Some(hrp) = get_segwit_hrp(&chain) { + if let Ok((decoded_hrp, witness_version, data)) = bech32::segwit::decode(address) { + if decoded_hrp.as_str() != hrp { + return Err(format!( + "Bech32 HRP mismatch: expected '{hrp}', got '{decoded_hrp}'" + )); + } + + let version = + WitnessVersion::try_from(witness_version).map_err(|err| format!("{err:?}"))?; + let program = WitnessProgram::new(version, &data).map_err(|err| { + format!("bech32 guarantees valid program length for witness: {err:?}") + })?; + + return Ok(Address::Segwit { program, chain }); + } + } + + let data = bitcoin::base58::decode_check(address) + .map_err(|e| format!("Base58 decode error: {e}"))?; + + let prefix = get_pubkey_address_prefix(&chain); + if data.starts_with(&prefix) { + let hash = PubkeyHash::from_slice(&data[prefix.len()..]) + .map_err(|e| format!("Invalid pubkey hash: {e}"))?; + return Ok(Address::P2pkh { hash, chain }); + } + + let prefix = get_script_address_prefix(&chain); + if data.starts_with(&prefix) { + let hash = ScriptHash::from_slice(&data[prefix.len()..]) + .map_err(|e| format!("Invalid script hash: {e}"))?; + return Ok(Address::P2sh { hash, chain }); + } + + Err("Unknown address format or unsupported chain".to_string()) + } + + /// Return the scriptPubKey corresponding to this address + pub fn script_pubkey(&self) -> Result { + match self { + Address::P2pkh { hash, .. } => Ok(bitcoin::ScriptBuf::new_p2pkh(hash)), + Address::P2sh { hash, .. } => Ok(bitcoin::ScriptBuf::new_p2sh(hash)), + Address::Segwit { program, .. } => Ok(bitcoin::ScriptBuf::new_witness_program(program)), + Address::Unified { address, .. } => { + let receiver_list = address.items_as_parsed(); + for receiver in receiver_list { + match receiver { + Receiver::P2pkh(data) => { + return Ok(bitcoin::ScriptBuf::new_p2pkh( + &PubkeyHash::from_slice(&data[..]).map_err(|err| { + format!("Error on parsing Pubkey Hash: {err:?}").to_string() + })?, + )) + } + Receiver::P2sh(data) => { + return Ok(bitcoin::ScriptBuf::new_p2sh( + &ScriptHash::from_slice(&data[..]).map_err(|err| { + format!("Error on parsing Script Hash: {err:?}").to_string() + })?, + )) + } + _ => {} + } + } + + Err("No receiver found in address".to_string()) + } + } + } + + /// Extract the Orchard receiver raw bytes from a Unified Address string for the given chain. + pub fn extract_orchard_receiver(&self) -> Result { + match self { + Address::Unified { address, .. } => { + let receiver_list = address.items_as_parsed(); + for receiver in receiver_list { + match receiver { + Receiver::Orchard(bytes) => return Ok(*bytes), + _ => continue, + } + } + + Err("Unified address missing Orchard receiver".to_string()) + } + _ => Err("No Orchard address found".to_string()), + } + } + + pub fn from_pubkey(chain: Chain, pubkey: bitcoin::PublicKey) -> Result { + let pubkey_hash = pubkey.pubkey_hash(); + + if let Some(_hrp) = get_segwit_hrp(&chain) { + // Chain supports Bech32 SegWit + let wp = WitnessProgram::p2wpkh( + &pubkey + .try_into() + .map_err(|e| format!("Error on converting pubkey to bytes: {e}"))?, + ); + let wp = WitnessProgram::new(WitnessVersion::V0, wp.program().as_bytes()) + .map_err(|e| format!("bech32 guarantees valid program length for witness: {e}"))?; + Ok(Address::Segwit { program: wp, chain }) + } else { + // Legacy P2PKH + Ok(Address::P2pkh { + hash: pubkey_hash, + chain, + }) + } + } + + pub fn from_script(script: &bitcoin::Script, chain: Chain) -> Option { + // Try P2PKH + if script.is_p2pkh() { + let hash = bitcoin::PubkeyHash::from_slice(&script.as_bytes()[3..23]).ok()?; + return Some(Address::P2pkh { hash, chain }); + } + + // Try P2SH + if script.is_p2sh() { + let hash = bitcoin::ScriptHash::from_slice(&script.as_bytes()[2..22]).ok()?; + return Some(Address::P2sh { hash, chain }); + } + + if script.is_witness_program() { + let opcode = script.first_opcode()?; + + let version = WitnessVersion::try_from(opcode).ok()?; + let program = WitnessProgram::new(version, &script.as_bytes()[2..]).ok()?; + return Some(Address::Segwit { program, chain }); + } + + None + } +} + +/// Formats bech32 as upper case if alternate formatting is chosen (`{:#}`). +impl fmt::Display for Address { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + use Address::{P2pkh, P2sh, Segwit, Unified}; + match self { + P2pkh { hash, chain } => { + let prefix = get_pubkey_address_prefix(chain); + let mut prefixed = Vec::with_capacity(20 + prefix.len()); + prefixed.extend(prefix); + prefixed.extend(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } + P2sh { hash, chain } => { + let prefix = get_script_address_prefix(chain); + let mut prefixed = Vec::with_capacity(20 + prefix.len()); + prefixed.extend(prefix); + prefixed.extend(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } + Segwit { program, chain } => { + let hrp = + Hrp::parse(get_segwit_hrp(chain).ok_or(fmt::Error)?).map_err(|_| fmt::Error)?; + let version = program.version().to_fe(); + let program = program.program().as_ref(); + + if fmt.alternate() { + bech32::segwit::encode_upper_to_fmt_unchecked(fmt, hrp, version, program) + } else { + bech32::segwit::encode_lower_to_fmt_unchecked(fmt, hrp, version, program) + } + } + Unified { address, chain } => { + let network = match chain { + Chain::ZcashMainnet => zcash_protocol::consensus::NetworkType::Main, + Chain::ZcashTestnet => zcash_protocol::consensus::NetworkType::Test, + _ => unreachable!(), + }; + + let str_address = ZcashAddress::from_unified(network, address.clone()).encode(); + write!(fmt, "{str_address}") + } + } + } +} + +pub fn get_segwit_hrp(chain: &Chain) -> Option<&'static str> { + match chain { + // Bitcoin (Bech32 - BIP173) + Chain::BitcoinMainnet => Some("bc"), + Chain::BitcoinTestnet => Some("tb"), + + // Litecoin (Bech32) + Chain::LitecoinMainnet => Some("ltc"), + Chain::LitecoinTestnet => Some("tltc"), + + // Zcash (Bech32m) support unified addresses with hrp but not segwit + Chain::ZcashMainnet | Chain::ZcashTestnet => None, + + // Dogecoin (no native Bech32 support yet) + Chain::DogecoinMainnet | Chain::DogecoinTestnet => None, + } +} + +/// Returns the P2PKH (pubkey) address prefix as a Vec +fn get_pubkey_address_prefix(chain: &Chain) -> Vec { + match chain { + // Bitcoin + Chain::BitcoinMainnet => vec![0x00], // "1" + Chain::BitcoinTestnet => vec![0x6F], // "m" or "n" + + // Litecoin + Chain::LitecoinMainnet => vec![0x30], // "L" + Chain::LitecoinTestnet => vec![0x6F], + + // Zcash + Chain::ZcashMainnet => vec![0x1C, 0xB8], // "t1" + Chain::ZcashTestnet => vec![0x1D, 0x25], // "tm" + + // Dogecoin + Chain::DogecoinMainnet => vec![0x1E], // "D" + Chain::DogecoinTestnet => vec![0x71], // "n" + } +} + +/// Returns the P2SH (script) address prefix as a Vec +fn get_script_address_prefix(chain: &Chain) -> Vec { + match chain { + // Bitcoin + Chain::BitcoinMainnet => vec![0x05], // "3" + Chain::BitcoinTestnet => vec![0xC4], // "2" + + // Litecoin + Chain::LitecoinMainnet => vec![0x32], // "M" (was "3") + Chain::LitecoinTestnet => vec![0x3A], + + // Zcash + Chain::ZcashMainnet => vec![0x1C, 0xBD], // "t3" + Chain::ZcashTestnet => vec![0x1C, 0xBA], // "t2" + + // Dogecoin + Chain::DogecoinMainnet => vec![0x16], // "9" + Chain::DogecoinTestnet => vec![0xC4], // same as Bitcoin testnet + } +} + +#[cfg(test)] +mod tests { + use crate::network::{Address, Chain}; + use bitcoin::PublicKey as BtcPublicKey; + use k256::elliptic_curve::sec1::ToEncodedPoint; + use near_sdk::PublicKey; + use std::str::FromStr; + pub fn generate_public_key(path: &str) -> Vec { + let mpc_pk = crypto_shared::near_public_key_to_affine_point( + PublicKey::from_str("secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3").unwrap(), + ); + let epsilon = crypto_shared::derive_epsilon( + &"zcash_connector-20250714-143829.testnet".parse().unwrap(), + path, + ); + let user_pk = crypto_shared::derive_key(mpc_pk, epsilon); + let user_pk_encoded_point = user_pk.to_encoded_point(false); + user_pk_encoded_point.as_bytes().to_vec() + } + + pub fn generate_btc_public_key(path: &str) -> BtcPublicKey { + let public_key_bytes = generate_public_key(path); + let uncompressed_btc_public_key = + BtcPublicKey::from_slice(&public_key_bytes).expect("Invalid public key bytes"); + uncompressed_btc_public_key + .inner + .to_string() + .parse() + .unwrap() + } + + #[test] + fn test_parse_address() { + for (address, chain) in [ + ( + "bc1pwyzhgwy30q2juhau2f2c4qscasddle5ymw9m7scq5kc62t8kyzkqyz059k", + Chain::BitcoinMainnet, + ), + ( + "tb1pt34385rvqtyuz6muh9hr5ed4fy0cx89zz0faxm6dhku0vqp2pxxs0ymh7y", + Chain::BitcoinTestnet, + ), + ("LWrHnw5xztWiPafMhKYTQued8iuhaET7Yd", Chain::LitecoinMainnet), + ( + "tltc1q0c8899qaxq4e5m9zucq9vkvrn4npfwa8pww9d8", + Chain::LitecoinTestnet, + ), + ("t1ggQ7ZgHRoR34Z2xCcF155VcDe5zDZpZF1", Chain::ZcashMainnet), + ("tmJpMbYtRf9Hgi8HUJ4FGkoM3FUSHsu28wM", Chain::ZcashTestnet), + ("DKNmffVbxrBcNvQ9uJEDLe8f6prxSmH2Vm", Chain::DogecoinMainnet), + ("njyMWWyh1L7tSX6QkWRgetMVCVyVtfoDta", Chain::DogecoinTestnet), + ] { + let parse_address = Address::parse(address, chain.clone()).unwrap(); + let script_pubkey = parse_address.script_pubkey().unwrap(); + let address_from_script = Address::from_script(&script_pubkey, chain).unwrap(); + let display_address = address_from_script.to_string(); + assert_eq!(display_address, address); + } + } + + #[test] + fn test_from_pubkey() { + for chain in [ + Chain::BitcoinMainnet, + Chain::BitcoinTestnet, + Chain::LitecoinMainnet, + Chain::LitecoinTestnet, + Chain::ZcashMainnet, + Chain::ZcashTestnet, + Chain::DogecoinMainnet, + Chain::DogecoinTestnet, + ] { + let btc_public_key = generate_btc_public_key("path"); + let address = Address::from_pubkey(chain.clone(), btc_public_key).unwrap(); + let script_pubkey = address.script_pubkey().unwrap(); + let address_from_script = Address::from_script(&script_pubkey, chain.clone()).unwrap(); + assert_eq!(address, address_from_script); + let address_from_str = Address::parse(&address_from_script.to_string(), chain).unwrap(); + assert_eq!(address, address_from_str); + } + } +} diff --git a/contracts/satoshi-bridge/src/psbt.rs b/contracts/satoshi-bridge/src/psbt.rs index 7090c251..6a782eaa 100644 --- a/contracts/satoshi-bridge/src/psbt.rs +++ b/contracts/satoshi-bridge/src/psbt.rs @@ -1,32 +1,50 @@ -use crate::*; +use crate::{ + env, network::Address, psbt_wrapper::PsbtWrapper, require, Amount, Contract, Event, ScriptBuf, + TxOut, U128, VUTXO, +}; impl Contract { + #[allow(clippy::too_many_arguments)] pub fn check_withdraw_psbt_valid( &self, - target_address_script_pubkey: &ScriptBuf, + target_btc_address: String, withdraw_change_address_script_pubkey: &ScriptBuf, - withdraw_psbt: &Psbt, + withdraw_psbt: &PsbtWrapper, vutxos: &[VUTXO], amount: u128, withdraw_fee: u128, + max_gas_fee: Option, ) -> (u128, u128) { let config = self.internal_config(); - let utxo_num = self.data().utxos.len() + vutxos.len() as u32; + let vutxos_len = u32::try_from(vutxos.len()).unwrap_or_else(|_| { + env::panic_str("vutxos len overflow"); + }); + let utxo_num = self.data().utxos.len() + vutxos_len; let (input_num, change_num, actual_received_amount, gas_fee) = self.check_withdraw_psbt( withdraw_psbt, - target_address_script_pubkey, + target_btc_address, withdraw_change_address_script_pubkey, vutxos, amount, withdraw_fee, ); + if let Some(max_gas_fee) = max_gas_fee { + require!( + gas_fee <= max_gas_fee.0, + format!( + "Gas fee does not match the provided max fee (gas fee = {}; max gas fee = {})", + gas_fee, max_gas_fee.0 + ) + ); + } + require!( - change_num <= config.max_change_number as usize, + change_num <= usize::from(config.max_change_number), format!("change_num must not exceed {}", config.max_change_number) ); require!( - input_num <= config.max_withdrawal_input_number as usize, + input_num <= usize::from(config.max_withdrawal_input_number), format!( "input must not exceed {}", config.max_withdrawal_input_number @@ -51,19 +69,22 @@ impl Contract { pub fn check_active_management_psbt_valid( &self, - psbt: &Psbt, + psbt: &PsbtWrapper, vutxos: &[VUTXO], ) -> (u128, u128) { let config = self.internal_config(); - let utxo_num = self.data().utxos.len() + vutxos.len() as u32; - let input_num = psbt.unsigned_tx.input.len(); - let output_num = psbt.unsigned_tx.output.len(); + let vutxos_len = u32::try_from(vutxos.len()).unwrap_or_else(|_| { + env::panic_str("vutxos len overflow"); + }); + let utxo_num = self.data().utxos.len() + vutxos_len; + let input_num = psbt.get_input_num(); + let output_num = psbt.get_output_num(); if !is_merge_unhealthy_utxos(output_num, vutxos, config.unhealthy_utxo_amount) { if utxo_num < config.active_management_lower_limit { require!(input_num < output_num, "require input_num < output_num"); require!( - output_num <= config.max_active_utxo_management_output_number as usize, + output_num <= usize::from(config.max_active_utxo_management_output_number), format!( "require output_num <= {}", config.max_active_utxo_management_output_number @@ -72,7 +93,7 @@ impl Contract { } else if utxo_num > config.active_management_upper_limit { require!(input_num > output_num, "require input_num > output_num"); require!( - input_num <= config.max_active_utxo_management_input_number as usize, + input_num <= usize::from(config.max_active_utxo_management_input_number), format!( "require input_num <= {}", config.max_active_utxo_management_input_number @@ -90,32 +111,31 @@ impl Contract { pub fn check_psbt_output_all_change_address( &self, - psbt: &Psbt, + psbt: &PsbtWrapper, vutxos: &[VUTXO], force_healthy_output: bool, is_cancel: bool, ) -> (u128, u128) { let config = self.internal_config(); - let withdraw_change_address_script_pubkey = config.get_change_address().script_pubkey(); + let withdraw_change_address_script_pubkey = config.get_change_script_pubkey(); let input_amount = vutxos .iter() - .map(|vutxo| vutxo.get_amount() as u128) + .map(|vutxo| u128::from(vutxo.get_amount())) .sum::(); let output_amount = psbt - .unsigned_tx - .output + .get_output() .iter() .map(|v| { if force_healthy_output { require!( v.value.to_sat() > config.unhealthy_utxo_amount - && v.value.to_sat() as u128 <= config.max_change_amount, + && u128::from(v.value.to_sat()) <= config.max_change_amount, "The output amount is not in the valid range" ); } else { require!( - v.value.to_sat() as u128 >= config.min_change_amount - && v.value.to_sat() as u128 <= config.max_change_amount, + u128::from(v.value.to_sat()) >= config.min_change_amount + && u128::from(v.value.to_sat()) <= config.max_change_amount, "The output amount is not in the valid range" ); } @@ -123,7 +143,7 @@ impl Contract { v.script_pubkey == withdraw_change_address_script_pubkey, "Invalid output script_pubkey" ); - v.value.to_sat() as u128 + u128::from(v.value.to_sat()) }) .sum::(); let gas_fee = input_amount - output_amount; @@ -141,49 +161,60 @@ impl Contract { pub fn check_withdraw_psbt( &self, - psbt: &Psbt, - target_address_script_pubkey: &ScriptBuf, + psbt: &PsbtWrapper, + target_btc_address: String, withdraw_change_address_script_pubkey: &ScriptBuf, vutxos: &[VUTXO], amount: u128, withdraw_fee: u128, ) -> (usize, usize, u128, u128) { let config = self.internal_config(); - let input_amounts = vutxos.iter().map(|vutxo| vutxo.get_amount() as u128); + let input_amounts = vutxos.iter().map(|vutxo| u128::from(vutxo.get_amount())); let min_input_amount = input_amounts.clone().min().unwrap(); let total_input_amount = input_amounts.sum::(); let mut total_output_amount = 0; let mut actual_received_amounts = vec![]; let mut change_amounts = vec![]; - psbt.unsigned_tx.output.iter().for_each(|output| { - let output_value = output.value.to_sat() as u128; - total_output_amount += output_value; - if &output.script_pubkey == target_address_script_pubkey { - actual_received_amounts.push(output_value); - } else if &output.script_pubkey == withdraw_change_address_script_pubkey { - require!( - output_value >= config.min_change_amount, - "The change amount is too small" - ); - require!( - output_value < min_input_amount, - "The change amount must be less than all inputs" - ); - change_amounts.push(output_value); - } else { - let output_address = Address::from_script(&output.script_pubkey, btc_network()) - .expect("Unsupported btc address type"); - env::panic_str( - format!("Invalid transaction output address: {}", output_address).as_str(), - ); - } - }); + + if !psbt.get_output().is_empty() { + let target_address_script_pubkey = self + .internal_config() + .string_to_script_pubkey(&target_btc_address); + + psbt.get_output().iter().for_each(|output| { + let output_value = output.value.to_sat() as u128; + total_output_amount += output_value; + if output.script_pubkey == target_address_script_pubkey { + actual_received_amounts.push(output_value); + } else if &output.script_pubkey == withdraw_change_address_script_pubkey { + require!( + output_value >= config.min_change_amount, + "The change amount is too small" + ); + require!( + output_value < min_input_amount, + "The change amount must be less than all inputs" + ); + change_amounts.push(output_value); + } else { + let output_address = + Address::from_script(&output.script_pubkey, config.chain.clone()) + .expect("Unsupported btc address type"); + env::panic_str( + format!("Invalid transaction output address: {}", output_address).as_str(), + ); + } + }); + } + + total_output_amount += psbt.add_extra_outputs(&mut actual_received_amounts); + require!( actual_received_amounts.len() == 1, "only one user output is allowed." ); let actual_received_amount = actual_received_amounts[0]; - let input_num = psbt.unsigned_tx.input.len(); + let input_num = psbt.get_input_num(); let change_num = change_amounts.len(); if input_num > change_num { require!( @@ -217,70 +248,30 @@ impl Contract { gas_fee, config.min_btc_gas_fee, config.max_btc_gas_fee ) ); + + self.check_psbt_chain_specific(psbt, gas_fee, target_btc_address); (input_num, change_num, actual_received_amount, gas_fee) } } impl Contract { - pub fn generate_psbt_and_vutxos( - &mut self, - input: Vec, - output: Vec, - ) -> (Psbt, Vec, Vec) { - require!(!input.is_empty(), "empty input"); - require!(!output.is_empty(), "empty output"); - let transaction = BtcTransaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: input - .into_iter() - .map(|previous_output| TxIn { - previous_output, - sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }) - .collect(), - output, - }; - let mut psbt = Psbt::from_unsigned_tx(transaction).expect("Failed to generate PSBT"); - let (utxo_storage_keys, vutxos) = self.remove_vutxo_by_psbt(&psbt); - vutxos.iter().enumerate().for_each(|(i, v)| { - psbt.inputs[i].witness_utxo = Some(TxOut { + pub fn generate_vutxos(&mut self, psbt: &mut PsbtWrapper) -> (Vec, Vec) { + let (utxo_storage_keys, vutxos) = self.remove_vutxo_by_psbt(psbt); + + let input_utxo = vutxos + .iter() + .map(|v| TxOut { value: Amount::from_sat(v.get_amount()), script_pubkey: self - .generate_btc_p2wpkh_address(&v.get_path()) - .script_pubkey(), + .generate_utxo_chain_address(&v.get_path()) + .script_pubkey() + .expect("Invalid address"), }) - }); - (psbt, utxo_storage_keys, vutxos) - } + .collect(); - pub fn generate_psbt_from_original_psbt_and_new_output( - &self, - original_tx_btc_pending_info: &BTCPendingInfo, - output: Vec, - ) -> Psbt { - let original_psbt = original_tx_btc_pending_info.get_psbt(); - let transaction = BtcTransaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: original_psbt - .unsigned_tx - .input - .into_iter() - .map(|original_psbt_input| TxIn { - previous_output: original_psbt_input.previous_output, - sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }) - .collect(), - output, - }; - let mut psbt = Psbt::from_unsigned_tx(transaction).expect("Failed to generate PSBT"); - original_psbt.inputs.iter().enumerate().for_each(|(i, v)| { - psbt.inputs[i].witness_utxo.clone_from(&v.witness_utxo); - }); - psbt + psbt.set_input_utxo(input_utxo); + + (utxo_storage_keys, vutxos) } } diff --git a/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs b/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs index 2cffd4a5..0afa66d0 100644 --- a/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs +++ b/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs @@ -1,14 +1,18 @@ -use crate::*; +use crate::{ + init_rbf_btc_pending_info, psbt_wrapper::PsbtWrapper, require, AccountId, BTCPendingInfo, + Contract, PendingInfoStage, PendingInfoState, RbfState, +}; impl Contract { pub fn check_active_utxo_management_rbf_psbt_valid( &self, original_tx_btc_pending_info: &BTCPendingInfo, - active_utxo_management_rbf_psbt: &Psbt, + active_utxo_management_rbf_psbt: &PsbtWrapper, ) -> (u128, u128) { - let original_tx = original_tx_btc_pending_info.get_transaction(); + let original_tx = + original_tx_btc_pending_info.get_transaction(&self.internal_config().chain); require!( - original_tx.output.len() == active_utxo_management_rbf_psbt.unsigned_tx.output.len(), + original_tx.output().len() == active_utxo_management_rbf_psbt.get_output_num(), "Invalid output num" ); let (actual_received_amount, gas_fee) = self.check_psbt_output_all_change_address( @@ -24,7 +28,8 @@ impl Contract { &mut self, account_id: &AccountId, original_btc_pending_verify_id: String, - output: Vec, + active_utxo_management_rbf_psbt: PsbtWrapper, + _predecessor_account_id: AccountId, ) -> String { let original_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id); @@ -34,8 +39,6 @@ impl Contract { ); original_tx_btc_pending_info.assert_not_canceled(); original_tx_btc_pending_info.assert_active_utxo_management_original_pending_verify_tx(); - let active_utxo_management_rbf_psbt = self - .generate_psbt_from_original_psbt_and_new_output(original_tx_btc_pending_info, output); let mut btc_pending_info = init_rbf_btc_pending_info( original_tx_btc_pending_info, PendingInfoState::ActiveUtxoManagementRbf(RbfState { diff --git a/contracts/satoshi-bridge/src/rbf/cancel_active_utxo_management.rs b/contracts/satoshi-bridge/src/rbf/cancel_active_utxo_management.rs index f7f28ad7..1d83b661 100644 --- a/contracts/satoshi-bridge/src/rbf/cancel_active_utxo_management.rs +++ b/contracts/satoshi-bridge/src/rbf/cancel_active_utxo_management.rs @@ -1,10 +1,13 @@ -use crate::*; +use crate::{ + env, init_rbf_btc_pending_info, nano_to_sec, psbt_wrapper::PsbtWrapper, require, AccountId, + BTCPendingInfo, Contract, PendingInfoStage, PendingInfoState, RbfState, +}; impl Contract { pub fn check_cancel_active_utxo_management_rbf_psbt_valid( &self, original_tx_btc_pending_info: &BTCPendingInfo, - cancel_active_utxo_management_rbf_psbt: &Psbt, + cancel_active_utxo_management_rbf_psbt: &PsbtWrapper, ) -> (u128, u128) { let (actual_received_amount, gas_fee) = self.check_psbt_output_all_change_address( cancel_active_utxo_management_rbf_psbt, @@ -17,8 +20,10 @@ impl Contract { pub fn internal_cancel_active_utxo_management( &mut self, + _account_id: &AccountId, original_btc_pending_verify_id: String, - output: Vec, + cancel_active_utxo_management_rbf_psbt: PsbtWrapper, + _predecessor_account_id: AccountId, ) -> String { let original_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id); @@ -29,8 +34,6 @@ impl Contract { ); original_tx_btc_pending_info.assert_not_canceled(); original_tx_btc_pending_info.assert_active_utxo_management_original_pending_verify_tx(); - let cancel_active_utxo_management_rbf_psbt = self - .generate_psbt_from_original_psbt_and_new_output(original_tx_btc_pending_info, output); let mut btc_pending_info = init_rbf_btc_pending_info( original_tx_btc_pending_info, diff --git a/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs b/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs index dc2ca5b8..a1d57b44 100644 --- a/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs +++ b/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs @@ -1,10 +1,14 @@ -use crate::*; +use crate::{ + env, init_rbf_btc_pending_info, nano_to_sec, psbt_wrapper::PsbtWrapper, require, + AccessControllable, AccountId, BTCPendingInfo, Contract, PendingInfoStage, PendingInfoState, + RbfState, Role, +}; impl Contract { pub fn check_cancel_withdraw_rbf_psbt_valid( &self, original_tx_btc_pending_info: &BTCPendingInfo, - cancel_withdraw_rbf_psbt: &Psbt, + cancel_withdraw_rbf_psbt: &PsbtWrapper, ) -> (u128, u128) { let (actual_received_amount, gas_fee) = self.check_psbt_output_all_change_address( cancel_withdraw_rbf_psbt, @@ -17,8 +21,10 @@ impl Contract { pub fn internal_cancel_withdraw( &mut self, + _account_id: &AccountId, original_btc_pending_verify_id: String, - output: Vec, + cancel_withdraw_rbf_psbt: PsbtWrapper, + predecessor_account_id: AccountId, ) -> String { let original_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id); @@ -29,8 +35,6 @@ impl Contract { ); original_tx_btc_pending_info.assert_not_canceled(); original_tx_btc_pending_info.assert_withdraw_original_pending_verify_tx(); - let cancel_withdraw_rbf_psbt = self - .generate_psbt_from_original_psbt_and_new_output(original_tx_btc_pending_info, output); let mut btc_pending_info = init_rbf_btc_pending_info( original_tx_btc_pending_info, @@ -47,15 +51,12 @@ impl Contract { btc_pending_info.gas_fee = gas_fee; btc_pending_info.burn_amount = gas_fee; btc_pending_info.actual_received_amount = actual_received_amount; - // Ensure that the RBF transaction pays more gas than the previous transaction. - let max_gas_fee = original_tx_btc_pending_info.get_max_gas_fee(); - let additional_gas_amount = gas_fee.saturating_sub(max_gas_fee); - require!(additional_gas_amount > 0, "No gas increase."); + Self::check_withdraw_chain_specific(original_tx_btc_pending_info, gas_fee); let excess_gas_fee = gas_fee .saturating_sub(btc_pending_info.transfer_amount - btc_pending_info.withdraw_fee); if excess_gas_fee > 0 { require!( - self.acl_has_role(Role::DAO.into(), env::predecessor_account_id()), + self.acl_has_role(Role::DAO.into(), predecessor_account_id), "gas fee exceeds the user's balance, only the owner is allowed to cancel" ); require!( diff --git a/contracts/satoshi-bridge/src/rbf/mod.rs b/contracts/satoshi-bridge/src/rbf/mod.rs index 1b3639eb..05db27a5 100644 --- a/contracts/satoshi-bridge/src/rbf/mod.rs +++ b/contracts/satoshi-bridge/src/rbf/mod.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + env, nano_to_sec, psbt_wrapper::PsbtWrapper, require, BTCPendingInfo, Contract, + PendingInfoState, +}; pub mod active_utxo_management; pub mod cancel_active_utxo_management; @@ -10,11 +13,11 @@ impl Contract { &mut self, original_btc_pending_verify_id: &str, mut btc_pending_info: BTCPendingInfo, - psbt: Psbt, + psbt: PsbtWrapper, is_cancel: bool, ) -> String { - let rbf_psbt_hex = psbt.serialize_hex(); - let btc_pending_id = psbt.extract_tx().unwrap().compute_txid().to_string(); + let rbf_psbt_hex = psbt.serialize(); + let btc_pending_id = psbt.get_pending_id(); btc_pending_info.btc_pending_id.clone_from(&btc_pending_id); btc_pending_info.psbt_hex = rbf_psbt_hex; require!( @@ -46,13 +49,13 @@ pub fn init_rbf_btc_pending_info( ) -> BTCPendingInfo { BTCPendingInfo { account_id: original_tx_btc_pending_info.account_id.clone(), - btc_pending_id: Default::default(), + btc_pending_id: String::new(), transfer_amount: original_tx_btc_pending_info.transfer_amount, actual_received_amount: original_tx_btc_pending_info.actual_received_amount, withdraw_fee: original_tx_btc_pending_info.withdraw_fee, burn_amount: original_tx_btc_pending_info.burn_amount, gas_fee: original_tx_btc_pending_info.gas_fee, - psbt_hex: Default::default(), + psbt_hex: String::new(), vutxos: original_tx_btc_pending_info.vutxos.clone(), signatures: vec![None; original_tx_btc_pending_info.signatures.len()], tx_bytes_with_sign: None, diff --git a/contracts/satoshi-bridge/src/rbf/withdraw.rs b/contracts/satoshi-bridge/src/rbf/withdraw.rs index cbeec961..36fdc9a4 100644 --- a/contracts/satoshi-bridge/src/rbf/withdraw.rs +++ b/contracts/satoshi-bridge/src/rbf/withdraw.rs @@ -1,28 +1,28 @@ -use crate::*; +use crate::{ + init_rbf_btc_pending_info, network::Address, psbt_wrapper::PsbtWrapper, require, AccountId, + BTCPendingInfo, Contract, PendingInfoStage, PendingInfoState, RbfState, +}; impl Contract { pub fn check_withdraw_rbf_psbt_valid( &self, original_tx_btc_pending_info: &BTCPendingInfo, - withdraw_rbf_psbt: &Psbt, + withdraw_rbf_psbt: &PsbtWrapper, ) -> (u128, u128) { let withdraw_change_address_script_pubkey = - self.internal_config().get_change_address().script_pubkey(); - let original_tx = original_tx_btc_pending_info.get_transaction(); - let target_address_script_pubkey = original_tx - .output - .iter() - .find(|v| v.script_pubkey != withdraw_change_address_script_pubkey) - .cloned() - .expect("The original tx is not a user withdraw tx.") - .script_pubkey; + self.internal_config().get_change_script_pubkey(); + let original_tx = + original_tx_btc_pending_info.get_transaction(&self.internal_config().chain); require!( - original_tx.output.len() == withdraw_rbf_psbt.unsigned_tx.output.len(), + original_tx.output().len() == withdraw_rbf_psbt.get_output_num(), "Invalid output num" ); + + let target_address = self.extract_recipient_address(original_tx_btc_pending_info); + let (_, _, actual_received_amount, gas_fee) = self.check_withdraw_psbt( withdraw_rbf_psbt, - &target_address_script_pubkey, + target_address, &withdraw_change_address_script_pubkey, &original_tx_btc_pending_info.vutxos, original_tx_btc_pending_info.transfer_amount, @@ -35,7 +35,8 @@ impl Contract { &mut self, account_id: &AccountId, original_btc_pending_verify_id: String, - output: Vec, + withdraw_rbf_psbt: PsbtWrapper, + _predecessor_account_id: AccountId, ) -> String { let original_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id); @@ -45,8 +46,6 @@ impl Contract { ); original_tx_btc_pending_info.assert_not_canceled(); original_tx_btc_pending_info.assert_withdraw_original_pending_verify_tx(); - let withdraw_rbf_psbt = self - .generate_psbt_from_original_psbt_and_new_output(original_tx_btc_pending_info, output); let mut btc_pending_info = init_rbf_btc_pending_info( original_tx_btc_pending_info, @@ -60,10 +59,8 @@ impl Contract { btc_pending_info.gas_fee = gas_fee; btc_pending_info.actual_received_amount = actual_received_amount; btc_pending_info.burn_amount = actual_received_amount + gas_fee; - // Ensure that the RBF transaction pays more gas than the previous transaction. - let max_gas_fee = original_tx_btc_pending_info.get_max_gas_fee(); - let additional_gas_amount = gas_fee.saturating_sub(max_gas_fee); - require!(additional_gas_amount > 0, "No gas increase."); + Self::check_withdraw_chain_specific(original_tx_btc_pending_info, gas_fee); + self.internal_unwrap_mut_btc_pending_info(&original_btc_pending_verify_id) .update_max_gas_fee(gas_fee); self.set_rbf_pending_info( @@ -74,3 +71,34 @@ impl Contract { ) } } + +impl Contract { + fn extract_recipient_address(&self, original_tx_btc_pending_info: &BTCPendingInfo) -> String { + let psbt = original_tx_btc_pending_info.get_psbt(); + + if let Some(recipient) = psbt.get_recipient_address().clone() { + return recipient; + } + + let withdraw_change_address_script_pubkey = + self.internal_config().get_change_script_pubkey(); + let original_tx = + original_tx_btc_pending_info.get_transaction(&self.internal_config().chain); + let target_address_script_pubkey = original_tx + .output() + .iter() + .find(|v| v.script_pubkey != withdraw_change_address_script_pubkey) + .cloned() + .expect("The original tx is not a user withdraw tx.") + .script_pubkey; + + let target_address = Address::from_script( + target_address_script_pubkey.as_script(), + self.internal_config().chain.clone(), + ) + .expect("Error on extract recipient address from script pubkey") + .to_string(); + + target_address + } +} diff --git a/contracts/satoshi-bridge/src/token_transfer.rs b/contracts/satoshi-bridge/src/token_transfer.rs index 57572287..c0edeff6 100644 --- a/contracts/satoshi-bridge/src/token_transfer.rs +++ b/contracts/satoshi-bridge/src/token_transfer.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + env, is_promise_success, near, AccountId, Contract, ContractExt, Event, Gas, NearToken, + Promise, U128, +}; use near_contract_standards::fungible_token::core::ext_ft_core; pub const GAS_FOR_TOKEN_TRANSFER: Gas = Gas::from_tgas(20); diff --git a/contracts/satoshi-bridge/src/unit/mod.rs b/contracts/satoshi-bridge/src/unit/mod.rs index 57b97bea..87b86a37 100644 --- a/contracts/satoshi-bridge/src/unit/mod.rs +++ b/contracts/satoshi-bridge/src/unit/mod.rs @@ -1,8 +1,14 @@ +#[cfg(not(feature = "zcash"))] +use crate::network::Chain::BitcoinTestnet; +#[cfg(feature = "zcash")] +use crate::network::Chain::ZcashTestnet; use crate::*; use near_sdk::test_utils::VMContextBuilder; pub use near_sdk::testing_env; mod post_action; +mod storage; +mod utils; pub fn burrowland_id() -> AccountId { "burrowland_id".parse().unwrap() @@ -34,6 +40,10 @@ pub fn btc_light_client_id() -> AccountId { pub fn init_contract() -> Contract { Contract::new(Config { + #[cfg(not(feature = "zcash"))] + chain: BitcoinTestnet, + #[cfg(feature = "zcash")] + chain: ZcashTestnet, chain_signatures_account_id: chain_signatures_id(), nbtc_account_id: nbtc_id(), btc_light_client_account_id: btc_light_client_id(), @@ -69,6 +79,8 @@ pub fn init_contract() -> Contract { chain_signatures_root_public_key: None, change_address: None, unhealthy_utxo_amount: 1000, + #[cfg(feature = "zcash")] + expiry_height_gap: 1000, }) } diff --git a/contracts/satoshi-bridge/src/unit/post_action.rs b/contracts/satoshi-bridge/src/unit/post_action.rs index 15772b0e..fc06a8ec 100644 --- a/contracts/satoshi-bridge/src/unit/post_action.rs +++ b/contracts/satoshi-bridge/src/unit/post_action.rs @@ -54,10 +54,10 @@ mod extend_post_action_msg_templates { .build()); unit_env .contract - .extend_post_action_msg_templates(burrowland_id(), HashSet::from(["".to_string()])); + .extend_post_action_msg_templates(burrowland_id(), HashSet::from([String::new()])); unit_env .contract - .extend_post_action_msg_templates(burrowland_id(), HashSet::from(["".to_string()])); + .extend_post_action_msg_templates(burrowland_id(), HashSet::from([String::new()])); } #[test] @@ -70,7 +70,7 @@ mod extend_post_action_msg_templates { .build()); unit_env.contract.extend_post_action_msg_templates( burrowland_id(), - HashSet::from(["".to_string(), "".to_string(), "aa".to_string()]), + HashSet::from([String::new(), "aa".to_string()]), ); let post_action_msg_templates = unit_env .contract @@ -151,7 +151,7 @@ mod remove_post_action_msg_templates { .build()); unit_env .contract - .extend_post_action_msg_templates(burrowland_id(), HashSet::from(["".to_string()])); + .extend_post_action_msg_templates(burrowland_id(), HashSet::from([String::new()])); unit_env .contract .remove_post_action_msg_templates(burrowland_id(), Some(HashSet::new())); @@ -168,7 +168,7 @@ mod remove_post_action_msg_templates { .build()); unit_env .contract - .extend_post_action_msg_templates(burrowland_id(), HashSet::from(["".to_string()])); + .extend_post_action_msg_templates(burrowland_id(), HashSet::from([String::new()])); unit_env.contract.remove_post_action_msg_templates( burrowland_id(), Some(HashSet::from(["aa".to_string()])), @@ -186,7 +186,7 @@ mod remove_post_action_msg_templates { unit_env.contract.extend_post_action_msg_templates( burrowland_id(), HashSet::from([ - "".to_string(), + String::new(), "aa".to_string(), "bb".to_string(), "cc".to_string(), @@ -239,6 +239,7 @@ fn test_check_deposit_msg() { recipient_id: recipient_id(), post_actions: None, extra_msg: None, + safe_deposit: None }, 100 ) @@ -251,6 +252,7 @@ fn test_check_deposit_msg() { recipient_id: recipient_id(), post_actions: Some(vec![]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -265,25 +267,26 @@ fn test_check_deposit_msg() { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: None }, PostAction { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: None }, PostAction { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: None }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -298,18 +301,19 @@ fn test_check_deposit_msg() { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: None }, PostAction { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: None }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -332,10 +336,11 @@ fn test_check_deposit_msg() { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(200)) },]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -350,18 +355,19 @@ fn test_check_deposit_msg() { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(50)) }, PostAction { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(10)) }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -376,18 +382,19 @@ fn test_check_deposit_msg() { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(50)) }, PostAction { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(100)) }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -402,18 +409,19 @@ fn test_check_deposit_msg() { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(50)) }, PostAction { receiver_id: burrowland_id(), amount: U128(100), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(50)) }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -425,7 +433,7 @@ fn test_check_deposit_msg() { .build()); unit_env .contract - .extend_post_action_msg_templates(burrowland_id(), HashSet::from(["".to_string()])); + .extend_post_action_msg_templates(burrowland_id(), HashSet::from([String::new()])); assert!(unit_env .contract .check_deposit_msg( @@ -439,6 +447,7 @@ fn test_check_deposit_msg() { gas: Some(Gas::from_tgas(50)) },]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -452,10 +461,11 @@ fn test_check_deposit_msg() { receiver_id: burrowland_id(), amount: U128(10), memo: None, - msg: "".to_string(), + msg: String::new(), gas: Some(Gas::from_tgas(50)) },]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -479,6 +489,7 @@ fn test_check_deposit_msg() { }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -498,6 +509,7 @@ fn test_check_deposit_msg() { }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -517,6 +529,7 @@ fn test_check_deposit_msg() { }, ]), extra_msg: None, + safe_deposit: None }, 100 ) @@ -536,6 +549,7 @@ fn test_check_deposit_msg() { }, ]), extra_msg: None, + safe_deposit: None }, 100 ) diff --git a/contracts/satoshi-bridge/src/unit/storage.rs b/contracts/satoshi-bridge/src/unit/storage.rs new file mode 100644 index 00000000..7212716a --- /dev/null +++ b/contracts/satoshi-bridge/src/unit/storage.rs @@ -0,0 +1,48 @@ +use crate::*; + +impl Contract { + pub(crate) fn calculate_required_balance_for_safe_deposit(&mut self) -> NearToken { + let storage_usage = env::storage_usage(); + + let txid = "83d28cfcff0d86035e2742ecde99ef4801bb9f928d7d0118a0c1dd87bdc299ac".to_owned(); + let vout = u32::MAX; + let path = get_deposit_path(&DepositMsg { + recipient_id: env::current_account_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }); + let utxo_storage_key = generate_utxo_storage_key(txid, vout); + let tx_bytes = vec![0u8; 300]; // TODO: optimise storage usage + + self.data_mut().utxos.insert( + utxo_storage_key.clone(), + VUTXO::Current(UTXO { + path, + tx_bytes, + vout: vout.try_into().unwrap(), + balance: u64::MAX, + }), + ); + + self.data_mut() + .verified_deposit_utxo + .insert(utxo_storage_key); + + let required_storage_for_deposit = env::storage_usage().saturating_sub(storage_usage); + + env::storage_byte_cost().saturating_mul(required_storage_for_deposit.into()) + } +} + +#[test] +fn test_storage_for_deposit() { + let mut unit_env = init_unit_env(); + + assert_eq!( + unit_env + .contract + .calculate_required_balance_for_safe_deposit(), + unit_env.contract.required_balance_for_safe_deposit() + ); +} diff --git a/contracts/satoshi-bridge/src/unit/utils.rs b/contracts/satoshi-bridge/src/unit/utils.rs new file mode 100644 index 00000000..16e2bad4 --- /dev/null +++ b/contracts/satoshi-bridge/src/unit/utils.rs @@ -0,0 +1,70 @@ +use crate::chain_signature::{BigR, SignatureResponse, S}; +use crate::{ + serde_json, MAX_BOOL_RESULT, MAX_FT_TRANSFER_CALL_RESULT, MAX_PUBLIC_KEY_RESULT, + MAX_SIGNATURE_RESULT, +}; +use near_sdk::json_types::U128; +use near_sdk::PublicKey; + +#[test] +fn test_bool_result_size_fits_constant() { + // `false` is the larger of the two bool JSON representations (5 bytes vs 4 for `true`) + let serialized = serde_json::to_vec(&false).unwrap(); + assert!( + serialized.len() <= MAX_BOOL_RESULT, + "serialized bool ({} bytes) exceeds MAX_BOOL_RESULT ({})", + serialized.len(), + MAX_BOOL_RESULT, + ); +} + +#[test] +fn test_ft_transfer_call_result_size_fits_constant() { + // U128(u128::MAX) produces the longest possible decimal string representation + let serialized = serde_json::to_vec(&U128(u128::MAX)).unwrap(); + assert!( + serialized.len() <= MAX_FT_TRANSFER_CALL_RESULT, + "serialized U128::MAX ({} bytes) exceeds MAX_FT_TRANSFER_CALL_RESULT ({})", + serialized.len(), + MAX_FT_TRANSFER_CALL_RESULT, + ); +} + +#[test] +fn test_public_key_result_size_fits_constant() { + // secp256k1 compressed public key (33 bytes raw, base58-encoded with curve prefix) + let pk: PublicKey = "secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3" + .parse() + .unwrap(); + let serialized = serde_json::to_vec(&pk).unwrap(); + assert!( + serialized.len() <= MAX_PUBLIC_KEY_RESULT, + "serialized PublicKey ({} bytes) exceeds MAX_PUBLIC_KEY_RESULT ({})", + serialized.len(), + MAX_PUBLIC_KEY_RESULT, + ); +} + +#[test] +fn test_signature_result_size_fits_constant() { + // Realistic worst-case SignatureResponse with full-length hex strings + let response = SignatureResponse { + big_r: BigR { + // Compressed secp256k1 point: 02/03 prefix + 64 hex digits = 66 chars + affine_point: "025802983164945D1C3E40818FF569E275451CC33613EDDFA0E54D23710DFAF3C8" + .to_string(), + }, + s: S { + // 256-bit scalar: 64 hex digits + scalar: "07511DF9E947BC61F88011A3166AA0E60E2D45BFCACD61AD35DB4340941C84DE".to_string(), + }, + recovery_id: 1, + }; + let serialized = serde_json::to_vec(&response).unwrap(); + assert!( + serialized.len() <= MAX_SIGNATURE_RESULT, + "serialized SignatureResponse ({} bytes) exceeds MAX_SIGNATURE_RESULT ({})", + serialized.len(), + MAX_SIGNATURE_RESULT, + ); +} diff --git a/contracts/satoshi-bridge/src/upgrade.rs b/contracts/satoshi-bridge/src/upgrade.rs index ec1ce1a0..f5bef78d 100644 --- a/contracts/satoshi-bridge/src/upgrade.rs +++ b/contracts/satoshi-bridge/src/upgrade.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{env, near, Contract, ContractExt, VersionedContractData}; #[near] impl Contract { @@ -11,6 +11,7 @@ impl Contract { contract.data = match contract.data { VersionedContractData::V0(data) => VersionedContractData::Current(data.into()), VersionedContractData::V1(data) => VersionedContractData::Current(data.into()), + VersionedContractData::V2(data) => VersionedContractData::Current(data.into()), VersionedContractData::Current(data) => VersionedContractData::Current(data), }; contract diff --git a/contracts/satoshi-bridge/src/utils.rs b/contracts/satoshi-bridge/src/utils.rs index 317498b4..f1ecea9d 100644 --- a/contracts/satoshi-bridge/src/utils.rs +++ b/contracts/satoshi-bridge/src/utils.rs @@ -1,9 +1,18 @@ use bitcoin::hashes::Hash; -use crate::*; +use crate::{env, Timestamp}; pub const UTXO_STORAGE_KEY_TAG: &str = "@"; +/// Maximum expected byte length of a JSON-serialized `bool` promise result (`true`/`false`). +pub const MAX_BOOL_RESULT: usize = 10; +/// Maximum expected byte length of a JSON-serialized `U128` promise result (e.g. from `ft_on_transfer`). +pub const MAX_FT_TRANSFER_CALL_RESULT: usize = 50; +/// Maximum expected byte length of a JSON-serialized `near_sdk::PublicKey` promise result. +pub const MAX_PUBLIC_KEY_RESULT: usize = 200; +/// Maximum expected byte length of a JSON-serialized `SignatureResponse` promise result. +pub const MAX_SIGNATURE_RESULT: usize = 300; + pub fn generate_utxo_storage_key(txid: String, vout: u32) -> String { format!( "{}{}{}", @@ -18,7 +27,8 @@ pub fn to_nano(sec: u32) -> Timestamp { } pub fn nano_to_sec(nano: u64) -> u32 { - (nano / 10u64.pow(9)) as u32 + u32::try_from(nano / 10u64.pow(9)) + .unwrap_or_else(|_| env::panic_str("Timestamp overflow when converting nano to sec")) } pub mod u64_dec_format { diff --git a/contracts/satoshi-bridge/src/utxo.rs b/contracts/satoshi-bridge/src/utxo.rs index e6832d36..ecefe7ea 100644 --- a/contracts/satoshi-bridge/src/utxo.rs +++ b/contracts/satoshi-bridge/src/utxo.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + generate_utxo_storage_key, near, psbt_wrapper::PsbtWrapper, u64_dec_format, Contract, OutPoint, +}; +use near_sdk::env; #[near(serializers = [borsh, json])] #[derive(Clone)] @@ -55,20 +58,19 @@ impl From for VUTXO { } impl Contract { - pub fn remove_vutxo_by_psbt(&mut self, psbt: &Psbt) -> (Vec, Vec) { + pub fn remove_vutxo_by_psbt(&mut self, psbt: &PsbtWrapper) -> (Vec, Vec) { let mut utxo_storage_keys = vec![]; let vutxos = psbt - .unsigned_tx - .input - .clone() + .get_utxo_storage_keys() .into_iter() - .map(|input| { - let utxo_storage_key = out_point_to_utxo_storage_key(&input.previous_output); + .map(|utxo_storage_key| { utxo_storage_keys.push(utxo_storage_key.clone()); self.data_mut() .utxos .remove(&utxo_storage_key) - .unwrap_or_else(|| panic!("UTXO {} not exist", utxo_storage_key)) + .unwrap_or_else(|| { + env::panic_str(&format!("UTXO {} not exist", utxo_storage_key)) + }) }) .collect::>(); (utxo_storage_keys, vutxos) diff --git a/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs b/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs new file mode 100644 index 00000000..104e3df7 --- /dev/null +++ b/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs @@ -0,0 +1,284 @@ +use crate::psbt_wrapper::PsbtWrapper; +use crate::zcash_utils::types::ChainSpecificData; +use crate::*; +use bitcoin::{OutPoint, TxOut}; +use near_sdk::json_types::U128; +use near_sdk::{near, require, AccountId}; + +pub const GAS_RBF_CALL_BACK: Gas = Gas::from_tgas(100); +pub const GAS_FOR_ACTIVE_UTXO_MANAGMENT_CALLBACK: Gas = Gas::from_tgas(100); + +macro_rules! define_rbf_callback { + ($method:ident, $callback_name:ident, $internal_fn:ident) => { + impl Contract { + pub(crate) fn $method( + &mut self, + user_account_id: AccountId, + original_btc_pending_verify_id: String, + output: Vec, + chain_specific_data: Option, + ) { + let predecessor_account_id = env::predecessor_account_id(); + self.get_last_block_height_promise().then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_RBF_CALL_BACK) + .$callback_name( + user_account_id, + original_btc_pending_verify_id, + output, + chain_specific_data, + predecessor_account_id, + ), + ); + } + } + + #[near] + impl Contract { + #[private] + pub fn $callback_name( + &mut self, + account_id: AccountId, + original_btc_pending_verify_id: String, + output: Vec, + chain_specific_data: Option, + presecessor_account_id: AccountId, + #[callback_unwrap] last_block_height: u32, + ) { + let expiry_height = self.get_expiry_height(&chain_specific_data, last_block_height); + let orchard_bundle_bytes = chain_specific_data.map(|c| c.orchard_bundle_bytes); + + let original_tx_btc_pending_info = + self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id); + + let new_psbt = self.generate_psbt_from_original_psbt_and_new_output( + original_tx_btc_pending_info, + output, + orchard_bundle_bytes.map(|b| b.0), + expiry_height, + last_block_height, + ); + + let btc_pending_id = self.$internal_fn( + &account_id, + original_btc_pending_verify_id, + new_psbt, + presecessor_account_id, + ); + + self.internal_unwrap_mut_account(&account_id) + .btc_pending_sign_id = Some(btc_pending_id.clone()); + + Event::GenerateBtcPendingInfo { + account_id: &account_id, + btc_pending_id: &btc_pending_id, + } + .emit(); + } + } + }; +} + +define_rbf_callback!( + withdraw_rbf_chain_specific, + withdraw_rbf_callback, + internal_withdraw_rbf +); +define_rbf_callback!( + cancel_withdraw_chain_specific, + cancel_withdraw_callback, + internal_cancel_withdraw +); +define_rbf_callback!( + active_utxo_management_rbf_chain_specific, + active_utxo_management_rbf_callback, + internal_active_utxo_management_rbf +); +define_rbf_callback!( + cancel_active_utxo_management_chain_specific, + cancel_active_utxo_management_callback, + internal_cancel_active_utxo_management +); + +#[near] +impl Contract { + #[private] + #[allow(clippy::too_many_arguments)] + pub fn ft_on_transfer_callback( + &mut self, + sender_id: AccountId, + amount: U128, + target_btc_address: String, + input: Vec, + output: Vec, + max_gas_fee: Option, + chain_specific_data: Option, + external_id: Option, + #[callback_unwrap] last_block_height: u32, + ) -> U128 { + let expiry_height = self.get_expiry_height(&chain_specific_data, last_block_height); + let orchard_bundle = chain_specific_data.map(|c| c.orchard_bundle_bytes.0); + + let psbt = PsbtWrapper::new( + input, + output, + orchard_bundle, + expiry_height, + last_block_height, + Some(target_btc_address.clone()), + self.internal_config(), + ); + + self.create_btc_pending_info( + sender_id, + amount.0, + target_btc_address, + psbt, + max_gas_fee, + external_id, + ); + + U128(0) + } + + #[private] + #[allow(clippy::too_many_arguments)] + pub fn active_utxo_management_callback( + &mut self, + account_id: AccountId, + input: Vec, + output: Vec, + #[callback_unwrap] last_block_height: u32, + ) { + let expiry_height = last_block_height + self.get_config().expiry_height_gap; + + // For active UTXO management, we don't validate orchard recipient/amount + // as this is internal bridge operations, not user withdrawals + let psbt = PsbtWrapper::new( + input, + output, + None, + expiry_height, + last_block_height, + None, + self.internal_config(), + ); + + self.create_active_utxo_management_pending_info(account_id, psbt); + } +} + +impl Contract { + fn get_expiry_height( + &self, + chain_specific_data: &Option, + last_block_height: u32, + ) -> u32 { + let expiry_height = if let Some(chain_specific_data) = chain_specific_data { + chain_specific_data.expiry_height + } else { + last_block_height + self.get_config().expiry_height_gap + }; + + require!( + expiry_height >= last_block_height + self.get_config().expiry_height_gap + && expiry_height <= last_block_height + 2 * self.get_config().expiry_height_gap, + format!( + "Invalid expiry height: {}. Expected value between {} and {}.", + expiry_height, + last_block_height + self.get_config().expiry_height_gap, + last_block_height + 2 * self.get_config().expiry_height_gap + ) + ); + + expiry_height + } + + pub(crate) fn check_psbt_chain_specific( + &self, + psbt: &PsbtWrapper, + gas_fee: u128, + target_btc_address: String, + ) { + let min_fee = psbt.get_min_fee(); + require!( + gas_fee >= min_fee.into_u64() as u128, + format!( + "Invalid gas fee ({}). min fee = {}.", + gas_fee, + min_fee.into_u64() + ) + ); + + // For withdrawals with Orchard bundle, calculate the expected net amount after fees + if psbt.has_orchard_bundle() { + psbt.validate_orchard_bundle(target_btc_address, self.internal_config().chain.clone()); + } + } + + pub(crate) fn check_withdraw_chain_specific( + _original_tx_btc_pending_info: &BTCPendingInfo, + _gas_fee: u128, + ) { + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn ft_on_transfer_withdraw_chain_specific( + &self, + sender_id: AccountId, + amount: u128, + target_btc_address: String, + input: Vec, + output: Vec, + max_gas_fee: Option, + chain_specific_data: Option, + ) -> PromiseOrValue { + PromiseOrValue::Promise( + self.get_last_block_height_promise().then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_FT_ON_TRANSFER_CALL_BACK) + .ft_on_transfer_callback( + sender_id, + amount.into(), + target_btc_address, + input, + output, + max_gas_fee, + chain_specific_data, + ), + ), + ) + } + + pub(crate) fn active_utxo_management_chain_specific( + &mut self, + account_id: AccountId, + input: Vec, + output: Vec, + ) { + self.get_last_block_height_promise().then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_FOR_ACTIVE_UTXO_MANAGMENT_CALLBACK) + .active_utxo_management_callback(account_id, input, output), + ); + } + + pub(crate) fn generate_psbt_from_original_psbt_and_new_output( + &self, + original_tx_btc_pending_info: &BTCPendingInfo, + output: Vec, + orchard_bundle_bytes: Option>, + expiry_height: u32, + current_height: u32, + ) -> PsbtWrapper { + let original_psbt = original_tx_btc_pending_info.get_psbt(); + PsbtWrapper::from_original_psbt( + original_psbt, + output, + orchard_bundle_bytes, + expiry_height, + current_height, + self.internal_config(), + ) + } +} diff --git a/contracts/satoshi-bridge/src/zcash_utils/mod.rs b/contracts/satoshi-bridge/src/zcash_utils/mod.rs new file mode 100644 index 00000000..ea6ecdfd --- /dev/null +++ b/contracts/satoshi-bridge/src/zcash_utils/mod.rs @@ -0,0 +1,5 @@ +pub mod contract_methods; +pub mod orchard_policy; +pub mod psbt_wrapper; +pub mod transaction; +pub mod types; diff --git a/contracts/satoshi-bridge/src/zcash_utils/orchard_policy.rs b/contracts/satoshi-bridge/src/zcash_utils/orchard_policy.rs new file mode 100644 index 00000000..f85813fd --- /dev/null +++ b/contracts/satoshi-bridge/src/zcash_utils/orchard_policy.rs @@ -0,0 +1,116 @@ +use crate::network::Address; +use crate::network::{Chain, OrchardRawAddress}; +use orchard::Bundle; +use std::io::Cursor; +use zcash_primitives::transaction::components::orchard::read_v5_bundle; +use zcash_protocol::value::ZatBalance; + +/// Bridge OVK used to recover outputs for policy checks. +/// Hardcoded to all zeroes for now; can be made configurable later. +pub const BRIDGE_OVK: [u8; 32] = [0u8; 32]; + +/// Minimum number of actions required in an Orchard bundle per the Orchard protocol. +/// The Orchard builder automatically pads bundles to meet this minimum for privacy. +/// See: https://github.com/zcash/orchard/blob/main/src/builder.rs#L36 +pub const EXPECTED_ACTIONS_NUMBER: usize = 1; + +pub struct OrchardOutput { + pub amount: u64, + pub recipient_addr: OrchardRawAddress, +} + +pub struct ParsedOrchardBundle { + pub bundle: Bundle, + pub output: OrchardOutput, +} + +impl ParsedOrchardBundle { + pub fn amount(&self) -> u128 { + self.output.amount.into() + } + + pub fn recipient_addr(&self) -> &OrchardRawAddress { + &self.output.recipient_addr + } +} + +pub fn extract_orchard_bundle( + orchard_bundle_bytes: Option>, +) -> Result, String> { + if let Some(orchard_bundle_bytes) = orchard_bundle_bytes { + let mut reader = Cursor::new(orchard_bundle_bytes); + let bundle = read_v5_bundle(&mut reader) + .map_err(|_| "Failed to read orchard bundle".to_string())? + .ok_or_else(|| "Orchard bundle is empty".to_string())?; + + // Check action count first per Orchard protocol requirements + if bundle.actions().len() != EXPECTED_ACTIONS_NUMBER { + return Err(format!( + "Orchard bundle must have {} actions, got {}", + EXPECTED_ACTIONS_NUMBER, + bundle.actions().len() + )); + } + + // Since we require exactly 1 action, directly recover the single output + let ovk = orchard::keys::OutgoingViewingKey::from(BRIDGE_OVK); + let (note, addr, _memo) = bundle + .recover_output_with_ovk(0, &ovk) + .ok_or_else(|| "Failed to recover Orchard output with bridge OVK".to_string())?; + + let value = note.value().inner(); + if value == 0 { + return Err("Orchard output value must be non-zero".to_string()); + } + + Ok(Some(ParsedOrchardBundle { + bundle, + output: OrchardOutput { + amount: value, + recipient_addr: addr.to_raw_address_bytes(), + }, + })) + } else { + Ok(None) + } +} + +/// Validate Orchard bundle against policy: +/// - Recovers all outputs using BRIDGE_OVK +/// - Verifies exactly one non-zero output exists +/// - Verifies the recovered amount is within expected range (allows dust adjustment) +/// - Verifies the recovered recipient matches the expected Unified Address's Orchard receiver +/// - Verifies value balance matches the output amount (value flows from transparent to Orchard) +pub fn validate_orchard_bundle( + orchard: &ParsedOrchardBundle, + expected_recipient: &str, + chain: &Chain, +) -> Result<(), String> { + let recipient_address = Address::parse(expected_recipient, chain.clone())?; + + // Validate recipient + let expected_addr_bytes = recipient_address.extract_orchard_receiver()?; + if orchard.recipient_addr() != &expected_addr_bytes { + return Err(format!( + "Orchard recipient mismatch: expected {} does not match recovered output", + expected_recipient + )); + } + + // Validate value balance: for withdrawal, value flows FROM transparent TO Orchard + // So value_balance should be negative and equal to the output amount + let value_balance = orchard.bundle.value_balance(); + let expected_value_balance = + -i64::try_from(orchard.amount()).map_err(|_| "Orchard amount too large for i64")?; + + let actual_value_balance: i64 = (*value_balance).into(); + if actual_value_balance != expected_value_balance { + return Err(format!( + "Orchard value balance mismatch: expected {}, got {}. \ + Value balance must equal negative output amount for withdrawals", + expected_value_balance, actual_value_balance + )); + } + + Ok(()) +} diff --git a/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs b/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs new file mode 100644 index 00000000..f3f376aa --- /dev/null +++ b/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs @@ -0,0 +1,541 @@ +use crate::network::ORCHARD_RAW_ADDRESS_SIZE; +use crate::zcash_utils::orchard_policy::{self, OrchardOutput, ParsedOrchardBundle}; +use crate::zcash_utils::transaction::{Transaction, TransparentUnauthorized}; +use crate::*; +use bitcoin::hashes::Hash; +use bitcoin::{OutPoint, TxOut}; +use near_sdk::{env, require}; +use std::io; +use std::io::{Cursor, Read, Write}; +use zcash_primitives::transaction::components::orchard::read_v5_bundle; +use zcash_primitives::transaction::fees::transparent::{InputSize, OutputView}; +use zcash_primitives::transaction::fees::FeeRule; +use zcash_primitives::transaction::{TransactionData, TransactionDigest, TxVersion}; +use zcash_protocol::consensus::{BlockHeight, BranchId}; +use zcash_protocol::value::Zatoshis; +use zcash_transparent::bundle::Authorized; +use zcash_transparent::bundle::TxIn as ZcashTxIn; +use zcash_transparent::bundle::TxOut as ZcashTxOut; +use zcash_transparent::sighash::SighashType; + +pub struct PsbtWrapper { + branch_id: BranchId, + expiry_height: u32, + vin: Vec>, + vout: Vec, + inputs_utxo: Vec, + orchard: Option, + recipient_address: Option, +} + +impl PsbtWrapper { + pub fn new( + input: Vec, + output: Vec, + orchard_bundle_bytes: Option>, + expiry_height: u32, + current_height: u32, + recipient_address: Option, + config: &Config, + ) -> Self { + require!(!input.is_empty(), "empty input"); + // Allow empty output if we have an orchard bundle (funds go to shielded pool) + require!( + !output.is_empty() || orchard_bundle_bytes.is_some(), + "empty output" + ); + + let sequence = bitcoin::Sequence::MAX; + let vout = output + .clone() + .into_iter() + .map(|o| ZcashTxOut { + value: Zatoshis::from_u64(o.value.to_sat()).unwrap(), + script_pubkey: zcash_primitives::legacy::Script(o.script_pubkey.to_bytes()), + }) + .collect(); + + let vin: Vec> = input + .into_iter() + .map(|i| ZcashTxIn { + prevout: zcash_transparent::bundle::OutPoint::new(*i.txid.as_byte_array(), i.vout), + script_sig: zcash_primitives::legacy::Script::default(), + sequence: sequence.0, + }) + .collect(); + + let inputs = vec![ + ZcashTxOut { + value: Zatoshis::from_u64(0).unwrap(), + script_pubkey: zcash_primitives::legacy::Script::default(), + }; + vin.len() + ]; + + let orchard = + orchard_policy::extract_orchard_bundle(orchard_bundle_bytes).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_ORCHARD_BUNDLE: failed to extract Orchard bundle") + }); + + Self { + branch_id: get_branch_id(current_height, config), + expiry_height, + vout, + vin, + inputs_utxo: inputs, + orchard, + recipient_address, + } + } + + pub fn validate_orchard_bundle(&self, expected_addr: String, chain: network::Chain) { + orchard_policy::validate_orchard_bundle( + self.orchard.as_ref().unwrap_or_else(|| { + env::panic_str("ERR_NO_ORCHARD_BUNDLE: Orchard bundle is required for validation") + }), + &expected_addr, + &chain, + ) + .unwrap_or_else(|_| { + env::panic_str("ERR_ORCHARD_VALIDATION: Orchard bundle validation failed") + }); + } + + pub fn from_original_psbt( + original_psbt: PsbtWrapper, + output: Vec, + orchard_bundle_bytes: Option>, + expiry_height: u32, + current_height: u32, + config: &Config, + ) -> Self { + let vout = if output.is_empty() { + original_psbt.vout.clone() + } else { + output + .clone() + .into_iter() + .map(|o| ZcashTxOut { + value: Zatoshis::from_u64(o.value.to_sat()).unwrap(), + script_pubkey: zcash_primitives::legacy::Script(o.script_pubkey.to_bytes()), + }) + .collect() + }; + + let orchard = + orchard_policy::extract_orchard_bundle(orchard_bundle_bytes).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_ORCHARD_BUNDLE: failed to extract Orchard bundle") + }); + + Self { + branch_id: get_branch_id(current_height, config), + expiry_height, + vin: original_psbt.vin, + vout, + inputs_utxo: original_psbt.inputs_utxo, + orchard, + recipient_address: original_psbt.recipient_address, + } + } + + pub fn set_input_utxo(&mut self, input_utxo: Vec) { + input_utxo.iter().enumerate().for_each(|(i, v)| { + self.inputs_utxo[i] = ZcashTxOut { + value: Zatoshis::from_u64(v.value.to_sat()).unwrap(), + script_pubkey: zcash_primitives::legacy::Script(v.script_pubkey.to_bytes()), + } + }); + } + + pub fn get_input_num(&self) -> usize { + self.vin.len() + } + + pub fn get_output_num(&self) -> usize { + self.vout.len() + } + + pub fn has_orchard_bundle(&self) -> bool { + self.orchard.is_some() + } + + /// Get the Orchard output amount by recovering it with the bridge OVK. + /// Returns the amount in zatoshis (satoshis for ZCash). + /// Panics if there is no Orchard bundle. + pub fn get_orchard_output_amount(&self) -> u128 { + self.orchard + .as_ref() + .unwrap_or_else(|| env::panic_str("No Orchard bundle present")) + .amount() + } + + pub fn get_utxo_storage_keys(&self) -> Vec { + self.vin + .clone() + .into_iter() + .map(|out_point| { + generate_utxo_storage_key( + out_point.prevout.txid().to_string(), + out_point.prevout.n(), + ) + }) + .collect() + } + + pub fn add_extra_outputs(&self, actual_received_amounts: &mut Vec) -> u128 { + if let Some(orchard) = &self.orchard { + actual_received_amounts.push(orchard.amount()); + return orchard.amount(); + } + + 0 + } + + pub fn get_output(&self) -> Vec { + self.vout + .clone() + .into_iter() + .map(|i| TxOut { + value: bitcoin::Amount::from_sat(i.value.into_u64()), + script_pubkey: ScriptBuf::from_bytes(i.script_pubkey.0), + }) + .collect() + } + + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::::new(); + let version: u8 = 3; + buf.push(version); + match self.branch_id { + BranchId::Nu6 => buf.write_all(&[7u8; 1]).unwrap(), + BranchId::Nu6_1 => buf.write_all(&[8u8; 1]).unwrap(), + _ => unreachable!(), + } + buf.write_all(&self.expiry_height.to_le_bytes()).unwrap(); + + let len = self.vin.len() as u64; + buf.write_all(&len.to_le_bytes()).unwrap(); + + for t in self.vin.clone() { + t.write(&mut buf).unwrap(); + } + + let len = self.vout.len() as u64; + buf.write_all(&len.to_le_bytes()).unwrap(); + + for t in self.vout.clone() { + t.write(&mut buf).unwrap(); + } + + let len = self.inputs_utxo.len() as u64; + buf.write_all(&len.to_le_bytes()).unwrap(); + + for t in self.inputs_utxo.clone() { + t.write(&mut buf).unwrap(); + } + + if let Some(orchard) = &self.orchard { + zcash_primitives::transaction::components::orchard::write_v5_bundle( + Some(&orchard.bundle), + &mut buf, + ) + .unwrap(); + + buf.write_all(&[1u8; 1]).unwrap(); + buf.write_all(&orchard.output.amount.to_le_bytes()).unwrap(); + buf.write_all(&orchard.output.recipient_addr).unwrap(); + } else { + buf.write_all(&[0u8; 1]).unwrap(); + } + + if let Some(recipient_address) = &self.recipient_address { + buf.write_all(&[1u8; 1]).unwrap(); + let recipient_address_bytes = recipient_address.as_bytes(); + + let len = recipient_address_bytes.len() as u64; + buf.write_all(&len.to_le_bytes()).unwrap(); + + buf.write_all(recipient_address_bytes).unwrap(); + } else { + buf.write_all(&[0u8; 1]).unwrap(); + } + + buf + } + pub fn serialize(&self) -> String { + hex::encode(self.to_bytes()) + } + + pub fn deserialize(psbt_hex: &String) -> Self { + let bytes = hex::decode(psbt_hex) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT_HEX: failed to decode hex")); + let mut rdr = Cursor::new(bytes); + let version = read_u8(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read version")); + let branch_id = if version >= 2 { + let branch_id_u8 = read_u8(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read branch_id")); + match branch_id_u8 { + 7 => BranchId::Nu6, + 8 => BranchId::Nu6_1, + _ => env::panic_str("ERR_INVALID_PSBT: unsupported branch_id"), + } + } else { + BranchId::Nu6_1 + }; + + let expiry_height = read_u32_le(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read expiry_height")); + + let vin_len = read_u64_le(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read vin length")) + as usize; + let mut vin = Vec::with_capacity(vin_len); + for _ in 0..vin_len { + vin.push( + ZcashTxIn::::read(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read vin")), + ); + } + + let vout_len = read_u64_le(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read vout length")) + as usize; + let mut vout = Vec::with_capacity(vout_len); + for _ in 0..vout_len { + vout.push( + ZcashTxOut::read(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read vout")), + ); + } + + let inputs_len = read_u64_le(&mut rdr) + .unwrap_or_else(|_| env::panic_str("ERR_INVALID_PSBT: failed to read inputs length")) + as usize; + let mut inputs = Vec::with_capacity(inputs_len); + for _ in 0..inputs_len { + inputs.push( + ZcashTxOut::read(&mut rdr).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_PSBT: failed to read input utxo") + }), + ); + } + + let orchard_bundle = if version >= 3 { + read_v5_bundle(&mut rdr).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_PSBT: failed to read Orchard bundle") + }) + } else { + None + }; + + let orchard = if let Some(orchard_bundle) = orchard_bundle { + let is_some = read_u8(&mut rdr).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_PSBT: failed to read orchard_output flag") + }); + if is_some == 1 { + let amount = read_u64_le(&mut rdr).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_PSBT: failed to read orchard amount") + }); + let mut addr = [0u8; ORCHARD_RAW_ADDRESS_SIZE]; + for addr_byte in &mut addr { + *addr_byte = read_u8(&mut rdr).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_PSBT: failed to read orchard address") + }); + } + + Some(ParsedOrchardBundle { + bundle: orchard_bundle, + output: OrchardOutput { + amount, + recipient_addr: addr, + }, + }) + } else { + None + } + } else { + None + }; + + let recipient_address = if version >= 3 { + let is_some = read_u8(&mut rdr).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_PSBT: failed to read recipient_address flag") + }); + if is_some == 1 { + Some(read_string(&mut rdr).unwrap_or_else(|_| { + env::panic_str("ERR_INVALID_PSBT: failed to read recipient_address") + })) + } else { + None + } + } else { + None + }; + + Self { + branch_id, + expiry_height, + vin, + vout, + inputs_utxo: inputs, + orchard, + recipient_address, + } + } + + pub fn extract_tx_bytes_with_sign(self) -> Vec { + self.get_zcash_tx() + .encode() + .unwrap_or_else(|_| env::panic_str("ERR_TX_ENCODE: failed to encode Zcash transaction")) + } + + pub fn get_zcash_tx(self) -> Transaction { + let transparent_bundle = zcash_transparent::bundle::Bundle { + vin: self.vin.clone(), + vout: self.vout.clone(), + authorization: zcash_transparent::bundle::Authorized, + }; + + // Here we encode the Zcash transaction with orchard bundle so it can be submited to the network + let inner_tx = TransactionData::from_parts( + TxVersion::V5, + self.branch_id, + 0, + BlockHeight::from(self.expiry_height), + Some(transparent_bundle), + None, + None, + self.orchard.map(|b| b.bundle), + ) + .freeze() + .unwrap_or_else(|_| { + env::panic_str("ERR_TX_FREEZE: failed to freeze Zcash transaction data") + }); + + Transaction { inner_tx } + } + + pub fn get_pending_id(self) -> String { + self.get_zcash_tx().compute_txid().to_string() + } + + fn tx_digest>( + &self, + tx_data: &TransactionData, + digester: D, + ) -> D::Digest { + digester.combine( + digester.digest_header( + tx_data.version(), + tx_data.consensus_branch_id(), + tx_data.lock_time(), + tx_data.expiry_height(), + ), + digester.digest_transparent(tx_data.transparent_bundle()), + digester.digest_sapling(None), + digester.digest_orchard(self.orchard.as_ref().map(|b| &b.bundle)), + ) + } + + #[allow(unused_variables)] + pub fn get_hash_to_sign(&self, vin: usize, public_keys: &[bitcoin::PublicKey]) -> [u8; 32] { + let tx_data = WrappedTransaction::to_zcash_tx( + &self.vin, + &self.vout, + &self.inputs_utxo, + self.expiry_height, + public_keys, + self.branch_id, + ); + let txid_parts = + self.tx_digest(&tx_data, zcash_primitives::transaction::txid::TxIdDigester); + + let script = &self.inputs_utxo[vin].script_pubkey; + let sig_input = zcash_primitives::transaction::sighash::SignableInput::Transparent( + zcash_transparent::sighash::SignableInput::from_parts( + SighashType::ALL, + vin, + script, + script, + self.inputs_utxo[vin].value, + ), + ); + + *zcash_primitives::transaction::sighash::signature_hash(&tx_data, &sig_input, &txid_parts) + .as_ref() + } + + pub fn save_signature( + &mut self, + sign_index: usize, + signature: SignatureResponse, + public_key: bitcoin::secp256k1::PublicKey, + ) { + let script_sig = bitcoin::script::Builder::new() + .push_slice(signature.to_btc_signature().serialize()) + .push_key(&bitcoin::PublicKey::new(public_key)) + .into_script(); + + self.vin[sign_index].script_sig = zcash_primitives::legacy::Script(script_sig.to_bytes()); + } + + pub fn get_min_fee(&self) -> Zatoshis { + let fee_rule = zcash_primitives::transaction::fees::zip317::FeeRule::standard(); + let orchard_action_count = self + .orchard + .as_ref() + .map(|orchard| orchard.bundle.actions().len()) + .unwrap_or(0); + + fee_rule + .fee_required( + &zcash_protocol::consensus::MainNetwork, + BlockHeight::from_u32(0u32), + vec![InputSize::STANDARD_P2PKH; self.vin.len()], + self.vout.iter().map(|i| i.serialized_size()), + 0, // sapling_input_count + 0, // sapling_output_count + orchard_action_count, + ) + .unwrap() + } + + pub fn get_recipient_address(&self) -> Option { + self.recipient_address.clone() + } +} + +fn get_branch_id(current_height: u32, config: &Config) -> BranchId { + config.chain.get_branch_id(current_height) +} + +fn read_u32_le(r: &mut R) -> io::Result { + let mut b = [0u8; 4]; + r.read_exact(&mut b)?; + Ok(u32::from_le_bytes(b)) +} + +fn read_u8(r: &mut R) -> io::Result { + let mut b = [0u8; 1]; + r.read_exact(&mut b)?; + Ok(b[0]) +} + +fn read_string(r: &mut R) -> io::Result { + let len = read_u64_le(r)? as usize; + let mut recipient_address_bytes = vec![]; + + for _ in 0..len { + recipient_address_bytes.push(read_u8(r)?); + } + + String::from_utf8(recipient_address_bytes.to_vec()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) +} + +fn read_u64_le(r: &mut R) -> io::Result { + let mut b = [0u8; 8]; + r.read_exact(&mut b)?; + Ok(u64::from_le_bytes(b)) +} diff --git a/contracts/satoshi-bridge/src/zcash_utils/transaction.rs b/contracts/satoshi-bridge/src/zcash_utils/transaction.rs new file mode 100644 index 00000000..94d2f49c --- /dev/null +++ b/contracts/satoshi-bridge/src/zcash_utils/transaction.rs @@ -0,0 +1,112 @@ +use crate::network; +use bitcoin::hashes::Hash; +use bitcoin::{absolute, ScriptBuf, TxOut, Txid}; +use zcash_primitives::consensus::{BlockHeight, BranchId}; +use zcash_primitives::transaction::{Transaction as ZCashTransaction, TransactionData, TxVersion}; +use zcash_transparent::builder::TransparentBuilder; +use zcash_transparent::bundle::Authorized; + +pub struct TransparentUnauthorized; +impl zcash_primitives::transaction::Authorization for TransparentUnauthorized { + type TransparentAuth = zcash_transparent::builder::Unauthorized; + type SaplingAuth = sapling_crypto::bundle::Authorized; + type OrchardAuth = orchard::bundle::Authorized; +} + +#[derive(Debug, PartialEq)] +pub struct Transaction { + pub inner_tx: ZCashTransaction, +} + +impl Transaction { + pub fn compute_txid(&self) -> Txid { + Txid::from_byte_array(*self.inner_tx.txid().as_ref()) + } + + pub fn output(&self) -> Vec { + let outputs = self.inner_tx.transparent_bundle().unwrap().vout.clone(); + outputs + .into_iter() + .map(|o| bitcoin::TxOut { + value: bitcoin::Amount::from_sat(o.value.into_u64()), + script_pubkey: ScriptBuf::from(bitcoin::Script::from_bytes(&o.script_pubkey.0)), + }) + .collect() + } + + pub fn lock_time(&self) -> absolute::LockTime { + let lock_time = self.inner_tx.lock_time(); + absolute::LockTime::from_consensus(lock_time) + } + + pub fn encode(&self) -> Result, std::io::Error> { + let mut buf = Vec::new(); + self.inner_tx.write(&mut buf)?; + + Ok(buf) + } + + pub fn decode(data: &[u8], chain: &network::Chain) -> Result { + let mut cursor = std::io::Cursor::new(data); + let branch_id = match chain { + network::Chain::ZcashTestnet => BranchId::Nu6_1, + _ => BranchId::Nu6, + }; + let tx = ZCashTransaction::read(&mut cursor, branch_id)?; + Ok(Self { inner_tx: tx }) + } + + pub fn get_transparent_builder( + vin: &[zcash_transparent::bundle::TxIn], + vout: &[zcash_transparent::bundle::TxOut], + input: &[zcash_transparent::bundle::TxOut], + public_keys: &[bitcoin::PublicKey], + ) -> TransparentBuilder { + let mut builder = zcash_transparent::builder::TransparentBuilder::empty(); + + for index in 0..vin.len() { + builder + .add_input( + public_keys[index].inner, + vin[index].prevout.clone(), + input[index].clone(), + ) + .unwrap(); + } + + for output in vout { + let key = output.script_pubkey.0[3..23].try_into().unwrap(); + let to = zcash_transparent::address::TransparentAddress::PublicKeyHash(key); + builder.add_output(&to, output.value).unwrap(); + } + + builder + } + + pub fn to_zcash_tx( + vin: &[zcash_transparent::bundle::TxIn], + vout: &[zcash_transparent::bundle::TxOut], + input: &[zcash_transparent::bundle::TxOut], + expiry_height: u32, + public_keys: &[bitcoin::PublicKey], + branch_id: BranchId, + ) -> TransactionData { + let transparent_bundle = Self::get_transparent_builder(vin, vout, input, public_keys) + .build() + .unwrap(); + + let lock_time = 0; + let expiry_height = BlockHeight::from_u32(expiry_height); + + TransactionData::from_parts( + TxVersion::V5, + branch_id, + lock_time, + expiry_height, + Some(transparent_bundle), + None, + None, + None, + ) + } +} diff --git a/contracts/satoshi-bridge/src/zcash_utils/types.rs b/contracts/satoshi-bridge/src/zcash_utils/types.rs new file mode 100644 index 00000000..aab7a40c --- /dev/null +++ b/contracts/satoshi-bridge/src/zcash_utils/types.rs @@ -0,0 +1,8 @@ +use near_sdk::json_types::Base64VecU8; +use near_sdk::near; + +#[near(serializers = [json])] +pub struct ChainSpecificData { + pub orchard_bundle_bytes: Base64VecU8, + pub expiry_height: u32, +} diff --git a/contracts/satoshi-bridge/tests/data/btc_bridge_v0-5-1.wasm b/contracts/satoshi-bridge/tests/data/btc_bridge_v0-5-1.wasm new file mode 100644 index 00000000..778b8f6e Binary files /dev/null and b/contracts/satoshi-bridge/tests/data/btc_bridge_v0-5-1.wasm differ diff --git a/contracts/satoshi-bridge/tests/data/nbtc_v0-5-1.wasm b/contracts/satoshi-bridge/tests/data/nbtc_v0-5-1.wasm new file mode 100644 index 00000000..4a6669f2 Binary files /dev/null and b/contracts/satoshi-bridge/tests/data/nbtc_v0-5-1.wasm differ diff --git a/contracts/satoshi-bridge/tests/data/nbtc_v0-6-0.wasm b/contracts/satoshi-bridge/tests/data/nbtc_v0-6-0.wasm new file mode 100644 index 00000000..ae91de0e Binary files /dev/null and b/contracts/satoshi-bridge/tests/data/nbtc_v0-6-0.wasm differ diff --git a/contracts/satoshi-bridge/tests/data/zcash_bridge_v0-6-0.wasm b/contracts/satoshi-bridge/tests/data/zcash_bridge_v0-6-0.wasm new file mode 100644 index 00000000..5ff14177 Binary files /dev/null and b/contracts/satoshi-bridge/tests/data/zcash_bridge_v0-6-0.wasm differ diff --git a/contracts/satoshi-bridge/tests/orchard_bundle_cache_100000.txt b/contracts/satoshi-bridge/tests/orchard_bundle_cache_100000.txt new file mode 100644 index 00000000..9935fd33 --- /dev/null +++ b/contracts/satoshi-bridge/tests/orchard_bundle_cache_100000.txt @@ -0,0 +1,2 @@ +utest1fk0tylrp4pde88uxz6sp25h0mv5q3tv99clzuvqzj7kqgmdv4mm4kqgfmx9z2tr0lqgff7l53e6uy3yq8kkrrnhgx07z65ktqdx5s694hytdtfaa7pg3dsgvghpn5rg07xm5snfl6vc +01cb7ee2007fac082558af7200f313956d869f63edefc65093c5798a36fe18988516bdf4fc0eda8481cb79e7312d5b35c0370d172d85921141a64f8738db95711808da3b75e79731f01e38814a59c76980c7ece4c3036cec201f18bc5acdc8df9eede38e633a5c7f919c8386d0bd1d7d42fa7195f611ad3e6500fb4422ff9b013ef5723d51f9ff40f128bb1c60408be789a41c6eba3cebde6bb31846d9b52297bc9a347073383bf2f83d3b0fbc18f0b1ccc8bedc6e528c462a47cf78c1becfb1d54c20d8cadfe80c973ebe21092b9caaf59e6bedde42f1932797f485252bb1c1ff0f370e94d9f41226aade352d52859182fe6d6cb4a364a7e20f4bd11c22e30fa96f82f055881ee9e0efc860d6ea70a1f8c6a3d09486e9de0b1c347ca15f78746cd41af4c85519e579c4f08b4fc124051efa54337788b76a8d5897bbc12f1f095df65d7e09202c73d308f8d0d3457c4308f2123d4026ad7914471dc8f50a04e1e6697f49873553e09edcf2ea9a81261edb9f34ab8f7228d3a996c84113c25f8bee09ef5b7c5770a63b6a1e5e70a5a5cb236876b9f5707756d14b0e1d12d547ef2059f0539b3f2258cd667b08e3b1d48eeb4becdce6d6a30f31afc9944fc72dd25148350e8809d1f8338c11864b9bf45861c1f3a2dfccf6bfc5216b910f89f084a67247586652610423eee5a23b0848ab62b203b3ddb9e6d529b68fa965b14086e78540f91afcdd8d06009d8a5cf21728ae5aeb75b5b44ee14ca3a6233e6302d10fa2c9e3238eab5a1dcd4e5943a012ec24f23fbe312b9bef0185c585b28a88d62a2bfef2b6c4c84c7344e4ede97276ff0ff3955fd87b006e851132a6583708f431d44ae97aa5923f2dd10e28b81e49fdb84280ec930c0303c289ba8a845c806ed4e187b1e45c1da20054fd18c222f2ab36f6761b5d7936732f0f2301f67c66ede8df8c7347e0c492d20c2ea8a1d92bd56b8a93eac96f5e0a08ec6a020ac1f0d84669dd7ee02f97469e88b2afbdca3902287025d4e208329f790b32e7776ff0cc13498a3506a02546e36f66f38b9c28ba6e2ea471e0d1f8b7f665268592101dd6df823c3d3a476d2c2d411e72bec3fa8fc3adb361b7faa4f595c71864c51e65865020ded108b10737048b2c4f92c1a8e08de6789897026079feffffffffffae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82ffd801304d08780c51301bbfe276a3185cf4380fafb9e5bad62d0807e970ab5588b9009fd4605b4cad7fa718afbbdf5bf2658a4a394b23729ef943338d59caa28578592a712ab052790f8a47ea6ca032a153c339e88cbeaf54b0926304ac30050083433a7b83bc8fdc49033c82ea5d3bf80ef9e3cce7d5eca6e31566c4ad48118690d8bb48f26919bcc6656d7044814c12256519be1d57004c0537e6f83d3fa4d977d84c01d6ebabd83fc36a7428158e698c4b6836db59a63a0362957e70db0a85fa13fb677e53f6f9c81ac09ea8efd0eaf3e916116646d2a6984f81787642555239db1a912d9cf3614e3081920a80057641f0ea2f5996baf6bac1169959a82fb9b7eb0fcc9340476ddea6ad6da311a8023984014eb3b6bb06c5760756de62bc48d6830d8516731d3d24eafafd27113edee038640e7fe7c2c3abf535f67c522dc9a87829f79f3ce4552fd84f2d79e85058ea0de939d6e98cadc018472e14dd4c4f80e85caab0af4129e809b7722e313c9fb8cdce25b1b548fe247264b96bb12bb26b41742a8551c0c4127e9811222d4a7cecbd650261afa516a83bc37792577fb59e6b765ca0488099c36ce8f78af4a072bff0c4d374a9f9d743e64af24d8fbfdddf38b76d2d3dec584fef8c3772a2c5e9596178e2f03b1da1f57c3cd31da53851728bd8f34fee2cffadd912880e67e56cc31e65ae9549251a297e7e1d9e015d2fe223d2851d4a0ffc7023f2073c778b9d16271663dde918a44701d63406b006895d6a532f2fdde9fc31aa6a114da32b460543e978e86d64ffe00ae598ac0a3c7d52b34a5fd6d3b934c0b60b32b73e1fc146fd0eca154a12cfd2e5fdf385891112e66a1e6f3eeb96fc824ee04a8c0ec83d102e349a655d1943dd129d470aa56350f7f1ceaba420f0e4e9b8743b67bab4cba0db01e4179927480aa00a6c9d901bc68febea40147583a7d6f5be317290e9e06a3187d7bdd3de302c651873922182d6dc4295c95df598c4d2581beebdddc27f71a75ba90c73b3e078000e30256c6eefb7a2e8e884bc1e8a2b6b3c5c2bfd8bac5823bf1574a121df29223c9746ae07be640bc3b05dac8c8c1793238d61b16bf547bd31b330449da071743afb28783e355ee880d854ca54dd9b93e7d5e8b8bc2f5fbf987e0195ce046b3a20e3f14b1a9f3b0861cf4fefc79eddd1fd6344f1d84c2ffbd8ff83d9206f31d7f51d4d68fb837050eda2af9c266a37d2995426e6754ca24045c9f00d1d3c2f4adeff15c2741723a9749f1e8e6dd89d169c9bc628821820cc0f35bc122e1e3ca4e4191a73a6238130d70c39871e213327cb1ceb0b5a6abe168263963877f23e2aaab147538b85f921646625274a26c8ef48eb6dc96c0c3085604f687bf880334f21ae456275a026ea28a167b6340a7110e34b0b26be14d1f722fdddf3e29aecc7242f85c8f9769450b2b13520275f09932846c17c70873f59df8b26ad5666c0913ed9398201d66d83b86143c06a13cc4673d32eb255d31b50ae973d1f18564f542649d226537159c1af7addb8826bd77ce744bc1eab9b7643e57f29016790a72958cf9e80ac11537029e359a05303fa629b7f2ae8156ad378cee6c5df97e73702847e2df0d23a0d3087d1bc64a9ffdd3d27f7db1d7699f6f2ddf274befa7c749824ad0a35ad9759d19be939b1098ecda61608a980aca50c77563de7f255b71efd502f154b407b0ac36bcd4ea8ed03c356629ecb72f710aa1809b9a6e096d1a896bb4f149736280990c635468d63c40623a64b019c6e0550871922de50f13e1bae2f7a7b323bccc58376e5a6712feca9953d973f3b162a5bba74d2c3525973324432be93e69de14dc2fccdefc414181068d4eca7f9e4b79f84cea00a3c507ba71b549b4d9e5ece7ca04c35ded03f3a574205419e756aabe5e5459cd8ff29ccd4d525a5ddc408711181d74fb60181d7b8fd193dd9abc0f8bd9270302069c8cda1712296fca753841ec1edffe341a2c067e20c9df9a5c309790fb0d463a96bd307f05cc70a98ad124050ac123d951384ab6e601a306422fbb6b2da1b84d1bf0e0b490c0ea310b2da1bf36a247a8a864fd8d3114d41d7dd43e625917c34a2b46a8df3a3621bee6a5d42b2c170ea27baca6bc1a78fa03f00c1bc410eccf241e3aac201817b8ffc93e79dd1bfb94f22e90dff8548ffecdbe41722ab8ec46140673836d2304aebb9d8f5309136e0a51719d3b02b0b14113ae68ba8556237a7888e9d2ce66b21a042c6c468e0723b2f1bde9a1144b15dbf1c5c135135a237a09baf1a5dd3d7e0864e15eca3109f56bb0547648b12c0d31738d13f2dada92cf139e25af74bba536c8b6f2551b35a0ffaa517051d7c38b96d35db4ffdb0fe6be4fabde7c42ae62b8b1588e65b927c8ffb7a21d24358cc3a0e8390eaa15abb96f35c5ff30466a52436557db2f773d5639caea747a204b748c6e7dde8a7a40550ca99a6196112c37171ff4c09a8d2054b9277b7b3884c1cd2ab751cd5e36534368a63b97a36ae4a3e156a664033a00dcecdda702f55ef6e3832ee58253624f87168e9c962ba46bdab392d077d0700ac516f50b3564d582d804df13149cc3cb34b1d0692a1b57d040de3d543357c7185106f2ca62f0f78c8203d672122a42123d833e83a3d358dd1ca0b495f2a67a16cc767c1cbe16887cb0389eb6904bca9d9e7a2f5989d24804ec6ca12fbf779f1580516609db4ca3f480b90c05260d8e518b9bbd9ce054b50c7fb44f57ec34622a2dc69c0c43954a2bccf22258bfd58d76c41256b9122c5be0144d8a7ef77ede2a31ba57f80ff7ecb82f43732d4642d730761bbf181a7d86306ce6d48436a94812277c7fd54ee794f10d8619575d1b8ef15957d2f2e5ba6ef31cacac51d0c39a2a519870422477f3581e6c1568032edd936364d2e6bced8636102784c733dce81738030083197bc485e80fae6ba92ec28858a8a1a53212c0d7452782c9d7c30d1d9225e4f94b1ea5fec4388d6ee85577a746035b49e65fba33a6c75a31e0a7552ade8ec4253e9124d8af9eede4b7c09947e61acda0d04d319a85bbd5e5cdc316173fabcbe9bedf7e1433016d17bc09135061c30bea4a91fa6acf9a71cb9fb9bc17de7fc232905776657896ad6b1a1b91e2078fdd308bc2e22f750fe4a6e880b605d185c4172442c83868974495e6d6e62aa9b1a72cf6992f008c9c485c971678324232199daa2f9e17919e76a3142141c2984c7ea7fab0bd38628745f3c9e47e06ce2ab7ccba895b9b3205ca372caddac6234d609b71a8216f65dde953c13bda1c649d5b1ec1709e66bd5e94b0c44e31ad1f9d1c1fe3629bc2ab874a25990bd322b533475630b1e05dc30a2fe317a53472506bdfe7a126d6b63c42c4f7537f540081521bc40136b1484e0eccf505e7ada8882e58ddac12cc39e5d007aa3e6f061e2a59b9aac90c3a2d8ae99131d0d182a86bd5fa51624778f83891263077d46114fc39b9702303f8dde8cc23fd737273a072f0e9bf36f255c0305894822b32fc06159686c5c4f00c1c9c2bb3cda206cb3e7a683bae024fbfc51bc820f5e5010f10c6275a412afa08bfe79cec9a200b775a43866eaead410c9f456205a99b835200167c59272e1a387804a1404bc409c915ae5c464c2346b2c73f80a32cbb670321dabc7abd14080600c72f29920c34bd5cd0e610f5f9933b62155c375c85bbd8045c1fecab1d1686bfd4f06e62def62c536b5d3f5da2f5e3cbb71e08b58e69c912725cdebdcbf929b966218e6ce03d03910e662ce18f2439aef96a09e3f6097300be352dd32981d615872a9d76155118927f229a1de42394b4e744af698ee5e8043d21ad051f0aaecba4e1d6138136ffb8adb9e343bef0e2abf3ed4164727e8e392a861e0486968484e38e26e5b204e7a53b462fb4e1cd6574c0bb812da9f90737922b26a5730cc20bd395481c7c7e6d689edbfe3fd2d5ed3f51335b06e0697d380dd8c0d9abfda66a3aca3a06cf5fa1fccebc8399e0b3ed4dcbd775b74a18e321547042212a44070c4b3ecc7918e7ce2cb9b4f40512ff8469b84d0f4d12d7d70ffae68b054d7ec0a49337281e857431ef1863d7bfc4544634fe88f282c898d00c78b586a7eac50d61f294c97be26ca5452d97505478553266fa167c1ef67d6d2f5795ce5826331f8339fcbd359dd3e5a0c0224030f2e730fa7ae7db66dfab0b20d498975a815a02f828837d229925d9be1883ad45a471706b3b4009b888237d2045f129a1bf188ce5ddc1328674d8ca81d8b09c8be1b99f82b3a1f010a1a7ad0abeac0ffc1617534acdd3ac66a7e816d3b4e651cba0777be2bfede4a178aef934ae6922dc1becfb0f1e9b9dc41b39f0d1093c793f10e275863102835eed5a300854b6e514eb8306371153b1984984d39fd0683d06642abf94ae551878d644032a6050ca80c683b36e7e7c74ea92048d2a14729bef775b7f8d5d6e3e6de1773c1e59296abfaaa50e031a2f7b919042e24267e62d7028cccd740365e487782c6d3856493fa19ac4a120f4c380fe1b8bae11dfdf8c8219762a10080034b945b08f2ebc872d106e005f35eaee925250b67c5f9e97a96808881d009e3d851aa25e6425db71305565cad35d9a549e0b0606c12ed778fb476142266b02365e10a50acc1a9d5c725ef2053175159ea4e20f7b1322f6f5b16e1abd58ed1f4efe5bc70dde012d86c92d78d90df176a7db168229e5c569bef0854a020345e50c63c46869a63651b13abb1c503e8b4c81aef11349de22af3792c044cb90fb0b0a39b2a556602293d694523af6458ee54050f1e7aa748d9cff866ecac7ecf5f409bdcd101a8f231a0797da5cea153b47df9ded41c1f8f796aa1b51d5a85618b7a54e89ee4fb82282b9c58f96b99010d7ecbce62ff1daf6bf16ed0cc2896564cb39f3346a2a3003c51ea797315ffb8aab7e6283cd34d7f10fbde6a0bd55ad3010ef342ef83c56061e7149db26f98a421e1a5cd93118f1305dadbe9354c6edb05d6ac66e166c3823fe3a088b6109d347f918f8ae18586cfe780c9ab8a10d8ee89354ba3c5ee7b22693738f910562f4fb6bb3e5caf57ed69845f5d6b55dafaea6311a67a9d01cc01cbe995ac8fd81eacac89734eb33de33eadef52722f26ba9524baed21799520528da336d7484fd5a7464864df0f74b977f57f9601f0a67f3ff487d6a9fe04e3a2641d3c96c0ef701c162dfe6692a75a5b08a2398769436adb3f2cf9b3d05175b04b185fb8155fda0f91ad0883d252ce56755b61ab066aec2437b5c2421e03704101af18e93a42e5920d0c7bc6769eb32340832caa95ad40f7dca6baef36900672d5d0471f838970f78f37a4d0b60d6e639416d4f0723619c743b270f86c9fc7202bb30970465c0a335f5325577f2670e7d14ba81215795195fe1865df95f01331c7938e9cdee7d1aaf6c3f9dc06b20ea6b2fe4f3c3193df9bea37ab98cee612d200439295bf7b9ee77c2ab3d1a1f1d30be6426e382407af7f1988d7ed5d0ea13137a5a8e77ed936612fb5afc08dde76307b1e69a3088a3f2d6bfd2a935a4dd99218fffe04c80a4ce553f3df8f51d60f6af54ff1bb818464a114d3c0ba345735d252ed5371a872fe97c22ff83a3252642849cd13fa0b0e2acad4996a74894f4e52658e8a1b281fd9640c06102a10e461155de03d2e212d3519659f6af1bfaff07304323123c856ca3f93326bc7b3893afe464f926676806ee5dbeff93fc76046e0f7100424199d7a3a07a81b824396ec35a2106a0233a7f70c3e39e306a6190f01d508d99762537411baa8101b9515983e4160d91bf811ae762fe90f9e64b93223e72b17a02c2b5d965919f91bfdbf80bfca3c5b8ae5c75ccbac566010c9fb017283c0b3880161234a2ce9955160431c9ba7dd2a3a1df9c99bab0806c6dcea03b04b7120b117b874369e8b5d0a27c05debde73bd965968307beec071561098b853552d81b7b9ce077d5899ca9970f61c7f5a887f5e950de3c3873495c26dead95a7bce3b34d74d4088bdc4d8adb3418916a66160149f97e2a3c2c47006ffa234810826689b7ad3d8497983b09399e7f4e4ea4f539d3b5b6d160dd09dc2d7c53e1906eee199431a616c462f1feed9f1ce3776af2d698d599ca3b80a3bdffc681e2a950861cf2b4ef4337fa11b2b4c77101e642f0a790a1e5e305f69ae9449a0fbbae0d9aea0c81f898ae8baf2af316e96cd70bf7533369058ae32e8cc77ea4c01fab19c0d09d0832fe6452350e83d7ad52602de62c53aa6fd042ca7cfb9fc119a43e8a1c6195162566ca0b25259ddc7f36d333af64eebfe38bcd8070ffcb84e571ba50ad6d08d543393de6960ab304b5e3019b179c841e9f226f1167618d938ecd9436fc6c9447bb2e29d236e26055587ab2cd3f758a0bd4eff165a29594332e272863e56d418fcae5040c519a3b02652fcd88bc057656be5044ff591c391750a4b0b5f9e61e7f7cc05df6a3991a59defc0a02565c470312f9d732c6c5f302c0dbbe612b8db7f5852309fd58ee4ff41570e5d03a96a8e2d4a55fae66dc3e87db3596192a4383a763b2d15b0e3bd905c623537dca44448509b750f01cbe8fe35ea31c7e51bbdff0b7a96af73ccc271e8fbe2915688fb2973aaaf416a7ace38f90563e3869713df4bee4fa1060fdf573254feeed51391ca1b2207ff245d316a6d473932598a31a2e07154f25f245b5161b65b1c0c77f70ce452a5e4b7b48011e82f8a641d048e5a5fc5ca380998b9f0159889b6ae3a23181113beddf7c7d36b5f90e09fb46512c57bae49759fb694ca91eb24940c048b1317606eac465abef986c4f82a134d4250478cdb1ae1a33a12e6a6a87b12811645f55061b126950407046ddb90927a9ffec8e8233f5ec81d56862e6760e11c5fa8acb2e6eb2cb76d8cfb001b95be2911480b60169fe38d32e5dafeb835db79604610b8c3e4cdb7f673c5015ae60f665270cbea1664b183d1cb3c3ff9a4bb32c657bdfaba015e95b98f5d23d0308e26909992c89cd3a1857ee88f1ab25aa8db7d74a897c763113616fdf6fcd0fc56eec213132a0c9d565de8ede5c87d43f4e655c4b2e4e08743eaaa569c39f2a53b2ba489d8e2a72f7a218d33e747455e64db6bc18a30753fc467b642fad95144c0b98a614dc59955f5b9c9a38a64fe9ea33f27d6d6618cc302b2f38b2368b387fac6b0095b8680a72b24aa94c7f341da32a2d998deb484c624e9d1661a9c222 \ No newline at end of file diff --git a/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000.txt b/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000.txt new file mode 100644 index 00000000..7b130cb0 --- /dev/null +++ b/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000.txt @@ -0,0 +1,2 @@ +utest1fk0tylrp4pde88uxz6sp25h0mv5q3tv99clzuvqzj7kqgmdv4mm4kqgfmx9z2tr0lqgff7l53e6uy3yq8kkrrnhgx07z65ktqdx5s694hytdtfaa7pg3dsgvghpn5rg07xm5snfl6vc +0151ae1c685ec3e5b7debc8ce7ce0c36af7b1d8b60d4b2a436a9970cf43ebb58978fb482348d4cd9b806bf6000373a33afce26a1f0c568000b47d1f9aa40e3a401c53f6ee2f2e3fab7a067d0dd683b7967292238823656eb5b108308be3a9d36993305ce41371a0031fbe1c49ab8b3217463b434f746a891d78a53330ed2d0ed305e9c241d063a765554a4fd97a60bd9b40c19830730262b42658ac79e4c9cb83f4743e93e0ebba99c3d3216b0bf6a9258f2c36c887f318f8d45f645df58f006d43567b84bc3fdfa7566f179e6d99aa53dbb70799f73aa41c1b387ed8470b31cb7251dd62c3b260d662a71623145cffe0f41472c8a14fb63ac3c6dc1c4b077504eace87e44b7ad82e42543ea958200902287dfc92549b4e478dec481d9ce864174a4990270beff7baa5fb281fbc248d2027d0429ab4947e528f865d4d672732c041be08157f6e9572b1f5b95ba5511f956511e624051fa09997d1b128b51304bc31c54890a06a5d8effdd77fe5da9641546029470a0c63be1e7121f06d560e56aab8030d455437a5dfdb7e9b9a613cadd2300b67aa733017495c526b2502fb5841796dd6f93b0c324e9d3be477a867a9006255a44f55b0e174c883c06ac53a7f00552701bd3c427dcd63581d0838ad554c28517e08e953807a1ffb24d792200950c012742698b6ba6c5434e855bfdcb1512ec5b0460c5de10e075412c0999c63f7dbf0e064460307f51cdca7624fc46c4d07f7ce74e99ca0eef1de0c4b48dada5d776adcc6e456e4b509e0ec12b14900c398ec3a7b4c9d1706518e891eb11be209a575e4366f1a9afc77069291aef682fe711679ac361f08cfbdd1998bbd9375f135a0a9e3f2d4e9a64e02f008c5b28eb39e08e25649b86da9584127e80946cb5a0141722e21cd62d6abd3e59aad16d4392d02c55c9e116ed5c16e0702e31885bea6e56990455413ce96f4e7141f3e2610538d2b4a3ac88cbcabc1229b5b8c5af2f1b903f0ae9516348fee334227c3ed25d388a42ad1975e62a48c8b1f1322700598d6e3ac77e16cced54cc82b0f9b19f9600ac20e7e662f8544dcf4c4b51b221bd3c19ee96f1dbce40ca99ac8dadce2c1c623b3b42948dd80a5c2a5bbb97c6287bd3a5d77f0ce3dcf8af38d8c0df612e2db29c80d02f067fdffffffffffae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82ffd80138680a2327386836af3a2def37be778f8d3e371f5c05d5d74286573c504a0350c382de3e97d510e624e697d99d173e91f9b54fa92a7eb8a3f816b94efd4061318135229bfa4ecb8331c12ed3c09111cd82b51767031b273a9f7346c656e2da60e29c7f5e431b52136fbbe0c6a2ef6bd3ba1abb3a196ec77c9d5152337a3e18e1fe3dbecc81d0583bc842f06d5f0f5acf5b317422d63b1270702c59b7c372362829029afd69aa63c785cf043129fba0492dd619ef8fc8c9ba324c66c0997970f9d3689d2e645b67146d143409a08b395f45f37d18ec14550b7e2c6cd14b2f269bf4ab8e5f00a114542560d15037da1bbc9a2652ba01e539b559806d9cb0a6af9293cd2ba2567cd6d4716132b68815e014ba2def0959f5ce299740371bee5d942a759d1686170695bb8d59521e5c6f44ac5395285ee1b7267a492856cb486131c30bb18bc785027e42fd20d7a75b96e37502ba88410d98a862fb7795ad6493a8e2f57b1bac00f37486324e55ba32ca8d03b91427c7cd62a8ddb78fa017582d0b0b44320dc10d790cdf66a00efd565cafdea3e7dff13782f6db76486676907469c30f786c5d2e46ad6fb7dbb84fa671d8aa7044ba71975a44ca6e7d8c3c2ebc9ce8f602f755cf34dc6ba594f37ef2c23554b7a8ae96cbd9f8367cadd44a0c911568277eaf11991dfe77ee283851ec1770311ba4c1b15c824f200f571fe852a013f9295f61dced4928afb737df837f2a54bc91431ac65a87997f6d396af54c3c30ea7d49e9d3701c6c1acb08e16e09d1c8754fc30c8627f326786571ece73abcda513791c8631b4d078a19dce9075fb848183458509ea7556af72ac39c7d55c97432c714e4be936aef7f8e5da3068a1ea847fcbc5f7b1231ced16ebe7d8035ee2549c3dd402c6ffb85c5e22959a7a57e7a753829d18d2085db8f5afa99b88823ae7057361735df67704477eaabb3cec2ecc5e50652b4f7d3c27950850cd008d03d9bdf0f730866ff4303228f2b154a0eae84b53a135e5d9bf3391b2e07bdcd625853078fe5d0c465b159b9ba88b177ce8c55348e22b1dec76ff550a51ae8f97f81cb8bfcabd05c99bf61a2c1cb0c9c3b6ead0bb79443a49bfaf366b36e8334f41173237a4f90d075e58c4a51f6d1e8ea27930bb44dbc1a3a28ff612516679a65ca23739d25bde55758a0159b9cb85defe14bf2fa32d8301a87db20790de418b003a0df65983f05842e4916f89f709d0abbc737f3fabb04b1238c3156a25d6a1d91caa61183b7e5d224a0048d17ec9e6ec18a3610f094e2085d1e5a381edcd80572d9bd6f4b042ee5e33cc1e176fe08105a95c631c08b4aad2e93192c9f668ce10c09b0c8e97fd4017d9d20f056ea0ceaa593ff009e441e725c7f25afc29c2f0f009826f5d936817db9af7b4274eeb8d71d3ddf3132b27023c2b07c490d55bd8647d3d32eb0c22fa5f9a3556ef6ef7e5876f59f8eb348d8f189206ac12207232e64d122f976815fafcb8df4c6fa9cf31211ade50a101040a1487c943e3ec3f718eec1006e4b475db40cca288808978af265ed943d57c97275ad7056d114a45ccdf9f2d6f7e0d4665c289b1b39405cb2960937357c396fd29a26eb6bef700eb5d76c00b4cb25b6c34a56fa162a9af57f029aa8dfc4991c392721cfbf667d0006e6a0b2ef54c93c427ca8f8e964339048656b62630ec3323f94e04b28bc66d62c432060e9a0d6f963b907a91d7d3c8dcaa2bf2291c11c28eb4d65606d4cc99a38f07e91b0ba9be840b1bee009aa79defef605df9f17fe26f64dfb675cae3290655cbe03abf98acd44a134baa7ae661d14247ef841a097847e86306a6f1a1086cc2d23c21d1fbce93a84cfc6734e3fffdb9cf165ae464f48f7fb66aa6922a18566c6d860195be705dca0b95f38933fd196d941ba6f2442dfb55cb04dee8ad7be375d17324520072193175e874e3b040bbc561853b3308d54a5954d49d2ccf60fae719bb3c8ad353e3665ce677854f61daabb31a9c418463b75d2a596902d386b6b9252009591d46d6ba15a1939c98b30276e471feeb07895d03618448f33db4f0d463220eb83bb93f1de218a243ad20b86d3475db0f561b73e848ba02f5ac2acacfa7e80f3183670a32e1898dc5d2d138f88f01d7cb69cd73ad3faa7a46ebc06cc78a022fa21672198d610e0289bec6dc1d74cb8a4dcfc7f35b9c5da1ae9f1f25f8dc9007ab22b87948ce2500096d30c4cba9d70bd512efa58148b134ce687b6c809d452276ae59d08704b2c980661542f682d991b8d9f4d96ec2b72398c74cb32868003cf1e368963d4359caa1b104fda554b01a39db3eeb64a9e65dd5fa4c6b7cbd252729da31172a0e14164984046212c69c7b932b50eb30a577b99fb7c4d5fc53250506f9259a38e7722042f91a630ca86fe3f32f1dc07ca5c3a2d29f2d47295df83578752aab318fe9c9d890cd5f629f4248c33f513e6de3b3f88e9786968c9b54210d470756fd2090b8d1115514eea468d47959f4547b7805a0b0bc2084f66f440100c2e488bee7943fc8ae48847614c213515742b56728d37bc1c931f95e047c2084a3f86d572ef33b463f8a0c84c07bad7f023c8a7e7ddea3933cba0a9567ec2914b601876e36391a66e213639eca6409856533cfe1a1f19b14ec07a587195726dbcd8b0dfccb7ffe03346cababd53b55c004d3b92fa9d60554a798b7eacdbf14e601cd388db98fa621647df9e67b69c171776ded59abbef7b5038756591e1a2fa204cfb6c5729291633808d84c20f492b5178ad7514fdf9dabe2eccea2db2e09bab4f2fd92b35dc739b53db40a7fda605fce5323bf51ea4198e347c961e81e136c2de97808a2ab6ca9978be74d1969a71e883f3b40f448ea24dffcddec54612d16a7cd3588e3ce974dda3b9bb893fab863aa87b4269600ccebca1e25fd8c2c0b6eaf5d50a06ec991b02eb7a810da95c6d6da6950155f9afb7f14160e75942b364155103bde38b7fddcb50506c800755ff9652a337c018652c1bf82674b3825330fd7a25b3d8262063d6eb56050f627ede3574d631dd7d3b0b3093a6f059e8d0594e6445827effff8ec816b69ebacc302931a615d6deb57e20919bca2843be03c53eb32ad4d97c6d35e797144dbe364421901d7a4161a3688ca6270d135c4200e352976891660ad6bf84de0fddab160e401bd783d54bb094ddf1a6fa595856f2b6f1fb7e4838d9a26622e730e45262803d1c770620373759ac982c7d7631cd834458968f2353aa270d488871df83481514a764e97bbc6dae90580f1405c51941dd4d26669434cb50fe7ba857a4b09db1817036e0364ba0b76718534369784c923a8d56195879f0574ddffa436ec049776b69e5a56050e6821666292f5f621e639e7670fea20c28179f27310ee9c7a5b6bad3cd537b90a73a7ccfa4871079aa00ca3f14072466fa0dbbee703e8e4c6573e47e5a21e41cf8a1f056d8f9fb02bb321a9463375fea6618cd4d875d5b3c77c151a57de46ed79a7315ed73e4a5263f82d8efdd2138bd117415cc160f26c6d5c105163b7d2b7b6d96045063c0c19450f052f0c6771786902d09556046b94bf6ce7439ec03009f88005b38abdf3df86a21b38dc985bd45925c571ec227181a61f477e549950a3913b6dd9b152812073b31003ee6a8034415e79034102bcfac39abb0a705a55d6e2bb8f681bd65fba3fc22b16fd1faeef1a1b3c8f65e5027a70ba67a07cc37b7fd509e99aa9c08ca6a5cc300b176b997f266dc851624f459b3fe6d44a086ac27bf770a815a2739ee0add81dd2d2094acd06d066c134fc57cda9fccab73de112b1e3e317c26688020ac14d3386236bbbc1791fc1b7e3e9d5e617c0408081e87b901c6ff65b9c57dc8053220f1c50ae36a1d4fc86f962eebd3c1646fb0fdb636b49a947345b2dd317b5d6840ab7dc560185f93553e2e8e5bf6288a6ff8d209ef5724fb28ed9ee6f567333522a7f9fa643cf5b6ef17c2bb12b6b9e1b73aac6df846ab9b2738ab0b854bf41a43fa9609f9d11ac694d340354a62200875bbbad9b39ece791ff23cd841dc71d191d9f0b5ed630b220c63b45d758dee86f2fb69c880b58a38cfdc758e97835798639a1f509abf47b1cf6722b0d5cf35cc12ac5217a7140598dfdf644b14a1c8e2122d2fb144cba4f3757c170d84904e4f9e9f119bd4c7e5e8156de11111f43937d0f43d3ce8ac13b4f3136406203abec57db182d24ff92a1da21137f8d2cf1ad283f8f2df7b63ea45ec26ed6926511c453e49b2373a8b3f0736caa71ecc301440a395fd2cb7d6ed03261d4460234260f9dbefdde4e34806d6b2e3e85ed531897363929a21686fc2ac446d5014f6c715b37a8f7d010e2ca49065542e274903335681171f1e7a8746b99f92506fa03eb76dc2205ad1fa4931b8a8b509f30fe61c1c03eb39807a70a25530cfda7a56b0054144502e7153e31668259f95fa9e14d93db202f420755195c707b4325708a66807c16aa90a103f28c5d887d85b90ec444283d65f1674ed8742e73838691dfcca75a123ff8f435c76597f544dc944e5396b6358cff04b9a1bd1db1267e6eb89b73a83d7eb68fbd55c737c9c5e49164d2df08051ec1134a5411242dd772c13002466936bfce9baa791ed971dd48d357e7b3012b4451360492072d7616d86d7d14e995c2ec06b9e0f24fabba74f7b83252b0bc1f89cd427d226a6948cb983e4b0425369bf54a8915c32fabacaf4543f45211421b502ac29a4820a15adafce36fa36d035f20d386104ef5e2e5ca54a8ca2b8f712565f945416c47c78d20724fb26e48a2477c0c574af06bebe02c845b2453d5d8129c5d263ed44c40e01cacccc03b3d1d5b0ba0298d5e20ff0fcaaed8b04704721d9ba599208bf1dfd63d7208379b10e546ae02f35417af54856abb25ceddb56e34a69d2f0eb633f059ed4bffb807d27c86d03ca01694c3392091320f25ca6d95271f5e1df7ac2133520c0b2f30b410a1080033e9640f5b9aa1f39ecaff2c93732c01477a6109eda0ee1f12cbb7f36e5962a5ec4d7b0aabb7088684a9d23b8381378f25c70088eb0243401f7aff670cae1b8207278c9f11fce63c07fc938202c91bb89c72e5ea94006f9fa1725c454e50cc18a2d107933b859f62605a776accab3e76649dc345fdfb7e2feb8a49b3872316c9babcf172ddd013f51676b2a667d92a02562ff95440442ea4923b626842a3a7ba80d87317d54692867345d40c529a3fda43ac9a8dcc751821e1f440f83945273f86f390328cdcf798231feb4f1424248e2093391e5eded325ccb6328735cf7ef89e67d658e80407eb4f352f7cb16b0728e55dbbed0595b9f9a7df2ad4a0066deac166f26f1e9a8c0e1c32fae6b7ef19e94915afd3e52793cfa4346289324d08aceb09eabe3b1dae485acc8b5c17ff310ddb72e58aeb56c5ee3786c2d4ec8c06395005296e377550d14b6ed6ab7a122b03453f31ef74c8c7b0ace4d05013b0b861f2d2e0b52ba7f2e99fb40953f2b91b2eb118091fff88f0b458dfabf7497126a610ceef08d26b6c94d9d3c32f115d1e8420599199cd2a811e80ebd1d3ca088925a56f3bdd61a9690ecb49912bacce1369e78c9a3cfb527afc2b92d1e0a8e5df7aa9dd18d61513d79f77f942b3c2d119857c8484b64a4a9fff76220c05217bd5e485227084d8e1162dd340d7efba3d04fe7491885b92e8858d6c95b5e05843176b0cb688c51734cabab9655af9da71203c6402f735d323019588da203537f0efac5a32073ec160188669f6b465c75d1adbc75878182243599dd2635ebe28be8dbffc56e65511aa7e415fbb99ec05a51153ef720f545a54cd4584295a9c489538ad0214f31790ce4e906c3eb6d3bb102085a1a4fbf4f94d26b4cd6f491017ea96571db3ad8d985775ba5eac4c93b5830111da14afeeeea41bc646236dcbd50a430a0086b832baf2455d131bbda7202407498c5a20768384a9e0f6b62821b59a4a679c32c52805e724930f0d5ff9b0be1b31498f82814f6394d4d3f5f25cb34f1145ed3be13dbd44130c94ca2f8a0bc92fca1df5f62a381d66164a8bba9b3110e5d7c0c1158b14beb9ae012beed2719a92c39243d847daecb2e20ed379bd1ce775b6e2b7ee64708087431787d3ef1be28eacb776fd8fceb0f854c1d8a5a54d485eaa6ce66fc971d9773167fa29695ad422148c281260f78882985e9f8dc1be965d320963c0f49649ae25d20eb97d09b59e3a2f3937349e5253c41aad62f4dfa69241a0c3be5977f308613a6d1df70fe3a42dd6e225af7e9dab4988fc8b44fe89eeb2aae7edf6cb393e0f8936cf060f5b9a2a3e2d45dd7c8137d85d290babc585add1f9586c08e0cb20c4e2b0e022627101449344ff8c325d28821f748ae5053c3848becb66e4617b30fd6909c3fff5e6a90a8eae297974f4977b9ca67250bc9e7a4378308049e3d6eb76383efcc1670417bfae3fa3b4fd983a5cbaba7d1ed0ebaec8447c4fd11b81d86b092e4a9ad0300085478f5beec99214fa7cc5006fb35f3d60f567de3ca82da637a0fcb39b3acc15bb28c0239a8da853a818315b3f51ca7ba30e142b2e191f6778506e4a04fbe98cc66f5d52469fd0bc4fe0d692095697617c0fcb7c7fdbe79e598460db3a38320763b8d327bbceb5c36bd783322ef036dd02e391e76545c5d9bd9733d5650edf10c7e2cb0aa1fd42c4d31e7dd00e7807c8b665fe8f8aebea8075a142510543dc8a008c753c82cca82619514f0f610331ce873120e594b290d5a4662c37841b828a5cfb8bc79eeaebf78f05995e714237277a5f482fd97f123ed3ec641823662883964ebce72bb98934e51bc4e8a43101c4d7885c78d44e4520d90bc02a519738aa573f67bbde7bf2402d86c9e9ca42cff4b5542e9010c9a358195cd0015add8d06895d76da0b3995e133e1c510239b5ca66165d6ca243ef991fd096f191dd32713ba778d1dcbc0988108b259860d9e7a639cbfb0e9d5b6ef66da6841bc61face12c83734b21af84be758a8b5f6d2012f9377ced5ca0f5c755c5bc1533d459ec53370e8b4e19237e7e2aa0e3565cd158332ca71f62127ed0ddcf59702ba06fddeb817c843c81e5124c514e47235ce85f3c51bf56cf68937c9c90c7d92f895b8fc3c3a4ddc893868f11c7a4fe978af5683f2971aced736b2c4994b031799df3ec80a1553640746fd19aaf3ad2004d8d18b3d86bd29cb9ff709d5f5acb7d3cc9ec103 \ No newline at end of file diff --git a/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000_0101010101010101010101010101010101010101010101010101010101010101.txt b/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000_0101010101010101010101010101010101010101010101010101010101010101.txt new file mode 100644 index 00000000..a97f73f4 --- /dev/null +++ b/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000_0101010101010101010101010101010101010101010101010101010101010101.txt @@ -0,0 +1,2 @@ +utest1uj9838ua605gzqjpufndhdqr56c2x4encs56zcatj692ttcm8yedqkr3elyglnued397nm3wyjdvcydpwsch740nstsvmvfcgtuh4g4kyygf3tf9y4gp9ku3hj4qcf3mtkf4zq7rzn2 +015cc6a0f17b38529be9c6e04baa8e72c2867355b2d6c15e90afd02f5f53398e2b853eb6cc036e800b420878d1f88a2396a0a940a3da008e96240b33e85c40e71b280450876c951eb52e84918914733e11c104ed4a64e972c2a524655af38df62d022babfd430470daa166896bd2538540a28339a4a9c0f0eb3dc7ed5404712b178a81a4851fd23e0fcfa122ad5e888474058e17aaa55c77d5963c29c22e79aa1159bbb32d6eb14237264ad1547ddc8c1c643df22c853beae9ff2214fbb5e8d2174becb8897809cce74e344c7b7fc2383a0b304d6ca6a5fadd19a0abefc43295c6b5a5b9763fdf1a6b7c205701d7dc2b01fcd6b93e086776e167d43ec058829b3d86228a47102ca5a2f2c19bf23c05a050bb789b57f43a8c683bd46f3e8403aedff1876a16d60201de5ffb3152d09847f037f042ff053ca0698764677a64e76fde17b97957250ba099e3ad2e9525bd4450aa36d3b60a5a5a1396ef80e4be25dadb619d8f4401c44305b801b1f389e10a665e9bed10f4490379cbf952046b6101d57f71894783a17a45810154ce1a139384c9b324e9883380df2bcefc55efa839033597fadb67258b12c51ba5a75b03405884cfdc826bba9fc5fa5fe2c6d003961e4cf666953ecab1ab8c128d60f194b65e89956c03a7c3af6141b9d44fd04e8ed05eedc00f42f5ec768576d573a99bd7d24ea77fa90078f09d7bb543794612900e333f5d51f96966619c1129127969de204df438c25f5e6d39ca71a346dcee7d05a1c1ca1df953d30c82921dbfd1ddde80705e40e061d946351de5b40449108bf586fe079016e7e39d8a47b76c632cb3b96600e5971815e0867b1223a94fc6488e41b8071564b9538e5a804c16095de2b9183aed9bc36dca6a2ab318508dcc0e6ee72809b5c4438e90656d94696f5e01d8373d14c9b5fdb1a90d93186ea2047c46a5be69b4912490edfb92240615a3aef2042643009137b55a6d796a40b67f1ec1b9e86aed2e7afb4b28557d3e05810c0f46130a0c0c17570ae91589a2047e05fcfcdddd65c03e94a7a8cd11df1c162db7dd8b657b7bfb696e9336ee25bbbdb2c021871188b37b55abe09a57299b02644382664f966bcbedb936c2e4d611e2dd86db175b0b3e81e4b8863b45068c54d292cf59b2da02f067fdffffffffffae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82ffd8013b1dd5ab75d2980d86d729775fb5d4af9de3d7daff2a186219a1b0fb916abfc2696bb6da7336631ddf0d30ecbe5400962874a48c0d10908ffeec0a0addf775622bb68d8c741624edebf1fc51fdfc2bc3285eb0d838fec7ae73d52b7a57b20062b5ed92cd9027f3a368b930ba2008a99c5f4f76d6fcc2d7b66845b5e9336e93f85d305e83e3d620a6df0409e2c377743e5e2e899129427be9dd2721a5ef0164c1ab0096a278f88b17d4fd8482f298740dc64ee9d1f72fe79348c912a1efffdd903f77088fa37c515be049df12e967435a59ccd485490ae2271776128640f43738c10b9b20840ec044f81bfc003b8a7d1248838b26b3903ae7539c88ed4c507ff10c2f341d99f2b4daf46ee72882648f03c09578c99003ec40e5c56d452007c0528d4332a6b614b90e389b164e160a3d1ab6fe6e8670e6107106914cd3af56b021bfc50c6d3211fd8dcc717743a2f6a1616507f3833a8d6a4f388e490e9f4bc171e0f8aec570cebb4e312a3ace0dabe20966b35b484557619ffb69b9f706b26838e7c32ddd4dc2d5cb51a6288ce3ef44c0aee56a9125d924102abda45b16a1c5335a4655c7f089d0018a0814bacc489373a6473113600ed9ea1910cac21cf521d10437f151c867593e7016785c7b8ef46a5a3676ac529d30f12f82d64f35176cb12b776b092f4146422795e66ed088414398e312cfddb2ff01e7658dfef1f6b07077988872952ff583cc9fea4a5179099b496729bd40c0aa2323912e133ff87da8237391c2a22825c389f41b6adb0aeee99c7191080c348106eba3b08f409443c23a3b4a3beb4b216368756cb22cbf64b55eea30472770f6ed545d03e72e541da338ecee5f8686a93cb42f185eae2730827d50076b8caba6bdc24b515614ad2eb2a1b407834772365df4f0f71307fa0bb4ef1f2ff3c20a58418cba23b5fe049dc1402e80a984f77844729cd39ae199d3f1cbb353a9a7dcdf91c8fdc7879b47e9086f7e208a4a2ac632c7a50d599ea34f5b4b723f997020c8133fa102b6efed66487f74bc347465ba11a36c37db6ebf7a276520f2c74b092e6c923fdc8d6fb285611184fb60a8c7d42ae4656afe80b72e4586a3b092ea0437e62ae21627d7a18cb80ddbc67c41e3da06cd252508094e0b23e614262e1faeb5b60d21539f2b48b7807869bd852998c06fd830e2e29f9e42e520d46f7c238d32c89e2a9f46265b4ac17e89cedd166454a4d583f220d4bdaafb4a009b76616dcb6d8cc8925fda6bec40344addb4427d9618f33a1a65a2b46248e37ae7cb3e138a1d80247442b75eb1910a55fcbec52663fb94804cd2c90de50b22697cf93178aa5f6d3b094b7748c50239a39c23050edf8e891d7e1dd3bcb75f888a497b59eb7d7406ceacf8c92414ca00d99809a9b45ecf2f3201883e9df9ba7eff517b87d03261c29a322ae18b7f83f87e7a89e4535e7013d6379eebf832b377b88fd74f1178807113c7e3911e2c3259ba72901c841faedf7ea994e3115a46cddbb11dcc76e2dc09b59daf291f6d92c3b7665c3c12810cb8cb2f238e2f9ed5ba9d44bea7478a84ce00cb9cc4b0fb51650525c16329449833ec974d951b336ebcd56834e2bbb441b001c54f98a37e623901a516933b03a495013e74318ebb850656371991b485c22ad7a848ad225a706f7e8fb0f688e64ad939b3d2659f01f3ce2579c04ddecf6a2a3d1fa07ad2c7d19f4b52ef9eb4268eee8726f964f19f8ffc1b5e3557faf2d8d5be686170591b9143ae6b03d9d7414cfea2312cdf88189418d9ac3ee9cc3d0cc8edd529cd4384b2d6066450e3001465beecf2909f0d81d15701922ae0ba303891bb6650f998bb51da580aa5458bd4e170f679529de67305c912e6d83c0514d60b1985675834706292fe57b44c0f68d5ea317622b66f81a2a27e4012c0392326f536afc5a70e2d114750b0f4d32eacf0b6c9224bb59e960ab17e63b73c12b3ada78b01c15e9fd1a1d1ab1f2ab6172e97e8e58a341f96686024dc722a8fc864ab3ba4bcaa22143700d34a82ffc2cd20b68626cb15bdbd9c2a8f101a0788c73e1d59120606bd43c6025f81716876e5b722316f719276f4864fba4184c1eedb4a57a1525b9b67325fe335833f1f6961fcedc71e1712345ba3cc925a0e9738bfbd5bd836e8f12153b150c69e67e9440490adc640ae94c9d4a70e3dbc3e32bd3528a3e71b20b302dbe2017898fd52cdc827fb87422a614dadf96cd27be242ad5ff99fb2f9421d8c72e921639ca5fd8bdc3083854db986a4b2d598345f7f93ce026e54229fc141ce0d2301d43757831ad71beec4b80be148030760f8bda064e22218d64b1e78f21c68f361987580f7d9b3f2115ddd1654198538682efc3b96c4936a34413206f03bd4c4403b2408c75dfef5301a2c74a1a24fedfbb51d9629f1d2a93c0c3f06a4ee95778137157838d3478850817c2b25cb9576408164f3fda6f44866b0010e5393cf2920142f931ee6734e1eb254378eb6270c0001c3d763e98aa8fe4aa64438517a835319afbc3ffb975891711dd27676c309b8bd6d8785909a8422c0325982a3ae100075a5b97aa1ac2bb3e9b8777ba4292b4d35758fcb68acb221a387a7c1735729f08f3e8e99dbc105c5b3954a5ab09fc9d9f046f039148913c1df9d971ace3ef17261aa3eef9cdcadcd22bf3caa3cfb04f54e2237407b694fc263bc3e932b2221e1fc8b561f4ed98eff34832be96519132713ad7632c8ad227d23a6865919b427c2151b1ac4d59946be5374236317d6499a7233494a2c901f1c850cb387873837e17ea672069c10a74d9b66c4725b7f0c11374a398246adc753395bef43757b71b0a53c9e06c0e68b9b9602f2d96fee51981a17fda0cfaa1bb03fe6c2e7248daa92c7610ec6f485d03fa5d185e7dd8ea20f1846a4409497a5b6096fe4f45462b83123260c60c35aaf64935fd4ca724cab194c0ed09a1521068655e0beead8e1aec1efa83cf9ed6a63e5a813f5e2404a8e1d24886e233fafc389a1c88773fe5ece13aad5ecd51f17d6ddcba75604c1de8094986e3dd29cd4a7e453e73f3c60a5136234187df9fb67f411cb535335e00bdc21408e01020d5c214897ec40778099f6d38a49d7bf6af411b0435554445d4cf34162d3abad9e1c0dc160e8aad7023872b1a2d4b4e8deeeb41cf3bde67c706ab5fac14e7a3db5866c9d7c7fa9f6f05380707242b04b8f0c5aa17f9c0e9d54423213c80fc1c5f544f39fdc12aa59fb658662d04ed21720c55cfef6360c1e9bdb678ce5e760e1a1fe659db1528bc153ee151265e73476530f772d519a35086917b70ab3b0c32a4feaa8b63bad8e70f0159b427ded98befecea820e0bc5e483ec02879cc39925f593f865e19830661663c025269c1ca0702372b2e8404afbf056a242308b2846035418d75a821faa9f1798061138311146e98786f7f7354cb5541fcf71d3e6454fcd565e2f36f6f7231b17b0212b92288e22287c1ccfaae46cf97e025c0d7e1c0c15c7051a5eed6d8e968fda3b9f6a5fbc168030e9b12d70da785cb7ad832e1fbd4aab11d54c183a9b8111f11a187f3753c273f9056432e9c6619bb899517001b59bd0f529fb5acf359b6e5326a49dbe83acff54ab3a12dd64e9f1b9b35e02676e302c17b89762c356bfa6582addf24501093152821954f9bc85a99b7847df615369cbb4d2ccb6b8e42052b700df128f40c2a6154dc38771ed3c2b5c1226cb5c8b241ed49efb7d7e33242b7d16a997b8a097d130ea4f943443a5fa44e896770f61bffe272a7b2047741d77f6393d0ca4e650e9f9228461fa407ba4e748c7b0280a014cd5c8240592b8aed3981f6957d8dd2233c88ff9ea87319de79f33dbd8c50f48dbdbb8533e481ffbe12324b96bdb9be7f1cf72eb714d6ff7629e9a2df61cb77b1019af847edb9170f68e39108c5ba12c1c643e1bb30bb5f7f8910b9585c312a1643b7aaaba36f4072a862578b459546d3d76783cc79a549c30572b3779765b74f7c60560c814eb19b167369c53632d63c6f74644b30a8e215e93b7bfee35ef3c391c2fede92ab485a64a31ae8d15439bc8ac26eb7dffa6e3e2c903baf75bce6e0cf218c6f02fac531e1429726938d6fc1bd5da5fca19f6cb4ca896ad3cdec9816f95d3dfc7f3252b44bd10af2a07890f4a0de5f0e7659bc2e2d6e4b442cf52794b47b7dd36e5abbac2b8006ec9e71783341c154a77dcce7ae4b110f2f2929361aaefa4076034d32f338b005ad1b309184120d7651a39f16b34560e0bf8926c28889a0fc628a7a1c5b2b0387f9e280f770b3afcca4b44fd4ca9642ffd11f543606633f8f6a8e4a6ba6ec6366da445274dde2464bad9c8cdb1916802e1647a4d3b1d4f315becda7c1e363d35fc3955ec7fd494d724ed50a8ac6482483f1ad4b9857238d06457ddce3e430724df35145ce1829f96de692d6e8c0f1b64621ef9c3e1b54dc8a50021cbaff91f0e04fc70b4ea5d23baefb48b5bb6b071fde9936b22e6edd61e8154ca88d352c634add06efb972e20a26f9a52206b445a3f3ca3be9fecdadffa8b6e7570e777831b89b0c8fda6bf74074981ae72a7aca4b7b895abca8202671dbd665da8c851a511e0d43dcfc6871f425f1fc2a95a583ca294f0ff35359ad5fe599829aad02a6a06298f50e89c357250704e6a74b25907e0ed3ff8c351fdc37f8fb1535bc521c51fcaed81c982a78a610f58883d8de295ca0e893f4433d269f4ff158ebfa44fd2191d64651efc36191ec9627680cab647d8a67fa1e71a7561a208288f8a8486ca2e965183fe90486167668156b6b7c673de60d3362e32ff2444a3fdc7c8e3da60049301a6040e08fc5a8cab54814bb9cdc9ccc35bc936c7ee1c9652d92e618e9229a74d90896886cacdd23e4ad75ae1575be285b5e36b5392dbbbb3fa2e5e7e1a1aea66e364469a288c99fbe749aacd6ab76c09cf6779a09705051bfc92005fe70b67d116294f9c306518a8ee46e5f4b8149d7758047ffcb2c90f5b03265ded12178d455154c049b115d221318577864ab274564e1279baf8300cfb3152aaf7361e4ade4f79914d16741d2845832bc61d5d246f1f441a9e76e52a3817de5e9af33580493d8fb69a48693bb2b4ac68bf578c860f088c8f91498ae44e24b3acbf6b2aadebf63c9358cd98bf3bc6c7ab8d9b6fba82bb66743ea53b5715a087ced7d137bdd175d01de824c57956000259af5d4708d46db044e7cc30fef92ac17aa7540a07b57ae87f98e60dc090785a2e9f2f461d7d8a333944119a3d8406508743ad0c8d935fb8728555e7455c461adadc9b8a41a81cbc02c25fbdf7eee93e5f893d31629991bb8e2add604602cb92b9995d91aafbb028efd6287bcce79fd5597efe286594e4c9c58a5d8be54ac1b8fb04208bd41ce5fda12fd0289a66ce641a3f8e1e082899d229d78919995b09570c5e74aaadfc786844590ae2e55ec4e55165bc2f6e58639160b49561b158ba1290aeaaefccc0853863c4f0d218f5ba16a3d4c600a67786443bb322262403cfa59164b7f928fb91459009e4e0a60225574d4a8b0eff88f997ffe550a9ecc3ae517852cfb4de6b0a8f33d0cb770263b6b57180192535e6624ce7d7a5b18a2b533e03abe7aaf91ada86b8736b5126e0e8dc63f8a8003dbdd7e3da583b2aa8e9b16a48bddd65abf44019531e0b00f2dd684d3bed7f8ac40d6f7426d12c5ca38f419fcdbfefeb587e7136a728fc49f8874ff8d6993c1509b8a6445ae390c3e9bf3e10f357749b93fac824150ee6a07ad40f002f1c123e14f82118abcd1adf89d1101fb6374632f2c8ac805aceeea8c43164f4f8f4a60b14de23d683ca30f76c39785f93c871dd1d613c1d13a01a60c40d37491ba0c23c78b827868a353ff16db6154bcae9477bd53549024ef92c1505a9bba9b8b9b11382ee063a634f702dab18eaea32bfcdf9379e0662279c8a744db2f5abe415ea10f477e715da5afc21ca25350bec6c159ffa093b8f762b15406a21691049da6518f5e25255d5b4d5bb4af2a13077e4804d15d2d8d6e15d785cd34cc31445102b3ad6ce799b5635dee0f6c99dac488d2aa7b4b4fd04b2aefff70cacfca26c3f64bb8f09df9136eabc8875a02980006fd424f583ef2c3070dabba6543dcc50b380bd51daaf61fa77bdec68740819090324bf350907fc9945939cadbb91c2b591320f81451c004897ecfd165084a908dfe43b53669369046fde0356cd42ae567b3429bf2238af546e7aa263af676205c2946baefd809789b52da11381fcd76f1eb510dee5ff478b8b7c82e9e3b03b62ac2bbba03cd6453868638e3ba0fbe5bbeec9118b9258f0768b2b7e1d87e052a062b0a87261f9ab822733c202b244e5b388e13de8467a3f488b64bfbdba6e8238cc9080cfa47991e5a249467c484dd5bb932ca32314d21acd249b007a8d685fa1ba7359f96c008653605444be6462be3440a7ba3eaa4571bb30cbe0cedee8563a1692c66bc56bafb9dc07c1eca7c0d82fb8282662f1666f6c8f373f1edeb4ad6cd8d4cd3cbb102a581bbb7473775e78c2593a2121c7cb71890ed224e9e16a6a3297beaad146acadf80bd97baab632b566402fa1c71910f0e062f4287dce118b74bddd3d1ef3c92b23877f2645b0ddb7bf13393495dba29f21f658aa76b3a85804f09b7f87eb4a676ab6a8a992091238a84395ad9f0b5baca693a4465b2ce3bc6faaf6e0a438f8dd165855051822da848a9a98020f6c7c323e8b1a0f5679625f88d6e0d439df43c2071d0fb7d0fb7abb5928b49bb3f5452114e00a4dcecacab271bda2312b25019f2903205afff1c177fb9e16be1f6583008fd2535fa41b84a96eb36ea0122b572b0292372ed7e3b539c27dd009720bab97907c32e66301a9a3fbeb02713490ff0592ca1bb2164190c4ded1dda29bc3b5064aceff7285ebf9ea49c5cdfb8e60f6d10a493df895757d1ba489ce9ccaf2d05be1d5c2ce9be40e905792c2e6feab7821df0cc7f2524b830b19cd610887df6b2030c9d7d58d91ce06c24d4c4e6af0841ab1cf0ef51c716a7e8e40be16be33ef5e9af810aed3a5dc35c5b3359afc5c1a41945169d7ad7ebd7c1361643e744a9cd7a08e67443a2dbd7d1e4003e7ada5c717707c7fc8d992da801ed4b017bb9a5eee128d23e4e5ad777d545e811b506b91a269d6aa83004f381dad938099dc0d25c1704eed302e0f244fe082457a03dc295a884a6a5e9949e6c6ddb17428 \ No newline at end of file diff --git a/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000_0202020202020202020202020202020202020202020202020202020202020202.txt b/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000_0202020202020202020202020202020202020202020202020202020202020202.txt new file mode 100644 index 00000000..451e846e --- /dev/null +++ b/contracts/satoshi-bridge/tests/orchard_bundle_cache_170000_0202020202020202020202020202020202020202020202020202020202020202.txt @@ -0,0 +1,2 @@ +utest1fnxc0uu58cm9j2ucvda3h30sgujzs2jz4ael039rwuvx23qgrr389lzde5trnekx7sl5t242t9zr3dq00zh7uz67z7gaezzf35j6gxq9jtwcg2xrs356qz2jjjqujymahm3z2kxwrxp +015cf5c38a183143c5dc18eec34c154e1d9244616293a4b709241f2613334a65ac6267d2cb27196c3ae9d3301165a3c5c55a039f3f1b0cd0d23268238ac076fb180b2d6e28e7bac0415f838a03a947cc607bd04b1613e27598672ff235ce1897000acc2e1f7bd27d6970f64418ecce291014ba4580bb5da099f10878993437bf1d76688538ecd8d3b7f94984ab794f40c161d8490d28fdd206cb120efb80ff46a0b7c9df812707d0f69629f5948838aa5bd836976946677151158c13b6bee04efa979499c87b441264199644b2d461bf0c4391650a2d567a3fa30b6f6da125895721beb97a5407329e25d3082522e5c3110b9d39006f429668ba9ae764aaa5cdb87dbcd9b8a3eb0a933b8f636aac6049717856ce9bc5b0835b9804ee062852a63c311f56b7798d6f00058f7426fd75704e7d7b0d29b32d6e90b45ddec8e19bba7f89aaa4af8bda709c4e4b4a278e2771c8562ed8806b4403bb8aa99123d603da96838258c1d20bce6c91303fc1cf7b8bd3d762e90b126a7cac2add87531d56417fd5c7799bb890a7c5d94cee9c9a9a61c69f3028d247fbb3f74f2c985fc0d090edd12e8f95021609bfa4a09dd8c297935a4f5386175adef8abf640ee85b167ca508761d58ab581003591f07a154aa2ef4c6bd290e5918d8d6cfcb2129f565e7fbbf5e3304da8ae2dd34c2a6846925220e42680116687fcf3d3714de0ce8f94f0d1b8210fd053b6c9c37ea7f0a318ee7f625bf3df089a31115f299af3c1947f0f687af2bef96f8e10cfd4bfa86599c483305ca1bb5bab445b8755201b71baf35380aa4ab01032d685f38fb72f7fcc3a88c870ec82b6536bbcfd61a222436c0276bb35f4d99d1175a5656bc076de192e9f778f23b115c2e8101daf1642687cf5b21e6eb464418bacb56dc86627715351dff702b0be8b3d3fbfbaf1d800c8171b251d53afc4c75ad70ef5fd2d48a80218fa5fa1847b9ec89588224bf450330b2d22280093b294bf3b4e091b2a41597fc55505dbbf6207f91a3c0eaf301b11584b7a0bfaa87204027273e1a843aceb3c12657b3262751e63950bcca7bae3c05c2f0048eb83ddf7290e7b08be5d11ee7b706a03739efd2885ef613b05ad8d04e443b2bc29a8422364e4aedde97c9ab47073838c8c8ca40e02f067fdffffffffffae2935f1dfd8a24aed7c70df7de3a668eb7a49b1319880dde2bbd9031ae5d82ffd80132fcae27f0b047610dd69865bc10ddb898f0c982e50f839903ab6113abc86979995a9046befe2b68d2bba1ec547e068bb02ba7e695085261311157fb7797a4c0de25f12acdbbf6d940106341de1820668d248fb4213f86ca1d20b4ad6c42022227c8404dfe849b988a42bed65eb925df909b39f6a0df495bc20336fcad42f222bc9d242c7d9f7b9b6030fbdcb7f8b05e06a4f258f25b3416c44f6f1f2a990548ae4bce350203d178e7b843ef59ac93fcaafec3eb3caa783b080f66e0264dc2598567286d280370351594550981c336b36c17350b30b3c6dcdf4d8e2cc24413d1332c6f2017f6678dd61eefacf51701c01d36e44e9188dddd2f8732e0f4828f288787f0db9c51748ec4a9a75de6983030e1aaad8b509628670094745eb02597a1d2d5f0759ad71db316d3cdf6cc32215691ba624d157762f2e495f20b965f36a93be44c873e9d3263046ae58a28b1d1e4b1664f7579dbef7a10949ca9a11ff0ab5b23334d39e160efe88e737ee63d311eece875c3e7f7109077ec6941fa87eeca839b3ec5e4685217c1aee96e9a594679f3c7e207d797d8de59f29e40ce5fdf21516d5a1ff6bba3f0a204f7a94e5fe9aafc3e43134d4ff47d74d1285850efdc32e677bf89130c5126b42e3900f6b30a25d9c64c45221c83968124caa29f1272b3fde5ad04c71fe1e3e33e822f7501b1a1573cc5436fb1a1723a4e764131e1d1e2f8ed6965296f444c049160a048e0e1a7a6f5f013f9a1a1c4c660007d867d71e3cd9b08ef5ce17d84821a2c0e2c7896fdaee60620729cd60510da4bc73565780b71e29bfe8342641b558a8f319a83cbd58afc544dc44cdcbac02d0057445dcb0b71dd3f38adced4a97003d265b6dbc930908929fa2a4ea7db5177b6dcf6271d709f8170ff4954c0e358d95cd2d055da80d0d5bc0b5fd71dd37478537602e3208ba9edeca406358f9ccebcdcff5e59b165fe031375424b2be003a0f3ec0de77672df1a64f8cb70edfce6832a9a1f68a8bc3a5027847755e5b11168905fd1258660affb1fbfe11d57df94fa967cacace99f203f34cf91d2ffa85d606c2d5df07ad85ebac1d882dc085cd08aa34caacdc24d80e76ada7e12efb35b0b29017c329d1294d2e7fe23fd028b85fc9befb1ad60439e52a19d14e74265878af95915ccd138a4f273b2db7b00b3eafd9aa48bc89e243ad07bc689377baf2d3f7a6f664dceb2f0ece08fd0ec24bdc08b123728624129c7276d36f7794080cb8582ccc0f1419289b5431fcd5745d5238a0b3ef211a3f31542b9a327009397ae0f9250b5c4571885d5da3357be37a9fa5fc883935a1cbb8339eb25c51ad70da6962dd235c414e1ba77403170d1646799a869b8990a55f01535b7707a24c6a4ce0b0f2003fcbf021d4dc3e41f8d7db31137273060a7a56fe39cc552686bc2899c423309f11c3dd0bd44aa18a45e5f1ad056e0075634793841e4ba2e2c7a7990b05c4e7a23a927637814da0e46669eb96d472c4999556277c5534b50cbeb80d252dd418c56eb116169226a2c7830baecddb605b9b48c1d947e55b4c532a22799a177827f878085410f1d0c76434e32ba011b58971249fd75324253564599acf9ad2bca7976ea68f295373be52452e80fa151464765f543f9459650774e5e37cf13acc6ed5d6e46e1b18c2940ba35d680c91abf408d2e979498617988b49c9e670e896b08dca5a3138adb1cc1ed85258af4d1d8a022c37ff20b54fc39fcd00b97490efb3cc1513851119866b42084dda753935c14459ce49a480e1315b618831fee3aeb0ef48578f2091bd904fc2327a062648b7a76ca9b611079558f569864324622cf281c88975092663b64a8a7e1d5f539d84f393b36841ff1064b5785ac37aff1feaf084f0dd198862ecd5beb2c86617f80c7dc8587ebcf21a8c13e6a93d943d7aac79710cb533e4b28b9e07fe56dc962f4705dfa7cd55b8987d8234ad15e20627818264ea650b4fa59ca289a12611bb74eb32950a238b2fa74a40fc679387b5c7401f61e7d32e6240f78746dfdea915c8158af929201447ae06d6017487221c2b497e10ef452274edf729ded3bfa1ed322763b07435a52063809db356117a9e99be960ffa3b2db595c962162622f5c7ad13dc802f3b8a10c348af5401a13cf54f31462f150b209dd060b5b91815ad2b2fef522391569c150106d0b46270313d463816b10f0e25434cd822dda157d0a7bb247b8bccfa85d6d35d6652245cdfe7e1c6fb0396cc24ba4e06e985ce8c42fe83847e9123f559498c5920e35a0dbc474d4bc69d34493027a711b7ab4bc37ffe7f0c009475415759b8ad88a86184194d9419d07bc9c72f6cec7c06335f4f5734236af49c3c420cdbacc6b0f8bd4139cfe64c7fc3fb63226f32bce00356dab1d50c8b074aeb2cf246a362215f1f20de560b02fe06c53f3ae51217a69b9d81722cf0429d6f482ba07b962bd723e5ac4aa27748ff53e7a115ecf232d38decd545b5f122a02921e1d0e6f4c95c7763886dae704c902739a028850731da66f364c16fac064a37605a875fd0df7093351f17049be5d32245c81afe717b2b2c1f06fd94c7e9fce7ba4f4632a76e0d46b1d0fc36d156263939450ad2963733dc9935fb24e24ce72abdbcc7732a80377977c2dbc2c7a6ad7a38052b6301c5e30136b4e85dd813ac0374142b59b32ff58f15bf75273a4de74ad3e2178927fe7aca3fd15113e8a935d04a220035cd5ad03fe7e850e358d570f5e7eb1d35697dfad60cdb8717f0c788e8593b1ecced4b58a8461586674572cab4131c0e63e75f0cc41d289d84f37696f73cbdfdf1f494a3c9875ae55ec0842304ede532e6f4a047288a12b5983723b77ce3daa97ead3f7bcd9a5af9154264d583dd5729f4bfaf3f6c5370140a7642907454abd82552892585ed1735f0c2a63dee0cd102d0ef97d378c2084b50f573c0a45e7a3908b2993f0ef9be266e73865a1b532e2f24b5200f99d66e29ea73b648bc74cd1b587e481d1028f92ac46235717944533b5465b2e12ceb43797cebf8aef6eec9062f40efea01b0df36805e3fd9a1cd6a1e601751ec03ea1c0c010a94ea62b57b508fd310d587919a750cd14e5221eb4408f6eb2e1398da5469db4298135e04da835183df6dd8b6e079e476c4af7204951610648570016c5852f9959d483df1dade667e1a1a594a5b65b824bfc057ed3e2668a72878c88cc4d601306845ceefa2622a869ce0cb374e6e507552d0241e8a015ab4479def6b304382d2d188967dfde4531720deea84312a159f4e4f79fab522b7f5d95c9e08e931e26110c4e495c292c3f31caa9545a462a3c4cde0a265eb391fd70b9e611851e23224b262437a3b657cf00e858f59d44af4b8d538f8bd961006cb6ca2125635eb88b69c09ecea05007dc6724df2ba08b8ba4888b72e4fc92ed15b59df3e563baf624dde9748aef7d2b1f19dee275fb7327ae2d8ed281bd620cc89b74b972ecd259e61e6dd8cf6a12046947785f77df5cd4787e968be1b321dbb169cdd9117d62f3eb2d48ec0182298e2677fcdd577e6848e0672a324b7ac3bc96a550e2c246fc57416395eb6f9b8f415fde180d098fa36e24dc4c58e8e833ddecd096d8e39c2872d8b0c056174ef7be24a148c9fb8569907f8bba73797ad3ab29b81e2a469ab5b9e07f78ffa5d580798b637a99aaba9d1db5713e0c5013e3b2fb7f3f94537528f48d31945af8fa7fa4f604479ab75db957d98829f9d2b161dd27567f2b3dd8d4f8238488e5f38d3e110460e0d419e93cc5c24b8e7e5b9ed1e1b3cba7e5e217b50cd169e0ad915c70e395c3d18c8b62c650385ae72d6decb3d9cd4ca5a6bc54df8ac63bd5bd9c21e6b4e6a9e89dbaeb8e6a253033f8e4aeb2cca6411e5d8307b4ff5ce6d2eb38aafe747d1d0ba6541a7912ce51b455f96500fdfea6744f0a16b508726e9af4d2bb09316857164b21b8c82c1b6c4659cfa1f1360eca2aaff39e17654a92347cdfa845311d77535fb0b87ce65db2ed9c37a750137f7e6a1ce4fb7854cc28f403769e595b06b0766d25f437874f42172477ef213cd6744e7142a1c400f61d58dfc69bc2a2a73bfc2a359acf0e84d6386e75e741dff21240bcde0465f02389b23b6e44ece1aeebdacb21106873e924dc5083ffe2188c84d1a466e1983995a04d8210a91347213a0bc112f4e83c50c4f81a54a080e45caef5a30d541bfa3d708eb7a47631a8492c3c01817670d37a0122da9b0f3229eb50435f4e091f551c0c8df9878eb93cb23b863b8f672792d11591fbf1213287aa0d0d7942f48454c1159f2e88124ca49dbd3e29e20f1c51c6dc3b6d95e0e00d474137fb8344151c7d874d7c99929abec7c5a0f13c5ce862ab10da3a70c7e08310f24763ee16fca2c4e03110665c5289406ffe26c46cba27b5d66f0ad1a24214a22e891d419fb2b0f2e31b556c7a43e361870dcd4feb8e586387a60774de5380d87ceefe3d198ad743d56697a8fc8e4032cc317d3a8e2f01c266f501495f6044493561b2e7e31ef8a0684e46b0b8e29e8bb20405f24f54f0efa54f1b4dd3e3ea62fec1522cff9281534122b1f29571197644e62aa9576dbe03f77209a0ff629d14fd6aca6df3a4c53865933215170d7b368a099f3aec20b6dded8d8139ff237fa237691abc8a62cbc8d4d3e18662acc70f7c1ba7bce15b134f4dac176a7f7167c82df632b6fc00a876ea6981186a0265653927cb32f2cea5bd0902ecb801d095d1bf1e9405601375f6636cdae329e8ba8651555d9a2da64d13b9a7989dfd23f247042a9bfa901e47fc228612c27eb8eb9182d6f2007b367606990e778c46e1f420a6bfecde89fab8c99bc61233b70a9034149596923a2f17afffce59b5e323e4f7c0d285816ecedc2925dfd673333e0659ede964fe0685fc9eadb7321c1e5339279a3a7a009451c2bc6ea622e4baf672e8d21d701619afd0506506602700108c37eb3c8d60e93fae673da1e8d8dc458ed0929364ac19f93a9b4fee8b5e0573fedbb1920a5c39fed94d0640a6bfb2afa6ac77d9240c211c7db17d9ca4171a10f46d9752c2ae654d036e95ae46c3d0a4f4d08db7cb0c48450f75aa5b23fc6f632ec61cf0f10261823257242089c1c57ce3e0a909caad971e7c24f8b0473133d03e1a25f2cf696f665027ba094b1aef034524304aae8c789c4c7ab0cdd231a032bf4742120f0b777ae72e49bcb3d02e3336d6608b78a534a220a8747c1f7b9a63b26684fdf94eac77f1a6fde299ed3536ebea74cfeef5c8008ef49d9113bd5b03cac27cbfc6cdd50d4380ba5495f33efaa0ffbf6785d3a856b529dbde111b28e054a14d5f8a08163710be6df9a71d211c34b1470eca8e563efada6396191b87a3f1605d5803562aeb4b6545a50b151ea81fd9e5909dd164b67b992c93188429c07cd3658d8a09437b944cc475c1dd77855cf925947ba754b240361636a5d6fac3add10e52fbee67af4c69b59b73ba4322f929483071c83cb8f3df7a4c3d819e535eb953578eca56726c96522338be9f878953f4cdddceefc8cd79e86d074b0733b814aecadc8d508348573c4bb5ff5d17ecaf176c6f93f28f2ed8b730e54a87e3aa675e569829f4c1f2c70055c016215ed909656b79f0f1f4cfe7bb92bba382803cb8744e75549ce2deb599e6de5221d30f5897fcf0ee8dce4e19347951f012ebda171088116b0f2524a853a68bbeb8ef13b7487770a47bc96590f8086d9bf7208ce312f7acb50dd779d71d81f3fd3a47d712ebbbe4291ceeada92551e3d43703dad0ba8187a4395733cf5e597ca921dae7efb336446ae2fd226710a3cb84e452b193f3b0542de7966ac8733b7e12ee8459a5d6514e15a797022506c867d30af23a6f54a7864a3d9324e76a51f653dbd642663a457c9d3d882dcfb1f02c3cda00728f359b9548db2e030e67a602fa950f7db9d42f6b1821925f16eaf7d0215813b265611dc173e45201a98724fe3b8db49338ee23823533377248eb5fe47bcbf2c6dbb4dda5c7d109ab643133065aa08ac2c91a39949d782547dc7d909f9db673739b924b50b222366d56bbf5ec1f112a2b1ee70a153f7265eab1600080f78018c00a1fea65e37312e9846b3bcc8bea4d14eef682193eb4e5d0d96d306ccdd57823a805784664b5e538a0efb0368e6551e49a8cf77b117910d7b472fb678e18486105c3be7563577b926d1fbcd380e238dd2516a13ccdce9dc8500db5e89cd23967539336703ce232d3ad0d284abf35faaa57798ce6deaa6e263fa2773cfa98015aa104bf5e8b9cdee12d825d1949a2d2cd0037e730b4d89e873b14ea6a3f8120fc25e2d3afa8eead2ee0795e23facc49fd32ee433223f643f231d7fa1a459a0bf05b95cf6e11dfe2c992116ca938a3298095a89807e3d078f50169e5c23d0543ff0e1db6908d97053ef7d611784984d39a9f7b13f80fbb9b1e1fec8149f2c8b0fbe0f85388294a52e1a6b6e76716d9c41815699f6ccd7ffe7764535ca48720e1d025e3f571ff0694a80cce4ee8c1d10ee7dfcf67e35cb19b117251567e43421915c9330c3a873d8930199b280589581da3ac9ac6b28946004198a01d151c7f9b27e73b0a26b4919695c2f246981097accf24d9c8d640f0a59db3245466bde0f267502451a33ef0ec730fd02affd9b0060ee2c64b54415278788caa2a61a1f6e027f3514b8f018333c3a6b0f0abbc8e8cc1ad2c9b34dc9674a002b718f2e01993f17b2102bacac38a4545174edaa8e7136a38e9b09298180709aa79ce194fddc3e6a623f1ff54f1a2491ca3dfc910c71b88f0df17ce781de3df8a419d8f4b7bb0d2bc67620acb7f1eed50dc87e98da96f2486be2feda42f84bfc7949565b3e91254c2699ec89ca6031a2bea801530a96297d49d56f32f46209ea2f10c665515191faa4d17a590e1f64da7c4b206561259692a3f29e349e5e7a95b4e2368d8c4db1ea83ce7fcdb23dbf92bdb2373eaa0240ed3632e7b51c4e0438f53722f50b27202e80d8a8b8d25d781fb015922e03bc93e5972da885cf20a87c88697a68aa4036955c80debeb43268798fcc656f801d04b2aebd38facdb943b648cbd9593aef95f50f06cb80ae2494e56b90febde729e757340b8d4470fce4da14e29d8b684b1a9c618ffc1db2a0a7ccd5413c9fbb4988b9a5943df8152127afdcff34a0bfc23e287b11be737f499bce06399250e8cfdb7b6108d29d96fc99db5191a43716a23a \ No newline at end of file diff --git a/contracts/satoshi-bridge/tests/setup/context.rs b/contracts/satoshi-bridge/tests/setup/context.rs index b5aa024c..5885cd30 100644 --- a/contracts/satoshi-bridge/tests/setup/context.rs +++ b/contracts/satoshi-bridge/tests/setup/context.rs @@ -21,6 +21,13 @@ use satoshi_bridge::{ use crate::{PRICE_ORICE_NEAR_PRICE_ID, PYTH_ORICE_NEAR_PRICE_ID}; +const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg%20width%3D%2232%22%20height%3D%2232%22%20viewBox%3D%220%200%2032%2032%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20clip-path%3D%22url(%23clip0_2351_779)%22%3E%3Cpath%20d%3D%22M16%2032C24.8366%2032%2032%2024.8366%2032%2016C32%207.16344%2024.8366%200%2016%200C7.16344%200%200%207.16344%200%2016C0%2024.8366%207.16344%2032%2016%2032Z%22%20fill%3D%22%2300E99F%22%2F%3E%3Cpath%20d%3D%22M16.0006%2028.2858C22.7858%2028.2858%2028.2863%2022.7853%2028.2863%2016.0001C28.2863%209.21486%2022.7858%203.71436%2016.0006%203.71436C9.21535%203.71436%203.71484%209.21486%203.71484%2016.0001C3.71484%2022.7853%209.21535%2028.2858%2016.0006%2028.2858Z%22%20stroke%3D%22black%22%2F%3E%3Cpath%20d%3D%22M27.1412%2016C27.1412%2022.1541%2022.1524%2027.1429%2015.9983%2027.1429C9.84427%2027.1429%204.85547%2022.1541%204.85547%2016C4.85547%209.84598%209.84427%204.85718%2015.9983%204.85718C22.1524%204.85718%2027.1412%209.84598%2027.1412%2016Z%22%20stroke%3D%22black%22%20stroke-width%3D%220.5%22%2F%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M16.2167%2011.1743C15.9198%2011.1643%2015.6095%2011.1622%2015.2868%2011.1668V9.32056H13.8907V11.2217C13.1583%2011.2659%2012.3792%2011.3332%2011.5625%2011.4149V12.811H12.9586V18.8607H11.7952V20.4895H13.8893V22.5836H15.2854V20.4895H16.2161V22.5836H17.3795V20.4895C18.4654%2020.4119%2020.6836%2019.7915%2020.8698%2017.93C21.0559%2016.0686%2019.7064%2015.6032%2019.0083%2015.6032C19.5512%2015.3705%2020.544%2014.5328%2020.1717%2013.0436C19.9215%2012.043%2019.0072%2011.5204%2017.6128%2011.2984V9.32164H16.2167V11.1743ZM18.0737%2013.9723C18.0737%2012.8554%2016.2122%2012.7313%2015.2815%2012.8088V15.1356C16.2122%2015.2132%2018.0737%2015.0891%2018.0737%2013.9723ZM15.2826%2016.5322V18.8591C16.2133%2018.9366%2018.3075%2018.859%2018.3075%2017.6956C18.3075%2016.2994%2016.2133%2016.4547%2015.2826%2016.5322Z%22%20fill%3D%22black%22%2F%3E%3C%2Fg%3E%3Cdefs%3E%3CclipPath%20id%3D%22clip0_2351_779%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22white%22%2F%3E%3C%2FclipPath%3E%3C%2Fdefs%3E%3C%2Fsvg%3E"; + +#[cfg(feature = "zcash")] +const BRIDGE_WASM_PATH: &str = "../../res/zcash_bridge.wasm"; +#[cfg(not(feature = "zcash"))] +const BRIDGE_WASM_PATH: &str = "../../res/bitcoin_bridge.wasm"; + pub struct Context { pub root: Account, pub tx_listener: Account, @@ -36,7 +43,7 @@ pub struct Context { } impl Context { - pub async fn new(worker: &Worker) -> Self { + pub async fn new(worker: &Worker, chain: Option) -> Self { let root = worker.root_account().unwrap(); let ( bridge_contract, @@ -54,7 +61,7 @@ impl Context { .unwrap() .unwrap(); bridge - .deploy(&std::fs::read("../../res/satoshi_bridge.wasm").unwrap()) + .deploy(&std::fs::read(BRIDGE_WASM_PATH).unwrap()) .await .unwrap() .unwrap() @@ -140,6 +147,10 @@ impl Context { .args_json(json!({ "controller": root.id(), "bridge_id": bridge_contract.id(), + "name": "Near BTC".to_string(), + "symbol": "NBTC".to_string(), + "icon": Some(DATA_IMAGE_SVG_NEAR_ICON.to_string()), + "decimals": 8, })) .transact() .await @@ -149,16 +160,21 @@ impl Context { chain_signatures_contract .call("new") .args_json(json!({ - "public_key": "secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3", + "public_key": "secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3", })) .transact() .await .unwrap() .unwrap(); + let chain = chain.unwrap_or( + std::env::var("TEST_CHAIN").unwrap_or_else(|_| "BitcoinMainnet".to_string()), + ); + root.call(bridge_contract.id(), "new") .args_json(json!({ "config": { + "chain": chain, "chain_signatures_account_id": chain_signatures_contract.id(), "nbtc_account_id": nbtc_contract.id(), "btc_light_client_account_id": btc_light_client_contract.id(), @@ -194,6 +210,7 @@ impl Context { "rbf_num_limit": 99, "max_btc_tx_pending_sec": 3600 * 24, "unhealthy_utxo_amount": 1000, + "expiry_height_gap": 5000, } })) .transact() @@ -220,6 +237,20 @@ impl Context { .unwrap() .unwrap(); + // Grant UnrestrictedRelayer role to test accounts so they can call + // methods guarded by #[trusted_relayer] + for account in [&relayer, &alice, &bob, &charlie, &tx_listener] { + root.call(bridge_contract.id(), "acl_grant_role") + .args_json(json!({ + "role": "UnrestrictedRelayer", + "account_id": account.id() + })) + .transact() + .await + .unwrap() + .unwrap(); + } + Self { root, tx_listener, @@ -260,7 +291,8 @@ impl Context { worker: &Worker, account_id: &AccountId, ) -> u128 { - match worker.view_account(account_id).await { + let ws_account_id: near_workspaces::AccountId = account_id.as_str().parse().unwrap(); + match worker.view_account(&ws_account_id).await { Ok(a) => a.balance.as_yoctonear(), Err(_) => 0, } @@ -1038,7 +1070,11 @@ pub struct UpgradeContext { } impl UpgradeContext { - pub async fn new(worker: &Worker) -> Self { + pub async fn new( + worker: &Worker, + previous_satoshi_bridge_contract_path: &str, + previous_nbtc_contract_path: &str, + ) -> Self { let root = worker.root_account().unwrap(); let ( previous_satoshi_bridge_contract, @@ -1048,7 +1084,7 @@ impl UpgradeContext { ) = tokio::join!( async { worker - .dev_deploy(&std::fs::read("../../res/satoshi_bridge.wasm").unwrap()) + .dev_deploy(&std::fs::read(previous_satoshi_bridge_contract_path).unwrap()) .await .unwrap() }, @@ -1060,7 +1096,7 @@ impl UpgradeContext { .await .unwrap() .unwrap(); - nbtc.deploy(&std::fs::read("../../res/nbtc.wasm").unwrap()) + nbtc.deploy(&std::fs::read(previous_nbtc_contract_path).unwrap()) .await .unwrap() .unwrap() @@ -1082,6 +1118,7 @@ impl UpgradeContext { root.call(previous_satoshi_bridge_contract.id(), "new") .args_json(json!({ "config": { + "chain": "BitcoinMainnet", "chain_signatures_account_id": chain_signatures_contract.id(), "nbtc_account_id": previous_nbtc_contract.id(), "btc_light_client_account_id": btc_light_client_contract.id(), @@ -1117,6 +1154,7 @@ impl UpgradeContext { "rbf_num_limit": 99, "max_btc_tx_pending_sec": 3600 * 24, "unhealthy_utxo_amount": 1000, + "expiry_height_gap": 1000, } })) .transact() @@ -1138,6 +1176,10 @@ impl UpgradeContext { .args_json(json!({ "controller": root.id(), "bridge_id": previous_satoshi_bridge_contract.id(), + "name": "nBTC", + "symbol": "nBTC", + "icon": Option::::None, + "decimals": 8, })) .transact() .await diff --git a/contracts/satoshi-bridge/tests/setup/mod.rs b/contracts/satoshi-bridge/tests/setup/mod.rs index 38dd94e7..5ddd6de5 100644 --- a/contracts/satoshi-bridge/tests/setup/mod.rs +++ b/contracts/satoshi-bridge/tests/setup/mod.rs @@ -1,6 +1,30 @@ #![allow(dead_code)] #![allow(unused_imports)] pub mod context; +#[cfg(feature = "zcash")] +pub mod orchard; pub mod utils; pub use context::*; +#[cfg(feature = "zcash")] +pub use orchard::*; pub use utils::*; + +// Re-export types used by tests +pub use bitcoin::OutPoint; +#[cfg(feature = "zcash")] +pub use satoshi_bridge::zcash_utils::types::ChainSpecificData; +pub use satoshi_bridge::{DepositMsg, TokenReceiverMessage}; + +/// Extension trait to convert `near_workspaces::Account` IDs (`near-account-id` v1) +/// to `near_sdk::AccountId` (`near-account-id` v2) via string roundtrip. +/// Required because `near-workspaces` and `near-sdk` depend on different major +/// versions of the `near-account-id` crate. +pub trait ToSdkAccountId { + fn sdk_id(&self) -> near_sdk::AccountId; +} + +impl ToSdkAccountId for near_workspaces::Account { + fn sdk_id(&self) -> near_sdk::AccountId { + self.id().as_str().parse().unwrap() + } +} diff --git a/contracts/satoshi-bridge/tests/setup/orchard.rs b/contracts/satoshi-bridge/tests/setup/orchard.rs new file mode 100644 index 00000000..f128ff6d --- /dev/null +++ b/contracts/satoshi-bridge/tests/setup/orchard.rs @@ -0,0 +1,164 @@ +use orchard::builder::{Builder, BundleType}; +use orchard::keys::{FullViewingKey, OutgoingViewingKey, Scope, SpendingKey}; +use orchard::tree::Anchor; +use orchard::value::NoteValue; +use rand::rngs::OsRng; +use zcash_address::unified::{Encoding, Receiver}; +use zcash_address::{ToAddress, ZcashAddress}; +use zcash_primitives::transaction::components::orchard::write_v5_bundle; + +/// Bridge OVK used for all test bundles (same as production) +pub const BRIDGE_OVK: [u8; 32] = [0u8; 32]; + +/// Generate a Unified Address containing an Orchard receiver and a single-action +/// Orchard v5 bundle hex that is recoverable with BRIDGE_OVK. +/// +/// This function is expensive (generates Halo2 proof), so results should be cached. +/// +/// If spending_key_bytes is provided, uses that key; otherwise defaults to [7u8; 32]. +pub fn gen_ua_and_orchard_bundle_hex_with_key( + amount: u64, + network: &str, + spending_key_bytes: Option<[u8; 32]>, +) -> (String, String) { + let mut rng = OsRng; + + // Use provided spending key or default to [7u8; 32] for test reproducibility + let sk_bytes = spending_key_bytes.unwrap_or([7u8; 32]); + let sk = SpendingKey::from_bytes(sk_bytes).expect("spending key"); + let fvk = FullViewingKey::from(&sk); + let recipient = fvk.address_at(0u32, Scope::External); + + // Build a simple output-only bundle with BRIDGE_OVK + // Use Coinbase bundle type which supports single output without dummy actions + let mut builder = Builder::new(BundleType::Coinbase, Anchor::empty_tree()); + builder + .add_output( + Some(OutgoingViewingKey::from(BRIDGE_OVK)), + recipient, + NoteValue::from_raw(amount), + [0u8; 512], // memo + ) + .expect("add output"); + + // Build and authorize the bundle (this is the expensive part - generates Halo2 proof) + let (unauth, _) = builder + .build::(&mut rng) + .expect("build orchard bundle") + .expect("bundle present"); + + let pk = orchard::circuit::ProvingKey::build(); + let authorized = unauth + .create_proof(&pk, &mut rng) + .expect("create proof") + .prepare(rng, [0u8; 32]) + .finalize() + .expect("finalize proof"); + + // Produce Unified Address string containing BOTH Orchard and transparent (P2PKH) receivers + // The transparent receiver is derived from the spending key for consistency + let orchard_raw = recipient.to_raw_address_bytes(); + + // Generate a deterministic P2PKH hash from the spending key + // This allows the contract to extract a script_pubkey for validation + let mut p2pkh_hash = [0u8; 20]; + p2pkh_hash.copy_from_slice(&sk_bytes[0..20]); + + let ua = zcash_address::unified::Address::try_from_items(vec![ + Receiver::Orchard(orchard_raw), + Receiver::P2pkh(p2pkh_hash), + ]) + .expect("UA from orchard and p2pkh receivers"); + + let network_type = match network { + "main" | "mainnet" => zcash_protocol::consensus::NetworkType::Main, + _ => zcash_protocol::consensus::NetworkType::Test, + }; + let zaddr = ZcashAddress::from_unified(network_type, ua); + let ua_str = zaddr.encode(); + + // Serialize bundle to v5 bytes and hex-encode + let mut bytes = vec![]; + write_v5_bundle(Some(&authorized), &mut bytes).expect("write v5 bundle"); + + (ua_str, hex::encode(bytes)) +} + +/// Generate a Unified Address and bundle with default spending key [7u8; 32]. +/// Wrapper for backward compatibility. +pub fn gen_ua_and_orchard_bundle_hex(amount: u64, network: &str) -> (String, String) { + gen_ua_and_orchard_bundle_hex_with_key(amount, network, None) +} + +/// Get or generate a cached Orchard bundle for the given amount. +/// Caches to a local file to avoid expensive regeneration. +pub fn get_or_gen_bundle(amount: u64) -> (String, String) { + use std::fs; + use std::path::Path; + + let cache_file = format!("tests/orchard_bundle_cache_{}.txt", amount); + let cache_path = Path::new(&cache_file); + + // Try to load from cache + if cache_path.exists() { + if let Ok(contents) = fs::read_to_string(cache_path) { + let lines: Vec<&str> = contents.lines().collect(); + if lines.len() == 2 { + return (lines[0].to_string(), lines[1].to_string()); + } + } + } + + // Cache miss or invalid - generate new bundle + println!( + "Generating Orchard bundle for amount {}... (this may take a while)", + amount + ); + let (ua, bundle_hex) = gen_ua_and_orchard_bundle_hex(amount, "testnet"); + + // Save to cache + let cache_content = format!("{}\n{}", ua, bundle_hex); + if let Err(e) = fs::write(cache_path, cache_content) { + eprintln!("Warning: Failed to cache bundle: {}", e); + } + + (ua, bundle_hex) +} + +/// Generate a bundle with a specific spending key (for testing recipient mismatch). +/// Caches based on both amount and spending key to avoid expensive regeneration. +pub fn gen_bundle_with_key(amount: u64, spending_key: [u8; 32]) -> (String, String) { + use std::fs; + use std::path::Path; + + // Create cache key from amount + hex-encoded spending key + let key_hex = hex::encode(spending_key); + let cache_file = format!("tests/orchard_bundle_cache_{}_{}.txt", amount, key_hex); + let cache_path = Path::new(&cache_file); + + // Try to load from cache + if cache_path.exists() { + if let Ok(contents) = fs::read_to_string(cache_path) { + let lines: Vec<&str> = contents.lines().collect(); + if lines.len() == 2 { + return (lines[0].to_string(), lines[1].to_string()); + } + } + } + + // Cache miss or invalid - generate new bundle + println!( + "Generating Orchard bundle with custom key for amount {}... (this may take a while)", + amount + ); + let (ua, bundle_hex) = + gen_ua_and_orchard_bundle_hex_with_key(amount, "testnet", Some(spending_key)); + + // Save to cache + let cache_content = format!("{}\n{}", ua, bundle_hex); + if let Err(e) = fs::write(cache_path, cache_content) { + eprintln!("Warning: Failed to cache bundle: {}", e); + } + + (ua, bundle_hex) +} diff --git a/contracts/satoshi-bridge/tests/setup/utils.rs b/contracts/satoshi-bridge/tests/setup/utils.rs index 7a455742..cac6975b 100644 --- a/contracts/satoshi-bridge/tests/setup/utils.rs +++ b/contracts/satoshi-bridge/tests/setup/utils.rs @@ -6,6 +6,7 @@ use bitcoin::{ Psbt, Transaction as BtcTransaction, TxIn, TxOut, }; use near_workspaces::{result::ExecutionFinalResult, Result}; +use satoshi_bridge::network::Chain; pub const PRICE_ORICE_BTC_PRICE_ID: &str = "btc_price_id"; pub const PRICE_ORICE_NEAR_PRICE_ID: &str = "near_price_id"; @@ -81,16 +82,17 @@ pub fn generate_tx_in(tx_id: &str, vout: u32, script_addr: Option<&str>) -> TxIn tx_in } -pub fn generate_tx_out(value: u64, script_addr: &str) -> TxOut { - let address = Address::from_str(script_addr) - .expect("Invalid btc address") - .assume_checked(); +pub fn generate_tx_out(value: u64, script_addr: &str, chain: Chain) -> TxOut { + let address = + satoshi_bridge::network::Address::parse(script_addr, chain).expect("Invalid btc address"); TxOut { value: Amount::from_sat(value), - script_pubkey: address.script_pubkey(), + script_pubkey: address + .script_pubkey() + .expect("Failed to get script pubkey"), } } - +#[cfg(not(feature = "zcash"))] pub fn generate_transaction_bytes( tx_ins: Vec<(&str, u32, Option<&str>)>, tx_outs: Vec<(&str, u64)>, @@ -104,7 +106,7 @@ pub fn generate_transaction_bytes( .collect(), output: tx_outs .into_iter() - .map(|(script_addr, value)| generate_tx_out(value, script_addr)) + .map(|(script_addr, value)| generate_tx_out(value, script_addr, Chain::BitcoinMainnet)) .collect(), }) } @@ -119,6 +121,129 @@ pub fn generate_input_bytes( bytes } +#[cfg(feature = "zcash")] +pub fn generate_transaction_bytes( + tx_ins: Vec<(&str, u32, Option<&str>)>, + tx_outs: Vec<(&str, u64)>, +) -> Vec { + use zcash_primitives::consensus::{BlockHeight, BranchId}; + use zcash_primitives::transaction::{TransactionData, TxVersion}; + use zcash_transparent::bundle::{ + Authorized, OutPoint as ZcashOutPoint, TxIn as ZcashTxIn, TxOut as ZcashTxOut, + }; + + // Create transparent inputs + let zcash_inputs: Vec> = tx_ins + .into_iter() + .map(|(tx_id, vout, script_addr)| { + // Parse the txid string to bytes + let txid_bytes = hex::decode(tx_id).expect("Invalid txid hex"); + let mut txid_array = [0u8; 32]; + txid_array.copy_from_slice(&txid_bytes); + + let prevout = ZcashOutPoint::new(txid_array, vout); + + // Create script_sig from address if provided + let script_sig = if let Some(addr) = script_addr { + let address = Address::from_str(addr) + .expect("Invalid btc address") + .assume_checked(); + zcash_transparent::address::Script(address.script_pubkey().to_bytes()) + } else { + zcash_transparent::address::Script(vec![]) + }; + + ZcashTxIn { + prevout, + script_sig, + sequence: 4294967293, // Same as Bitcoin version + } + }) + .collect(); + + // Create transparent outputs + let zcash_outputs: Vec = tx_outs + .into_iter() + .map(|(script_addr, value)| { + // Try to parse as Zcash address first, fallback to Bitcoin address + let script_pubkey = if script_addr.starts_with('t') || script_addr.starts_with('z') { + // Zcash address - decode it properly + use zcash_address::ZcashAddress; + match ZcashAddress::try_from_encoded(script_addr) { + Ok(zcash_addr) => { + // Use zcash_address crate to properly convert to transparent address + use zcash_protocol::consensus::NetworkType; + let (_network, addr_data) = zcash_addr.convert::<(NetworkType, zcash_transparent::address::TransparentAddress)>() + .expect("Failed to convert Zcash address to transparent address"); + + // Create script from the transparent address + let script_bytes = match addr_data { + zcash_transparent::address::TransparentAddress::PublicKeyHash(hash) => { + // P2PKH: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG + let mut script = vec![0x76, 0xa9, 0x14]; // OP_DUP, OP_HASH160, push 20 bytes + script.extend_from_slice(&hash); + script.push(0x88); // OP_EQUALVERIFY + script.push(0xac); // OP_CHECKSIG + script + }, + zcash_transparent::address::TransparentAddress::ScriptHash(hash) => { + // P2SH: OP_HASH160 OP_EQUAL + let mut script = vec![0xa9, 0x14]; // OP_HASH160, push 20 bytes + script.extend_from_slice(&hash); + script.push(0x87); // OP_EQUAL + script + }, + }; + zcash_transparent::address::Script(script_bytes) + }, + Err(_) => panic!("Invalid Zcash address: {}", script_addr), + } + } else { + // Bitcoin address + let address = Address::from_str(script_addr) + .expect("Invalid btc address") + .assume_checked(); + zcash_transparent::address::Script(address.script_pubkey().to_bytes()) + }; + + ZcashTxOut { + value: zcash_protocol::value::Zatoshis::const_from_u64(value), + script_pubkey, + } + }) + .collect(); + + // Create transparent bundle + let transparent_bundle = zcash_transparent::bundle::Bundle { + vin: zcash_inputs, + vout: zcash_outputs, + authorization: zcash_transparent::bundle::Authorized, + }; + + // Build Zcash v5 transaction + // Use Nu6_1 for testnet to match contract's decode expectations + let tx_data = TransactionData::from_parts( + TxVersion::V5, // V5 + BranchId::Nu6_1, // Use Nu6_1 for testnet + 0, // lock_time + BlockHeight::from_u32(2000), // expiry_height (must be > 0 for v5) + Some(transparent_bundle), + None, // sapling + None, // orchard_v5 + None, // orchard (will be added later in withdrawal flow) + ); + + // Freeze the transaction to get the final Transaction object + let tx = tx_data.freeze().expect("Failed to freeze transaction"); + + // Serialize the transaction + let mut buf = Vec::new(); + tx.write(&mut buf) + .expect("Failed to serialize Zcash transaction"); + + buf +} + pub fn tool_err_msg(outcome: &Result) -> String { match outcome { Ok(res) => { diff --git a/contracts/satoshi-bridge/tests/test_orchard_validation.rs b/contracts/satoshi-bridge/tests/test_orchard_validation.rs new file mode 100644 index 00000000..fbd115dc --- /dev/null +++ b/contracts/satoshi-bridge/tests/test_orchard_validation.rs @@ -0,0 +1,375 @@ +mod setup; +use setup::*; + +#[cfg(feature = "zcash")] +use bitcoin::{Amount, TxOut}; +#[cfg(feature = "zcash")] +use satoshi_bridge::network::{Address, Chain}; + +/// Test: Bundle with wrong recipient should be rejected +/// +/// Generates two bundles with different spending keys to create different recipients. +/// Uses bundle A but claims it's for recipient B - should be rejected. +#[tokio::test] +#[cfg(feature = "zcash")] +async fn test_orchard_wrong_recipient() { + // Set chain to ZcashTestnet for this test + std::env::set_var("TEST_CHAIN", "ZcashTestnet"); + + let worker = near_workspaces::sandbox().await.unwrap(); + let context = Context::new(&worker, None).await; + + check!(context.set_deposit_bridge_fee(10000, 0, 9000)); + check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); + + let config = context.get_bridge_config().await.unwrap(); + + // Setup: Deposit for alice + let alice_btc_deposit_address = context + .get_user_deposit_address(DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }) + .await + .unwrap(); + + check!(context.verify_deposit( + "relayer", + DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }, + generate_transaction_bytes( + vec![( + "c6774e76452c36bba6c357653f620a4364fc063ba021e2acf6049f8d9e6b0234", + 1, + None, + )], + vec![ + ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (alice_btc_deposit_address.as_str(), 500000), + ], + ), + 1, + "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d".to_string(), + 1, + vec![] + )); + + let utxos_keys = context + .get_utxos_paged() + .await + .unwrap() + .keys() + .cloned() + .collect::>(); + let first_utxo = utxos_keys[0].split('@').collect::>(); + + // Withdrawal with Orchard bundle and change output + let utxo_value = 500000u128; + let withdraw_amount = 200000u128; + let btc_gas_fee = 10000u128; + let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); + let orchard_amount = withdraw_amount - btc_gas_fee - withdraw_fee; + let change_amount = utxo_value - orchard_amount as u128 - btc_gas_fee; + + // Generate bundle for recipient A (using spending key [1u8; 32]) + let (recipient_a, bundle_a) = gen_bundle_with_key(orchard_amount as u64, [1u8; 32]); + + // Generate bundle for recipient B (using spending key [2u8; 32]) + let (recipient_b, _bundle_b) = gen_bundle_with_key(orchard_amount as u64, [2u8; 32]); + + println!("Recipient A: {}", recipient_a); + println!("Recipient B: {}", recipient_b); + assert_ne!( + recipient_a, recipient_b, + "Recipients should be different with different spending keys" + ); + + // Get change address and parse it for Zcash + let withdraw_change_address = context.get_change_address().await.unwrap(); + let change_script_pubkey = Address::parse(&withdraw_change_address, Chain::ZcashTestnet) + .expect("Invalid change address") + .script_pubkey() + .expect("Failed to get script pubkey"); + + // This should fail: use bundle_a but claim it's for recipient_b + let result = context + .do_withdraw( + "alice", + "bridge", + withdraw_amount, + TokenReceiverMessage::Withdraw { + target_btc_address: recipient_b, // Wrong recipient! + input: vec![OutPoint { + txid: first_utxo[0].parse().unwrap(), + vout: first_utxo[1].parse().unwrap(), + }], + output: vec![TxOut { + value: Amount::from_sat(change_amount as u64), + script_pubkey: change_script_pubkey, + }], + max_gas_fee: None, + chain_specific_data: Some(ChainSpecificData { + orchard_bundle_bytes: hex::decode(&bundle_a).unwrap().into(), // Bundle for recipient A + expiry_height: 10000, + }), + }, + ) + .await; + + // Verify the error message + let err_msg = tool_err_msg(&result); + assert!( + err_msg.contains("Orchard bundle validation failed"), + "Expected 'Orchard bundle validation failed' error, got: {}", + err_msg + ); +} + +/// Test: Missing Orchard bundle when no transparent outputs are provided +/// +/// Should reject with "empty output" error when neither transparent outputs +/// nor Orchard bundle is provided. +#[tokio::test] +#[cfg(feature = "zcash")] +async fn test_orchard_missing_bundle() { + // Set chain to ZcashTestnet for this test + std::env::set_var("TEST_CHAIN", "ZcashTestnet"); + + let worker = near_workspaces::sandbox().await.unwrap(); + let context = Context::new(&worker, None).await; + + check!(context.set_deposit_bridge_fee(10000, 0, 9000)); + check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); + + let _config = context.get_bridge_config().await.unwrap(); + + // Setup: Deposit for alice + let alice_btc_deposit_address = context + .get_user_deposit_address(DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }) + .await + .unwrap(); + + check!(context.verify_deposit( + "relayer", + DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }, + generate_transaction_bytes( + vec![( + "c6774e76452c36bba6c357653f620a4364fc063ba021e2acf6049f8d9e6b0234", + 1, + None, + )], + vec![ + ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (alice_btc_deposit_address.as_str(), 500000), + ], + ), + 1, + "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d".to_string(), + 1, + vec![] + )); + + let utxos_keys = context + .get_utxos_paged() + .await + .unwrap() + .keys() + .cloned() + .collect::>(); + let first_utxo = utxos_keys[0].split('@').collect::>(); + + let withdraw_amount = 200000u128; + + // Generate a Unified Address (but don't provide a bundle) + let (unified_address, _bundle) = get_or_gen_bundle(100000); // Just get a UA, ignore bundle + + // This should FAIL: no outputs and no Orchard bundle + let result = context + .do_withdraw( + "alice", + "bridge", + withdraw_amount, + TokenReceiverMessage::Withdraw { + target_btc_address: unified_address, // UA provided + input: vec![OutPoint { + txid: first_utxo[0].parse().unwrap(), + vout: first_utxo[1].parse().unwrap(), + }], + output: vec![], + max_gas_fee: None, + chain_specific_data: None, // No bundle provided + }, + ) + .await; + + // Verify the error message - contract requires either outputs or orchard bundle + let err_msg = tool_err_msg(&result); + assert!( + err_msg.contains("empty output"), + "Expected 'empty output' error when no bundle and no outputs provided, got: {}", + err_msg + ); + + println!("✓ Missing bundle with empty outputs correctly rejected"); +} + +/// Test: Verify the generated Zcash transaction includes the Orchard bundle +#[tokio::test] +#[cfg(feature = "zcash")] +async fn test_orchard_bundle_in_zcash_tx() { + // Set chain to ZcashTestnet for this test + std::env::set_var("TEST_CHAIN", "ZcashTestnet"); + + let worker = near_workspaces::sandbox().await.unwrap(); + let context = Context::new(&worker, None).await; + + check!(context.set_deposit_bridge_fee(10000, 0, 9000)); + check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); + + let config = context.get_bridge_config().await.unwrap(); + + // Setup: Deposit for alice + let alice_btc_deposit_address = context + .get_user_deposit_address(DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }) + .await + .unwrap(); + + check!(context.verify_deposit( + "relayer", + DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }, + generate_transaction_bytes( + vec![( + "c6774e76452c36bba6c357653f620a4364fc063ba021e2acf6049f8d9e6b0234", + 1, + None, + )], + vec![ + ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (alice_btc_deposit_address.as_str(), 500000), + ], + ), + 1, + "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d".to_string(), + 1, + vec![] + )); + + let utxos_keys = context + .get_utxos_paged() + .await + .unwrap() + .keys() + .cloned() + .collect::>(); + let first_utxo = utxos_keys[0].split('@').collect::>(); + + // Withdrawal with Orchard bundle and change output + let utxo_value = 500000u128; + let withdraw_amount = 200000u128; + let btc_gas_fee = 10000u128; + let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); + let orchard_amount = withdraw_amount - btc_gas_fee - withdraw_fee; + let change_amount = utxo_value - orchard_amount as u128 - btc_gas_fee; + + let (recipient_ua, bundle_hex) = get_or_gen_bundle(orchard_amount as u64); + + // Get change address and parse it for Zcash + let withdraw_change_address = context.get_change_address().await.unwrap(); + let change_script_pubkey = Address::parse(&withdraw_change_address, Chain::ZcashTestnet) + .expect("Invalid change address") + .script_pubkey() + .expect("Failed to get script pubkey"); + + check!(print "Withdrawal" context.do_withdraw( + "alice", + "bridge", + withdraw_amount, + TokenReceiverMessage::Withdraw { + target_btc_address: recipient_ua, + input: vec![OutPoint { + txid: first_utxo[0].parse().unwrap(), + vout: first_utxo[1].parse().unwrap(), + }], + output: vec![TxOut { + value: Amount::from_sat(change_amount as u64), + script_pubkey: change_script_pubkey, + }], + max_gas_fee: None, + chain_specific_data: Some(ChainSpecificData { + orchard_bundle_bytes: hex::decode(&bundle_hex).unwrap().into(), + expiry_height: 10000, + }), + } + )); + + let btc_pending_sign_txs = context + .get_btc_pending_infos_paged() + .await + .unwrap() + .keys() + .cloned() + .collect::>(); + + println!("Pending transactions: {:?}", btc_pending_sign_txs); + assert!( + !btc_pending_sign_txs.is_empty(), + "Should have pending transactions" + ); + + check!(print "Signing" context.sign_btc_transaction("relayer", &btc_pending_sign_txs[0], 0, 0)); + + // Fetch the pending info and check the transaction bytes + let pending_infos = context.get_btc_pending_infos_paged().await.unwrap(); + + let pending_info = pending_infos + .get(&btc_pending_sign_txs[0]) + .expect("Pending info not found"); + + // The tx_bytes_with_sign should contain the Orchard bundle + if let Some(tx_bytes) = &pending_info.tx_bytes_with_sign { + let tx_hex = hex::encode(tx_bytes); + + // The bundle hex should appear somewhere in the transaction bytes + // (It won't be exact match due to the transaction wrapper, but the bundle data should be there) + println!("Transaction hex length: {}", tx_hex.len()); + println!("Bundle hex length: {}", bundle_hex.len()); + + // At minimum, verify the transaction is longer than just transparent data + // A v5 Zcash transaction with Orchard should be significantly larger + assert!( + tx_hex.len() > 1000, + "Transaction should include Orchard bundle (tx_len={})", + tx_hex.len() + ); + + println!("✓ Zcash transaction includes Orchard data"); + } else { + panic!("No transaction bytes found after signing"); + } +} diff --git a/contracts/satoshi-bridge/tests/test_orchard_withdrawal.rs b/contracts/satoshi-bridge/tests/test_orchard_withdrawal.rs new file mode 100644 index 00000000..618c29ad --- /dev/null +++ b/contracts/satoshi-bridge/tests/test_orchard_withdrawal.rs @@ -0,0 +1,285 @@ +mod setup; +use setup::*; + +#[cfg(feature = "zcash")] +use bitcoin::{Amount, TxOut}; +#[cfg(feature = "zcash")] +use satoshi_bridge::network::{Address, Chain}; + +#[tokio::test] +#[cfg(feature = "zcash")] +async fn test_orchard_withdrawal_with_ovk_validation() { + // Set chain to ZcashTestnet for this test + std::env::set_var("TEST_CHAIN", "ZcashTestnet"); + + let worker = near_workspaces::sandbox().await.unwrap(); + let context = Context::new(&worker, None).await; + + // Setup bridge fees + check!(context.set_deposit_bridge_fee(10000, 0, 9000)); + check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); + + let config = context.get_bridge_config().await.unwrap(); + + // Verify we're on Zcash chain + println!("Testing on chain: {:?}", config.chain); + assert_eq!(format!("{:?}", config.chain), "ZcashTestnet"); + + // Deposit for alice using Zcash transaction format + let alice_btc_deposit_address = context + .get_user_deposit_address(DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }) + .await + .unwrap(); + + println!("Alice deposit address: {}", alice_btc_deposit_address); + + // Generate Zcash v5 transaction for deposit + let zcash_tx_bytes = setup::utils::generate_transaction_bytes( + vec![( + "c6774e76452c36bba6c357653f620a4364fc063ba021e2acf6049f8d9e6b0234", + 1, + None, + )], + vec![ + ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (alice_btc_deposit_address.as_str(), 500000), + ], + ); + + println!( + "Generated Zcash transaction, size: {} bytes", + zcash_tx_bytes.len() + ); + + // Verify the deposit using Zcash transaction + check!(context.verify_deposit( + "relayer", + DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }, + zcash_tx_bytes, + 1, + "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d".to_string(), + 1, + vec![] + )); + + println!("✅ Deposit verified successfully"); + + let utxos_keys = context + .get_utxos_paged() + .await + .unwrap() + .keys() + .cloned() + .collect::>(); + + assert!(!utxos_keys.is_empty(), "Should have UTXOs after deposit"); + println!("UTXOs: {:?}", utxos_keys); + + let first_utxo = utxos_keys[0].split('@').collect::>(); + + // Withdrawal with Orchard bundle and change output + let utxo_value = 500000u128; + let withdraw_amount = 200000u128; + let btc_gas_fee = 10000u128; + let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); + let orchard_amount = (withdraw_amount - withdraw_fee - btc_gas_fee) as u64; + let change_amount = utxo_value - orchard_amount as u128 - btc_gas_fee; + + println!( + "Withdraw fee: {}, BTC gas fee: {}", + withdraw_fee, btc_gas_fee + ); + + println!( + "Withdraw amount: {}, Orchard amount: {}, Change amount: {}", + withdraw_amount, orchard_amount, change_amount + ); + + // Generate Orchard bundle with correct amount + let (recipient_ua, bundle_hex) = setup::orchard::get_or_gen_bundle(orchard_amount); + println!("Generated Orchard bundle for recipient: {}", recipient_ua); + println!("Bundle size: {} bytes", bundle_hex.len() / 2); + + // Get change address and parse it for Zcash + let withdraw_change_address = context.get_change_address().await.unwrap(); + let change_script_pubkey = Address::parse(&withdraw_change_address, Chain::ZcashTestnet) + .expect("Invalid change address") + .script_pubkey() + .expect("Failed to get script pubkey"); + + // Perform Orchard withdrawal with change output + check!(context.do_withdraw( + "alice", + "bridge", + withdraw_amount, + TokenReceiverMessage::Withdraw { + target_btc_address: recipient_ua.clone(), + input: vec![OutPoint { + txid: first_utxo[0].parse().unwrap(), + vout: first_utxo[1].parse().unwrap(), + }], + output: vec![TxOut { + value: Amount::from_sat(change_amount as u64), + script_pubkey: change_script_pubkey, + }], + max_gas_fee: None, + chain_specific_data: Some(ChainSpecificData { + orchard_bundle_bytes: hex::decode(&bundle_hex).unwrap().into(), + expiry_height: 10000, + }), + } + )); + + println!("✅ Withdrawal with Orchard bundle completed successfully"); +} + +#[tokio::test] +#[cfg(feature = "zcash")] +async fn test_orchard_withdrawal_amount_mismatch() { + // Set chain to ZcashTestnet for this test + std::env::set_var("TEST_CHAIN", "ZcashTestnet"); + + let worker = near_workspaces::sandbox().await.unwrap(); + let context = Context::new(&worker, None).await; + + check!(context.set_deposit_bridge_fee(10000, 0, 9000)); + check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); + + let config = context.get_bridge_config().await.unwrap(); + + // Deposit for alice using Zcash transaction format + let alice_btc_deposit_address = context + .get_user_deposit_address(DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }) + .await + .unwrap(); + + // Generate Zcash v5 transaction for deposit + let zcash_tx_bytes = setup::utils::generate_transaction_bytes( + vec![( + "c6774e76452c36bba6c357653f620a4364fc063ba021e2acf6049f8d9e6b0234", + 1, + None, + )], + vec![ + ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (alice_btc_deposit_address.as_str(), 500000), + ], + ); + + check!(context.verify_deposit( + "relayer", + DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }, + zcash_tx_bytes, + 1, + "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d".to_string(), + 1, + vec![] + )); + + let utxos_keys = context + .get_utxos_paged() + .await + .unwrap() + .keys() + .cloned() + .collect::>(); + let first_utxo = utxos_keys[0].split('@').collect::>(); + + // Withdrawal with Orchard bundle and change output - test amount mismatch + let utxo_value = 500000u128; + let withdraw_amount = 200000u128; + let btc_gas_fee = 10000u128; + let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); + + // Calculate expected orchard amount + let expected_orchard_amount = (withdraw_amount - withdraw_fee - btc_gas_fee) as u64; + + // Generate bundle with WRONG amount (different from what we're withdrawing) + let wrong_amount = 100000u64; // Different from expected_orchard_amount + let (recipient_ua, bundle_hex) = get_or_gen_bundle(wrong_amount); + + // Change amount must be calculated based on the ACTUAL orchard amount in the bundle + // to ensure gas_fee stays within valid range + let change_amount = utxo_value - wrong_amount as u128 - btc_gas_fee; + + println!( + "Expected orchard amount: {}, Using wrong amount: {}", + expected_orchard_amount, wrong_amount + ); + + // Get change address and parse it for Zcash + let withdraw_change_address = context.get_change_address().await.unwrap(); + let change_script_pubkey = Address::parse(&withdraw_change_address, Chain::ZcashTestnet) + .expect("Invalid change address") + .script_pubkey() + .expect("Failed to get script pubkey"); + + // This should fail with "Orchard amount mismatch" because the bundle has wrong_amount + // but we're withdrawing expected_orchard_amount (the contract expects the orchard amount + // to match withdraw_amount - withdraw_fee - gas_fee) + let result = context + .do_withdraw( + "alice", + "bridge", + withdraw_amount, + TokenReceiverMessage::Withdraw { + target_btc_address: recipient_ua, // Use the bundle's actual recipient + input: vec![OutPoint { + txid: first_utxo[0].parse().unwrap(), + vout: first_utxo[1].parse().unwrap(), + }], + output: vec![TxOut { + value: Amount::from_sat(change_amount as u64), + script_pubkey: change_script_pubkey, + }], + max_gas_fee: None, + chain_specific_data: Some(ChainSpecificData { + orchard_bundle_bytes: hex::decode(&bundle_hex).unwrap().into(), + expiry_height: 10000, + }), + }, + ) + .await; + + // Check that it failed with the expected error + assert!( + result.is_ok(), + "Withdrawal call should not fail at network level" + ); + let outcome = result.unwrap(); + assert!( + !outcome.is_success() || !outcome.receipt_failures().is_empty(), + "Withdrawal should fail due to Orchard validation" + ); + + // Check the error message - the contract validates that the user's output amount + // matches the expected range based on withdraw_amount - withdraw_fee - gas_fee + let err_msg = setup::utils::tool_err_msg(&Ok(outcome)); + assert!( + err_msg.contains("output amount") && err_msg.contains("out of the valid range"), + "Expected output amount validation error, got: {}", + err_msg + ); + + println!("✓ Orchard amount mismatch correctly rejected"); +} diff --git a/contracts/satoshi-bridge/tests/test_satoshi_bridge.rs b/contracts/satoshi-bridge/tests/test_satoshi_bridge.rs index 8dedc5be..256ab58c 100644 --- a/contracts/satoshi-bridge/tests/test_satoshi_bridge.rs +++ b/contracts/satoshi-bridge/tests/test_satoshi_bridge.rs @@ -1,20 +1,37 @@ mod setup; -use std::str::FromStr; - -use bitcoin::{Address, Amount, OutPoint, TxOut}; -use near_sdk::{AccountId, Gas}; +use bitcoin::{Amount, OutPoint, TxOut}; +use near_sdk::{AccountId, Gas, NearToken}; +use satoshi_bridge::network::{Address, Chain}; use satoshi_bridge::{DepositMsg, PendingInfoState, PostAction, TokenReceiverMessage}; use setup::*; +use std::string::ToString; + +#[cfg(feature = "zcash")] +const CHAIN: &str = "ZcashTestnet"; +#[cfg(not(feature = "zcash"))] +const CHAIN: &str = "BitcoinMainnet"; + +#[cfg(feature = "zcash")] +const TARGET_ADDRESS: &str = "tmD67UTsZ4iBbhCae4D43k1x8fhFNhwd4Jn"; +#[cfg(not(feature = "zcash"))] +const TARGET_ADDRESS: &str = "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ"; + +fn get_chain() -> Chain { + match CHAIN { + "ZcashTestnet" => Chain::ZcashTestnet, + _ => Chain::BitcoinMainnet, + } +} #[tokio::test] async fn test_role() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; assert_eq!( context.get_metadata().await.unwrap().super_admins, vec!["test.near".parse::().unwrap()] ); - check!(print context.bridge_add_super_admin("root", context.get_account_by_name("alice").id())); + check!(print context.bridge_add_super_admin("root", &context.get_account_by_name("alice").sdk_id())); assert_eq!( context.get_metadata().await.unwrap().super_admins, vec![ @@ -22,17 +39,17 @@ async fn test_role() { "alice.test.near".parse::().unwrap() ] ); - check!(print context.bridge_remove_super_admin("alice", context.get_account_by_name("root").id())); + check!(print context.bridge_remove_super_admin("alice", &context.get_account_by_name("root").sdk_id())); assert_eq!( context.get_metadata().await.unwrap().super_admins, vec!["alice.test.near".parse::().unwrap()] ); check!( - context.bridge_add_super_admin("root", context.get_account_by_name("alice").id()), + context.bridge_add_super_admin("root", &context.get_account_by_name("alice").sdk_id()), "Insufficient permissions" ); check!( - context.bridge_remove_super_admin("alice", context.get_account_by_name("alice").id()), + context.bridge_remove_super_admin("alice", &context.get_account_by_name("alice").sdk_id()), "cannot remove oneself" ); assert_eq!( @@ -88,22 +105,24 @@ async fn test_role() { #[tokio::test] async fn test_base() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; let config = context.get_bridge_config().await.unwrap(); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); let bob_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("bob").id().clone(), + recipient_id: context.get_account_by_name("bob").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -111,9 +130,10 @@ async fn test_base() { check!(printr "alice 10000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -123,7 +143,7 @@ async fn test_base() { )], vec![ (alice_btc_deposit_address.as_str(), 10000), - ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000) + (TARGET_ADDRESS, 90000) ], ), 0, @@ -158,9 +178,10 @@ async fn test_base() { check!(printr "alice 50000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -169,7 +190,7 @@ async fn test_base() { None, ),], vec![ - ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (TARGET_ADDRESS, 90000), (alice_btc_deposit_address.as_str(), 50000), ], ), @@ -206,9 +227,10 @@ async fn test_base() { context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -217,7 +239,7 @@ async fn test_base() { None, ),], vec![ - ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (TARGET_ADDRESS, 90000), (alice_btc_deposit_address.as_str(), 50000), ], ), @@ -238,9 +260,10 @@ async fn test_base() { check!(context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("bob").id().clone(), + recipient_id: context.get_account_by_name("bob").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -250,7 +273,7 @@ async fn test_base() { ),], vec![ (bob_btc_deposit_address.as_str(), 200000), - ("1F3HTDzfWnPPbBaUrxg99LJEjHQd4NsisC", 50000), + (TARGET_ADDRESS, 50000), ], ), 0, @@ -291,11 +314,11 @@ async fn test_base() { let first_utxo = utxos_keys[0].split('@').collect::>(); let second_utxo = utxos_keys[1].split('@').collect::>(); let withdraw_amount = 110000; - let btc_gas_fee = 10000; + let btc_gas_fee = 25000; let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); let total_change_amount = 250000 - (withdraw_amount - withdraw_fee) as u64; check!(print context.do_withdraw("alice", "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![ OutPoint { txid: first_utxo[0].parse().unwrap(), @@ -307,30 +330,32 @@ async fn test_base() { }], output: vec![TxOut { value: Amount::from_sat((withdraw_amount - btc_gas_fee - withdraw_fee) as u64),// 50000 - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(total_change_amount / 4), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(total_change_amount / 4), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(total_change_amount / 4), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(total_change_amount / 4 + total_change_amount % 4), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") }], + max_gas_fee: None, + chain_specific_data: None, })); assert_eq!( @@ -447,16 +472,17 @@ async fn test_base() { #[tokio::test] async fn test_fix_bridge_fee_and_relayer() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(10000, 0, 9000)); check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); let config = context.get_bridge_config().await.unwrap(); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -464,9 +490,10 @@ async fn test_fix_bridge_fee_and_relayer() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -505,22 +532,24 @@ async fn test_fix_bridge_fee_and_relayer() { let btc_gas_fee = 10000; let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); check!(print "do_withdraw" context.do_withdraw("alice", "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: first_utxo[0].parse().unwrap(), vout: first_utxo[1].parse().unwrap(), }], output: vec![TxOut { value: Amount::from_sat((withdraw_amount - btc_gas_fee - withdraw_fee) as u64),// 50000 - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(320000), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") }], + max_gas_fee: None, + chain_specific_data: None, })); let btc_pending_sign_txs = context .get_btc_pending_infos_paged() @@ -583,16 +612,17 @@ async fn test_fix_bridge_fee_and_relayer() { #[tokio::test] async fn test_ratio_bridge_fee_and_relayer() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(0, 1000, 9000)); check!(context.set_withdraw_bridge_fee(0, 2000, 9000)); let config = context.get_bridge_config().await.unwrap(); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -600,9 +630,10 @@ async fn test_ratio_bridge_fee_and_relayer() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -641,22 +672,24 @@ async fn test_ratio_bridge_fee_and_relayer() { let btc_gas_fee = 10000; let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); check!(print "do_withdraw" context.do_withdraw("alice", "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: first_utxo[0].parse().unwrap(), vout: first_utxo[1].parse().unwrap(), }], output: vec![TxOut { value: Amount::from_sat((withdraw_amount - btc_gas_fee - withdraw_fee) as u64),// 50000 - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(500000 - (withdraw_amount - withdraw_fee) as u64), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") }], + max_gas_fee: None, + chain_specific_data: None, })); let btc_pending_sign_txs = context .get_btc_pending_infos_paged() @@ -722,16 +755,17 @@ async fn test_ratio_bridge_fee_and_relayer() { #[tokio::test] async fn test_directly_withdraw() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(10000, 0, 9000)); check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); let config = context.get_bridge_config().await.unwrap(); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -739,9 +773,10 @@ async fn test_directly_withdraw() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -786,22 +821,24 @@ async fn test_directly_withdraw() { let btc_gas_fee = 10000; let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); check!(print "do_withdraw" context.do_withdraw("bob", "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: first_utxo[0].parse().unwrap(), vout: first_utxo[1].parse().unwrap(), }], output: vec![TxOut { value: Amount::from_sat((withdraw_amount - btc_gas_fee - withdraw_fee) as u64),// 50000 - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(320000), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") }], + max_gas_fee: None, + chain_specific_data: None, })); let btc_pending_sign_txs = context .get_btc_pending_infos_paged() @@ -842,21 +879,22 @@ async fn test_directly_withdraw() { #[tokio::test] async fn test_one_click() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(10000, 0, 9000)); let mut times = 0; { // dapp not in post_action_receiver_id_white_list let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), gas: Some(Gas::from_tgas(100)), }]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -904,19 +942,19 @@ async fn test_one_click() { check!( context.extend_post_action_receiver_id_white_list(vec![context .get_account_by_name("dapp") - .id() - .clone()]) + .sdk_id()]) ); let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), gas: Some(Gas::from_tgas(100)), }]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -964,15 +1002,16 @@ async fn test_one_click() { { // PostAction gas too large let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), gas: Some(Gas::from_tgas(101)), }]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -1020,17 +1059,17 @@ async fn test_one_click() { { // PostAction total gas too large let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![ PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), gas: Some(Gas::from_tgas(100)), }, PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), @@ -1038,6 +1077,7 @@ async fn test_one_click() { }, ]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -1085,24 +1125,24 @@ async fn test_one_click() { { // PostAction > 2 let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![ PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), gas: None, }, PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), gas: None, }, PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 5000.into(), memo: None, msg: "".to_string(), @@ -1110,6 +1150,7 @@ async fn test_one_click() { }, ]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -1157,15 +1198,16 @@ async fn test_one_click() { { // amount > current deposit let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 500000.into(), memo: None, msg: "".to_string(), gas: None, }]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -1213,17 +1255,17 @@ async fn test_one_click() { { // The user is not registered with the dapp let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![ PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 20000.into(), memo: None, msg: "".to_string(), gas: Some(Gas::from_tgas(50)), }, PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 20000.into(), memo: None, msg: "".to_string(), @@ -1231,6 +1273,7 @@ async fn test_one_click() { }, ]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -1279,17 +1322,17 @@ async fn test_one_click() { check!(context.storage_deposit("nbtc", "dapp")); check!(context.storage_deposit("dapp", "alice")); let deposit_msg = DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: Some(vec![ PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 20000.into(), memo: None, msg: "".to_string(), gas: Some(Gas::from_tgas(100)), }, PostAction { - receiver_id: context.get_account_by_name("dapp").id().clone(), + receiver_id: context.get_account_by_name("dapp").sdk_id(), amount: 20000.into(), memo: None, msg: "".to_string(), @@ -1297,6 +1340,7 @@ async fn test_one_click() { }, ]), extra_msg: None, + safe_deposit: None, }; let alice_btc_deposit_address = context .get_user_deposit_address(deposit_msg.clone()) @@ -1346,7 +1390,7 @@ async fn test_one_click() { #[tokio::test] async fn test_utxo_passive_management() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(0, 0, 9000)); check!(context.set_withdraw_bridge_fee(0, 0, 9000)); // The bridge deposit fee is 0, so the bridge will not be automatically registered with mint @@ -1355,9 +1399,10 @@ async fn test_utxo_passive_management() { let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -1366,9 +1411,10 @@ async fn test_utxo_passive_management() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -1389,9 +1435,10 @@ async fn test_utxo_passive_management() { check!(printr "alice 60000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -1425,7 +1472,7 @@ async fn test_utxo_passive_management() { .clone(); let utxo60000 = utxo_key60000.split('@').collect::>(); let withdraw_amount = 200000; - let btc_gas_fee = 10000; + let btc_gas_fee = 15000; let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); check!(context.set_passive_management_limit(3, 10)); check!( @@ -1434,7 +1481,7 @@ async fn test_utxo_passive_management() { "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: utxo500000[0].parse().unwrap(), vout: utxo500000[1].parse().unwrap(), @@ -1444,19 +1491,24 @@ async fn test_utxo_passive_management() { value: Amount::from_sat( (withdraw_amount - btc_gas_fee - withdraw_fee) as u64 ), - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked() .script_pubkey() + .expect("Failed to get script pubkey") }, TxOut { value: Amount::from_sat(500000 - (withdraw_amount - withdraw_fee) as u64), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) - .expect("Invalid btc address") - .assume_checked() - .script_pubkey() + script_pubkey: Address::parse( + withdraw_change_address.as_str(), + get_chain() + ) + .expect("Invalid btc address") + .script_pubkey() + .expect("Failed to get script pubkey") } ], + max_gas_fee: None, + chain_specific_data: None, } ), "require input_num < change_num" @@ -1469,7 +1521,7 @@ async fn test_utxo_passive_management() { "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: utxo500000[0].parse().unwrap(), vout: utxo500000[1].parse().unwrap(), @@ -1479,26 +1531,34 @@ async fn test_utxo_passive_management() { value: Amount::from_sat( (withdraw_amount - btc_gas_fee - withdraw_fee) as u64 ), - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked() .script_pubkey() + .expect("Failed to get script pubkey") }, TxOut { value: Amount::from_sat(total_change / 2), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) - .expect("Invalid btc address") - .assume_checked() - .script_pubkey() + script_pubkey: Address::parse( + withdraw_change_address.as_str(), + get_chain() + ) + .expect("Invalid btc address") + .script_pubkey() + .expect("Failed to get script pubkey") }, TxOut { value: Amount::from_sat(total_change / 2 + total_change % 2), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) - .expect("Invalid btc address") - .assume_checked() - .script_pubkey() + script_pubkey: Address::parse( + withdraw_change_address.as_str(), + get_chain() + ) + .expect("Invalid btc address") + .script_pubkey() + .expect("Failed to get script pubkey") } ], + max_gas_fee: None, + chain_specific_data: None, } ), "require input_num > change_num" @@ -1511,7 +1571,7 @@ async fn test_utxo_passive_management() { "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![ OutPoint { txid: utxo500000[0].parse().unwrap(), @@ -1527,19 +1587,24 @@ async fn test_utxo_passive_management() { value: Amount::from_sat( (withdraw_amount - btc_gas_fee - withdraw_fee) as u64 ), - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked() .script_pubkey() + .expect("Failed to get script pubkey") }, TxOut { value: Amount::from_sat(total_change), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) - .expect("Invalid btc address") - .assume_checked() - .script_pubkey() + script_pubkey: Address::parse( + withdraw_change_address.as_str(), + get_chain() + ) + .expect("Invalid btc address") + .script_pubkey() + .expect("Failed to get script pubkey") } ], + max_gas_fee: None, + chain_specific_data: None, } ), "The change amount must be less than all inputs" @@ -1549,16 +1614,17 @@ async fn test_utxo_passive_management() { #[tokio::test] async fn test_cancel_withdraw() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(10000, 0, 9000)); check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); let config = context.get_bridge_config().await.unwrap(); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -1566,9 +1632,10 @@ async fn test_cancel_withdraw() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -1608,22 +1675,24 @@ async fn test_cancel_withdraw() { let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); let change_amount = 500000 - (withdraw_amount - withdraw_fee) as u64; check!(print "do_withdraw" context.do_withdraw("alice", "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: first_utxo[0].parse().unwrap(), vout: first_utxo[1].parse().unwrap(), }], output: vec![TxOut { value: Amount::from_sat((withdraw_amount - btc_gas_fee - withdraw_fee) as u64),// 50000 - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(change_amount), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") }], + max_gas_fee: None, + chain_specific_data: None, })); let btc_pending_sign_txs = context @@ -1649,9 +1718,10 @@ async fn test_cancel_withdraw() { vec![ generate_tx_out( (withdraw_amount - btc_gas_fee - withdraw_fee) as u64, - "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ" + TARGET_ADDRESS, + get_chain() ), - generate_tx_out(change_amount, withdraw_change_address.as_str()), + generate_tx_out(change_amount, withdraw_change_address.as_str(), get_chain()), ] ), "Please wait user rbf" @@ -1665,23 +1735,26 @@ async fn test_cancel_withdraw() { vec![ generate_tx_out( (withdraw_amount - btc_gas_fee - withdraw_fee) as u64, - "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ" + TARGET_ADDRESS, + get_chain() ), - generate_tx_out(change_amount, withdraw_change_address.as_str()), + generate_tx_out(change_amount, withdraw_change_address.as_str(), get_chain()), ] ), "Invalid output script_pubkey" ); + #[cfg(not(feature = "zcash"))] check!( context.cancel_withdraw( &original_btc_pending_verify_id, vec![ generate_tx_out( (withdraw_amount - btc_gas_fee - withdraw_fee) as u64, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), + get_chain() ), - generate_tx_out(change_amount, withdraw_change_address.as_str()), + generate_tx_out(change_amount, withdraw_change_address.as_str(), get_chain()), ] ), "No gas increase." @@ -1694,9 +1767,9 @@ async fn test_cancel_withdraw() { vec![ generate_tx_out( (withdraw_amount - new_btc_gas_fee - withdraw_fee) as u64, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), get_chain() ), - generate_tx_out(change_amount, withdraw_change_address.as_str()), + generate_tx_out(change_amount, withdraw_change_address.as_str(), get_chain()), ] ) ); @@ -1776,16 +1849,17 @@ async fn test_cancel_withdraw() { #[tokio::test] async fn test_cancel_withdraw2() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(10000, 0, 9000)); check!(context.set_withdraw_bridge_fee(20000, 0, 9000)); let config = context.get_bridge_config().await.unwrap(); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -1793,9 +1867,10 @@ async fn test_cancel_withdraw2() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -1835,22 +1910,24 @@ async fn test_cancel_withdraw2() { let withdraw_fee = config.withdraw_bridge_fee.get_fee(withdraw_amount); let change_amount = 500000 - (withdraw_amount - withdraw_fee) as u64; check!(print "do_withdraw" context.do_withdraw("alice", "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: first_utxo[0].parse().unwrap(), vout: first_utxo[1].parse().unwrap(), }], output: vec![TxOut { value: Amount::from_sat((withdraw_amount - btc_gas_fee - withdraw_fee) as u64),// 50000 - script_pubkey: Address::from_str("1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ") + script_pubkey: Address::parse(TARGET_ADDRESS, get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") },TxOut { value: Amount::from_sat(change_amount), - script_pubkey: Address::from_str(withdraw_change_address.as_str()) + script_pubkey: Address::parse(withdraw_change_address.as_str(), get_chain()) .expect("Invalid btc address") - .assume_checked().script_pubkey() + .script_pubkey().expect("Failed to get script pubkey") }], + max_gas_fee: None, + chain_specific_data: None, })); let btc_pending_sign_txs = context @@ -1874,7 +1951,7 @@ async fn test_cancel_withdraw2() { // (withdraw_amount - new_btc_gas_fee - withdraw_fee) as u64, // withdraw_change_address.as_str() // ), - generate_tx_out(change_amount - 111, withdraw_change_address.as_str()), + generate_tx_out(change_amount - 111, withdraw_change_address.as_str(), get_chain()), ] ) ); @@ -1956,16 +2033,17 @@ async fn test_cancel_withdraw2() { #[tokio::test] async fn test_utxo_active_management() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(10000, 0, 10000)); // The bridge deposit fee is 0, so the bridge will not be automatically registered with mint check!(context.storage_deposit("nbtc", "bridge")); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -1974,9 +2052,10 @@ async fn test_utxo_active_management() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -1997,9 +2076,10 @@ async fn test_utxo_active_management() { check!(printr "alice 60000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -2049,8 +2129,8 @@ async fn test_utxo_active_management() { } ], vec![ - generate_tx_out(output_amount, "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ"), - generate_tx_out(output_amount, withdraw_change_address.as_str()), + generate_tx_out(output_amount, TARGET_ADDRESS, get_chain()), + generate_tx_out(output_amount, withdraw_change_address.as_str(), get_chain()), ] ), "Active management conditions are not met" @@ -2069,8 +2149,8 @@ async fn test_utxo_active_management() { } ], vec![ - generate_tx_out(output_amount, "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ"), - generate_tx_out(output_amount, withdraw_change_address.as_str()), + generate_tx_out(output_amount, TARGET_ADDRESS, get_chain()), + generate_tx_out(output_amount, withdraw_change_address.as_str(), get_chain()), ] ), "require input_num < output_num" @@ -2089,8 +2169,8 @@ async fn test_utxo_active_management() { } ], vec![ - generate_tx_out(output_amount, "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ"), - generate_tx_out(output_amount, withdraw_change_address.as_str()), + generate_tx_out(output_amount, TARGET_ADDRESS, get_chain()), + generate_tx_out(output_amount, withdraw_change_address.as_str(), get_chain()), ] ), "require input_num > output_num" @@ -2109,7 +2189,8 @@ async fn test_utxo_active_management() { ], vec![generate_tx_out( output_amount * 2, - "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ" + TARGET_ADDRESS, + get_chain() ),] ), "Invalid output script_pubkey" @@ -2128,7 +2209,8 @@ async fn test_utxo_active_management() { ], vec![generate_tx_out( output_amount * 2 - 30000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), + get_chain() ),] ), "Insufficient protocol_fee" @@ -2153,7 +2235,7 @@ async fn test_utxo_active_management() { vec![ generate_tx_out( output_amount * 2 - 10000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), get_chain() ), ] ) @@ -2167,7 +2249,8 @@ async fn test_utxo_active_management() { original_btc_pending_verify_id, vec![generate_tx_out( output_amount * 2 - 10000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), + get_chain() ),] ), "No gas increase." @@ -2176,8 +2259,16 @@ async fn test_utxo_active_management() { context.active_utxo_management_rbf( original_btc_pending_verify_id, vec![ - generate_tx_out(output_amount - 10000, withdraw_change_address.as_str()), - generate_tx_out(output_amount - 10000, withdraw_change_address.as_str()), + generate_tx_out( + output_amount - 10000, + withdraw_change_address.as_str(), + get_chain() + ), + generate_tx_out( + output_amount - 10000, + withdraw_change_address.as_str(), + get_chain() + ), ] ), "Invalid output num" @@ -2187,7 +2278,8 @@ async fn test_utxo_active_management() { original_btc_pending_verify_id, vec![generate_tx_out( output_amount * 2 - 25000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), + get_chain() ),] ), "Insufficient protocol fee" @@ -2196,7 +2288,8 @@ async fn test_utxo_active_management() { original_btc_pending_verify_id, vec![generate_tx_out( output_amount * 2 - 15000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), + get_chain() ),] )); @@ -2219,7 +2312,8 @@ async fn test_utxo_active_management() { original_btc_pending_verify_id, vec![generate_tx_out( output_amount * 2 - 15000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), + get_chain() ),] ), "Please wait user rbf" @@ -2229,8 +2323,12 @@ async fn test_utxo_active_management() { context.cancel_active_utxo_management( original_btc_pending_verify_id, vec![ - generate_tx_out(output_amount - 15000, withdraw_change_address.as_str()), - generate_tx_out(output_amount, withdraw_change_address.as_str()), + generate_tx_out( + output_amount - 15000, + withdraw_change_address.as_str(), + get_chain() + ), + generate_tx_out(output_amount, withdraw_change_address.as_str(), get_chain()), ] ), "No gas increase." @@ -2241,11 +2339,11 @@ async fn test_utxo_active_management() { vec![ generate_tx_out( output_amount - 16000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), get_chain() ), generate_tx_out( output_amount, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), get_chain() ), ] ) @@ -2304,16 +2402,17 @@ async fn test_utxo_active_management() { #[tokio::test] async fn test_utxo_active_management2() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, Some(CHAIN.to_string())).await; check!(context.set_deposit_bridge_fee(10000, 0, 10000)); // The bridge deposit fee is 0, so the bridge will not be automatically registered with mint check!(context.storage_deposit("nbtc", "bridge")); let withdraw_change_address = context.get_change_address().await.unwrap(); let alice_btc_deposit_address = context .get_user_deposit_address(DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }) .await .unwrap(); @@ -2322,9 +2421,10 @@ async fn test_utxo_active_management2() { check!(printr "alice 500000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -2345,9 +2445,10 @@ async fn test_utxo_active_management2() { check!(printr "alice 60000" context.verify_deposit( "relayer", DepositMsg { - recipient_id: context.get_account_by_name("alice").id().clone(), + recipient_id: context.get_account_by_name("alice").sdk_id(), post_actions: None, extra_msg: None, + safe_deposit: None, }, generate_transaction_bytes( vec![( @@ -2405,7 +2506,7 @@ async fn test_utxo_active_management2() { vec![ generate_tx_out( output_amount * 2 - 10000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), get_chain() ), ] ) @@ -2418,7 +2519,8 @@ async fn test_utxo_active_management2() { original_btc_pending_verify_id, vec![generate_tx_out( output_amount * 2 - 15000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), + get_chain() ),] )); let btc_pending_verify_txs = context.get_btc_pending_infos_paged().await.unwrap(); @@ -2442,11 +2544,11 @@ async fn test_utxo_active_management2() { vec![ generate_tx_out( output_amount - 16000, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), get_chain() ), generate_tx_out( output_amount, - withdraw_change_address.as_str() + withdraw_change_address.as_str(), get_chain() ), ] ) @@ -2500,3 +2602,129 @@ async fn test_utxo_active_management2() { 560000 - 20000 ); } + +#[tokio::test] +async fn test_unauthorized_account_cannot_call_trusted_relayer_methods() { + let worker = near_workspaces::sandbox().await.unwrap(); + let context = Context::new(&worker, Some(CHAIN.to_string())).await; + + // Create a new account that does NOT receive the UnrestrictedRelayer role. + // Context::new only grants UnrestrictedRelayer to relayer, alice, bob, charlie, and tx_listener. + let unauthorized = worker.dev_create_account().await.unwrap(); + + let alice_btc_deposit_address = context + .get_user_deposit_address(DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }) + .await + .unwrap(); + + // verify_deposit should fail for an account without the trusted-relayer role + let outcome = unauthorized + .call(context.bridge_contract.id(), "verify_deposit") + .args_json(near_sdk::serde_json::json!({ + "deposit_msg": DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }, + "tx_bytes": generate_transaction_bytes( + vec![( + "c6774e76452c36bba6c357653f620a4364fc063ba021e2acf6049f8d9e6b0234", + 1, + None, + )], + vec![ + (alice_btc_deposit_address.as_str(), 50000), + (TARGET_ADDRESS, 90000), + ], + ), + "vout": 0u32, + "tx_block_blockhash": "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d", + "tx_index": 1u64, + "merkle_proof": Vec::::new(), + })) + .max_gas() + .transact() + .await; + assert!( + tool_err_msg(&outcome).contains("Relayer is not active"), + "verify_deposit should reject an account without trusted-relayer role" + ); + + // safe_verify_deposit should fail for an account without the trusted-relayer role + let outcome = unauthorized + .call(context.bridge_contract.id(), "safe_verify_deposit") + .args_json(near_sdk::serde_json::json!({ + "deposit_msg": DepositMsg { + recipient_id: context.get_account_by_name("alice").sdk_id(), + post_actions: None, + extra_msg: None, + safe_deposit: None, + }, + "tx_bytes": generate_transaction_bytes( + vec![( + "c6774e76452c36bba6c357653f620a4364fc063ba021e2acf6049f8d9e6b0234", + 1, + None, + )], + vec![ + (alice_btc_deposit_address.as_str(), 50000), + (TARGET_ADDRESS, 90000), + ], + ), + "vout": 0u32, + "tx_block_blockhash": "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d", + "tx_index": 1u64, + "merkle_proof": Vec::::new(), + })) + .max_gas() + .deposit(NearToken::from_near(1)) + .transact() + .await; + assert!( + tool_err_msg(&outcome).contains("Relayer is not active"), + "safe_verify_deposit should reject an account without trusted-relayer role" + ); + + // verify_withdraw should fail for an account without the trusted-relayer role + let outcome = unauthorized + .call(context.bridge_contract.id(), "verify_withdraw") + .args_json(near_sdk::serde_json::json!({ + "tx_id": "", + "tx_block_blockhash": "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d", + "tx_index": 1u64, + "merkle_proof": Vec::::new(), + })) + .max_gas() + .transact() + .await; + assert!( + tool_err_msg(&outcome).contains("Relayer is not active"), + "verify_withdraw should reject an account without trusted-relayer role" + ); + + // verify_active_utxo_management should fail for an account without the trusted-relayer role + let outcome = unauthorized + .call( + context.bridge_contract.id(), + "verify_active_utxo_management", + ) + .args_json(near_sdk::serde_json::json!({ + "tx_id": "", + "tx_block_blockhash": "0000000000000c3f818b0b6374c609dd8e548a0a9e61065e942cd466c426e00d", + "tx_index": 1u64, + "merkle_proof": Vec::::new(), + })) + .max_gas() + .transact() + .await; + assert!( + tool_err_msg(&outcome).contains("Relayer is not active"), + "verify_active_utxo_management should reject an account without trusted-relayer role" + ); +} diff --git a/contracts/satoshi-bridge/tests/test_upgrade.rs b/contracts/satoshi-bridge/tests/test_upgrade.rs index d24a8c1c..541771e0 100644 --- a/contracts/satoshi-bridge/tests/test_upgrade.rs +++ b/contracts/satoshi-bridge/tests/test_upgrade.rs @@ -2,18 +2,70 @@ mod setup; use setup::*; #[tokio::test] -async fn test_satoshi_bridge_upgrade() { +async fn test_btc_bridge_upgrade() { let worker = near_workspaces::sandbox().await.unwrap(); - let upgrade_context = UpgradeContext::new(&worker).await; + let upgrade_context = UpgradeContext::new( + &worker, + "../../res/bitcoin_bridge.wasm", + "../../res/nbtc.wasm", + ) + .await; check!(view upgrade_context.get_satoshi_bridge_version()); - check!(upgrade_context.upgrade_satoshi_bridge("../../res/satoshi_bridge.wasm")); + check!(upgrade_context.upgrade_satoshi_bridge("../../res/bitcoin_bridge.wasm")); + check!(view upgrade_context.get_satoshi_bridge_version()); +} + +#[tokio::test] +async fn test_btc_bridge_upgrade_from_v0_5_1() { + let worker = near_workspaces::sandbox().await.unwrap(); + let upgrade_context = UpgradeContext::new( + &worker, + "tests/data/btc_bridge_v0-5-1.wasm", + "tests/data/nbtc_v0-5-1.wasm", + ) + .await; + check!(view upgrade_context.get_satoshi_bridge_version()); + check!(upgrade_context.upgrade_satoshi_bridge("../../res/bitcoin_bridge.wasm")); + check!(view upgrade_context.get_satoshi_bridge_version()); +} + +#[tokio::test] +async fn test_zcash_bridge_upgrade_from_v0_6_0() { + let worker = near_workspaces::sandbox().await.unwrap(); + let upgrade_context = UpgradeContext::new( + &worker, + "tests/data/zcash_bridge_v0-6-0.wasm", + "tests/data/nbtc_v0-6-0.wasm", + ) + .await; + check!(view upgrade_context.get_satoshi_bridge_version()); + check!(upgrade_context.upgrade_satoshi_bridge("../../res/zcash_bridge.wasm")); check!(view upgrade_context.get_satoshi_bridge_version()); } #[tokio::test] async fn test_nbtc_upgrade() { let worker = near_workspaces::sandbox().await.unwrap(); - let upgrade_context = UpgradeContext::new(&worker).await; + let upgrade_context = UpgradeContext::new( + &worker, + "../../res/bitcoin_bridge.wasm", + "../../res/nbtc.wasm", + ) + .await; + check!(view upgrade_context.get_nbtc_version()); + check!(upgrade_context.upgrade_nbtc("../../res/nbtc.wasm")); + check!(view upgrade_context.get_nbtc_version()); +} + +#[tokio::test] +async fn test_nbtc_upgrade_from_v0_5_1() { + let worker = near_workspaces::sandbox().await.unwrap(); + let upgrade_context = UpgradeContext::new( + &worker, + "tests/data/btc_bridge_v0-5-1.wasm", + "tests/data/nbtc_v0-5-1.wasm", + ) + .await; check!(view upgrade_context.get_nbtc_version()); check!(upgrade_context.upgrade_nbtc("../../res/nbtc.wasm")); check!(view upgrade_context.get_nbtc_version()); @@ -22,7 +74,7 @@ async fn test_nbtc_upgrade() { #[tokio::test] async fn test_set_icon() { let worker = near_workspaces::sandbox().await.unwrap(); - let context = Context::new(&worker).await; + let context = Context::new(&worker, None).await; println!("{:?}", context.ft_metadata().await.unwrap().icon); check!(context.set_metadata("new icon")); println!("{:?}", context.ft_metadata().await.unwrap().icon); diff --git a/migrate/check_proposal.sh b/migrate/check_proposal.sh new file mode 100755 index 00000000..5fff4f5e --- /dev/null +++ b/migrate/check_proposal.sh @@ -0,0 +1,31 @@ +PROPOSAL_ID=187 +EXPECTED_NBTC_BS58_HASH=GtmZ6FMxRCN7KcANFDaNmR5JK2rusKAeyGaDSFTNoWvx +DAO_ACCOUNT_ID=rainbowbridge.sputnik-dao.near +NEAR_NETWORK=mainnet + +mkdir -p tmp + +PROP_JSON=./tmp/actual_proposal.json +WASM_PATH=./tmp/decoded_args.wasm + +near contract call-function as-read-only "$DAO_ACCOUNT_ID" get_proposal json-args "{\"id\": $PROPOSAL_ID}" network-config $NEAR_NETWORK now > $PROP_JSON + +if ! jq -e '.kind.FunctionCall.actions[0].args' "$PROP_JSON" >/dev/null 2>&1; then + echo "❌ kind.FunctionCall.actions[0].args not found" + echo "File: $PROP_JSON" + exit 1 +fi + +WASM_B64="$(jq -r '.kind.FunctionCall.actions[0].args' "$PROP_JSON")" +printf '%s' "$WASM_B64" | base64 -d > "$WASM_PATH" + +DECODED_NBTC_BS58_HASH=$(sha256sum $WASM_PATH | awk '{print $1}' | xxd -r -p | base58) +if [[ "$DECODED_NBTC_BS58_HASH" != "$EXPECTED_NBTC_BS58_HASH" ]]; then + echo "❌ Incorrect nBTC wasm hash" + echo "Expected: $EXPECTED_NBTC_BS58_HASH" + echo "Actual: $DECODED_NBTC_BS58_HASH" + exit 1 +else + echo "✅ nBTC wasm hash is correct" + exit 0 +fi diff --git a/migrate/create_proposal.sh b/migrate/create_proposal.sh new file mode 100755 index 00000000..df89fdc7 --- /dev/null +++ b/migrate/create_proposal.sh @@ -0,0 +1,47 @@ +EXPECTED_NBTC_BS58_HASH=3dXNLxNT1cSso4SeDzffnpq1SynCBb95WF9gBLVYvYSE +NBTC_ACCOUNT_ID=nbtc.bridge.near +DAO_ACCOUNT_ID=rainbowbridge.sputnik-dao.near +SIGNER_ACCOUNT_ID=bridge-ops.near +NEAR_NETWORK=mainnet + +mkdir -p tmp + +cd ../contracts/nbtc +cargo near build reproducible-wasm +cd ../../migrate + +NBTC_WASM_PATH=../target/near/nbtc/nbtc.wasm +ACTUAL_NBTC_BS58_HASH=$(sha256sum $NBTC_WASM_PATH | awk '{print $1}' | xxd -r -p | base58) + +if [[ "$ACTUAL_NBTC_BS58_HASH" != "$EXPECTED_NBTC_BS58_HASH" ]]; then + echo "❌ Incorrect nBTC wasm hash" + echo "Expected: $EXPECTED_NBTC_BS58_HASH" + echo "Actual: $ACTUAL_NBTC_BS58_HASH" + exit 1 +fi + +WASM_B64=$(base64 -w 0 $NBTC_WASM_PATH 2>/dev/null || base64 $NBTC_WASM_PATH | tr -d '\n') + +{ + echo '{ + "proposal": { + "description": "Upgrade + migrate nBTC", + "kind": { + "FunctionCall": { + "receiver_id": "'$NBTC_ACCOUNT_ID'", + "actions": [ + { + "method_name": "upgrade_and_migrate", + "args": "'$WASM_B64'", + "deposit": "0", + "gas": "180000000000000" + } + ] + } + } + } + }' +} > ./tmp/proposal.json + + +near contract call-function as-transaction $DAO_ACCOUNT_ID add_proposal file-args ./tmp/proposal.json prepaid-gas '100.0 Tgas' attached-deposit '1 NEAR' sign-as $SIGNER_ACCOUNT_ID network-config $NEAR_NETWORK sign-with-keychain send