diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bda6dc..120c9c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,19 +28,13 @@ jobs: with: cache-provider: warpbuild - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Run Clippy - run: cargo clippy --lib --bins --examples -- -D warnings -D clippy::pedantic -D clippy::as_conversions -A clippy::must_use_candidate -A clippy::needless_pass_by_value -A clippy::missing_panics_doc -A clippy::missing_errors_doc -A clippy::match_same_arms -A clippy::unused_self -A clippy::too_many_lines + - 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: Build contracts - run: make build - timeout-minutes: 60 - - name: Run tests - run: cargo test --all -- --nocapture + run: make test + diff --git a/Cargo.lock b/Cargo.lock index d4805a6..ba1975f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3715,9 +3715,10 @@ dependencies = [ [[package]] name = "satoshi-bridge" -version = "0.7.3" +version = "0.7.4" dependencies = [ "bitcoin", + "bs58 0.5.1", "core2", "crypto-shared", "ed25519-dalek", @@ -3728,6 +3729,9 @@ dependencies = [ "near-plugins", "near-sdk", "near-workspaces", + "orchard", + "rand", + "sapling-crypto", "tokio", "zcash_address", "zcash_primitives", diff --git a/Makefile b/Makefile index 00b2269..f8dd2a5 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,48 @@ -.PHONY: zcash-bridge +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 zcash-bridge nbtc mock-chain-signatures mock-btc-light-client mock-dapp +FEATURES = bitcoin zcash -lint: +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: $(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)) -zcash-bridge: contracts/satoshi-bridge - $(call local_build_zcash_wasm) +$(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) @@ -26,19 +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) - $(call build_release_zcash_wasm) - -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)) diff --git a/contracts/mock-btc-light-client/src/lib.rs b/contracts/mock-btc-light-client/src/lib.rs index 6b03fc5..c7984e0 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/satoshi-bridge/Cargo.toml b/contracts/satoshi-bridge/Cargo.toml index 6e85315..eba415f 100644 --- a/contracts/satoshi-bridge/Cargo.toml +++ b/contracts/satoshi-bridge/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "satoshi-bridge" -version = "0.7.3" +version = "0.7.4" edition.workspace = true publish.workspace = true repository.workspace = true @@ -21,18 +21,11 @@ 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" -] +container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "--locked", "--no-abi", "--no-default-features", "--features", "zcash"] [dependencies] near-sdk.workspace = true @@ -45,6 +38,8 @@ 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" } @@ -55,7 +50,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", "core2"] +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 6d02b2f..6fc2cef 100644 --- a/contracts/satoshi-bridge/src/account.rs +++ b/contracts/satoshi-bridge/src/account.rs @@ -1,4 +1,5 @@ use crate::{near, u128_dec_format, AccountId, Contract}; +use near_sdk::env; use std::collections::HashSet; #[near(serializers = [borsh, json])] @@ -85,16 +86,20 @@ impl Contract { self.data() .accounts .get(account_id) - .map(Into::into) - .expect("ACCOUNT NOT REGISTERED") + .map(|o| o.into()) + .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 { self.data_mut() .accounts .get_mut(account_id) - .map(Into::into) - .expect("ACCOUNT NOT REGISTERED") + .map(|o| o.into()) + .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 6747485..d61975c 100644 --- a/contracts/satoshi-bridge/src/api/bridge.rs +++ b/contracts/satoshi-bridge/src/api/bridge.rs @@ -1,9 +1,5 @@ -use crate::{ - assert_one_yocto, env, generate_utxo_storage_key, get_deposit_path, nano_to_sec, near, - psbt_wrapper::PsbtWrapper, require, AccessControllable, AccountId, BTCPendingInfo, Contract, - ContractExt, DepositMsg, Event, LockTime, OriginalState, OutPoint, Pausable, PendingInfoStage, - PendingInfoState, PendingUTXOInfo, Promise, Role, TxOut, WrappedTransaction, UTXO, -}; +use crate::psbt_wrapper::PsbtWrapper; +use crate::*; use near_plugins::{access_control_any, pause}; #[near] @@ -204,7 +200,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) @@ -213,7 +214,12 @@ impl Contract { "Previous btc tx has not been signed" ); - self.withdraw_rbf_chain_specific(account_id, original_btc_pending_verify_id, output); + 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. @@ -242,6 +248,7 @@ impl Contract { user_account_id, original_btc_pending_verify_id, output, + None, ); } @@ -327,6 +334,7 @@ impl Contract { account_id, original_btc_pending_verify_id, output, + None, ); } @@ -359,6 +367,7 @@ impl Contract { user_account_id, original_btc_pending_verify_id, output, + None, ); } @@ -416,7 +425,7 @@ impl Contract { pub fn create_active_utxo_management_pending_info( &mut self, account_id: AccountId, - psbt: &mut PsbtWrapper, + mut psbt: PsbtWrapper, ) { let account = self.internal_unwrap_account(&account_id); require!( @@ -424,9 +433,9 @@ impl Contract { "Previous btc tx has not been signed" ); - let (utxo_storage_keys, vutxos) = self.generate_vutxos(psbt); + let (utxo_storage_keys, vutxos) = self.generate_vutxos(&mut psbt); let (actual_received_amount, gas_fee) = - self.check_active_management_psbt_valid(psbt, &vutxos); + self.check_active_management_psbt_valid(&psbt, &vutxos); require!( gas_fee <= self.data().cur_available_protocol_fee, "Insufficient protocol_fee" diff --git a/contracts/satoshi-bridge/src/api/token_receiver.rs b/contracts/satoshi-bridge/src/api/token_receiver.rs index 26a0c6a..7906b8e 100644 --- a/contracts/satoshi-bridge/src/api/token_receiver.rs +++ b/contracts/satoshi-bridge/src/api/token_receiver.rs @@ -1,9 +1,4 @@ -use crate::{ - env, nano_to_sec, near, psbt_wrapper::PsbtWrapper, require, serde_json, AccessControllable, - AccountId, BTCPendingInfo, Contract, ContractExt, Event, Gas, OriginalState, OutPoint, - Pausable, PendingInfoStage, PendingInfoState, PromiseOrValue, Role, TxOut, U128, -}; - +use crate::{psbt_wrapper::PsbtWrapper, *}; use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; use near_plugins::pause; @@ -12,11 +7,13 @@ 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, }, } @@ -56,6 +53,7 @@ impl FungibleTokenReceiver for Contract { input, output, max_gas_fee, + chain_specific_data, } => self.ft_on_transfer_withdraw_chain_specific( sender_id, amount, @@ -63,6 +61,7 @@ impl FungibleTokenReceiver for Contract { input, output, max_gas_fee, + chain_specific_data, ), } } @@ -74,27 +73,24 @@ impl Contract { sender_id: AccountId, amount: u128, target_btc_address: String, - psbt: &mut PsbtWrapper, + mut psbt: PsbtWrapper, max_gas_fee: Option, ) { - let (utxo_storage_keys, vutxos) = self.generate_vutxos(psbt); + 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 target_address_script_pubkey = self - .internal_config() - .string_to_script_pubkey(&target_btc_address); 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_address_script_pubkey, + target_btc_address.clone(), &withdraw_change_address_script_pubkey, - psbt, + &psbt, &vutxos, amount, withdraw_fee, diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs b/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs index e3bb76d..5a5114c 100644 --- a/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs +++ b/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs @@ -1,3 +1,5 @@ +use crate::bitcoin_utils::types::ChainSpecificData; +use crate::env; use crate::psbt_wrapper::PsbtWrapper; use crate::{BTCPendingInfo, Contract, Event}; use bitcoin::{OutPoint, TxOut}; @@ -11,7 +13,9 @@ macro_rules! define_rbf_method { 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); @@ -20,8 +24,12 @@ macro_rules! define_rbf_method { output, ); - let btc_pending_id = - self.$internal_fn(&account_id, original_btc_pending_verify_id, new_psbt); + 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()); @@ -36,7 +44,13 @@ macro_rules! define_rbf_method { } impl Contract { - pub(crate) fn check_psbt_chain_specific(&self, _psbt: &PsbtWrapper, _gas_fee: u128) {} + 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, @@ -48,6 +62,7 @@ impl Contract { 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, @@ -56,13 +71,13 @@ impl Contract { input: Vec, output: Vec, max_gas_fee: Option, + _chain_specific_data: Option, ) -> PromiseOrValue { - let mut psbt = PsbtWrapper::new(input, output); self.create_btc_pending_info( sender_id, amount, target_btc_address, - &mut psbt, + PsbtWrapper::new(input, output), max_gas_fee, ); PromiseOrValue::Value(U128(0)) @@ -85,8 +100,10 @@ impl Contract { input: Vec, output: Vec, ) { - let mut psbt = PsbtWrapper::new(input, output); - self.create_active_utxo_management_pending_info(account_id, &mut psbt); + self.create_active_utxo_management_pending_info( + account_id, + PsbtWrapper::new(input, output), + ); } pub(crate) fn generate_psbt_from_original_psbt_and_new_output( diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/mod.rs b/contracts/satoshi-bridge/src/bitcoin_utils/mod.rs index 89ef353..18ae7bc 100644 --- a/contracts/satoshi-bridge/src/bitcoin_utils/mod.rs +++ b/contracts/satoshi-bridge/src/bitcoin_utils/mod.rs @@ -1,3 +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 index d6a3721..85c3577 100644 --- a/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs +++ b/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs @@ -105,14 +105,18 @@ impl PsbtWrapper { }) .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).unwrap(); + 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_HEX"), + psbt: Psbt::deserialize(&psbt_bytes).expect("ERR_INVALID_PSBT: failed to parse PSBT"), } } @@ -124,27 +128,27 @@ impl PsbtWrapper { self.psbt .clone() .extract_tx() - .unwrap() + .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_key: &bitcoin::PublicKey) -> [u8; 32] { + 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, - &self.psbt.inputs[vin] - .witness_utxo - .as_ref() - .unwrap() - .script_pubkey, - self.psbt.inputs[vin].witness_utxo.as_ref().unwrap().value, + &witness_utxo.script_pubkey, + witness_utxo.value, bitcoin::EcdsaSighashType::All, ) - .unwrap() + .expect("ERR_SIGHASH: failed to compute signature hash") .to_raw_hash() .to_byte_array() } @@ -158,4 +162,8 @@ impl PsbtWrapper { 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/types.rs b/contracts/satoshi-bridge/src/bitcoin_utils/types.rs new file mode 100644 index 0000000..137cd84 --- /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_pending_info.rs b/contracts/satoshi-bridge/src/btc_pending_info.rs index 108dfea..cdf15dd 100644 --- a/contracts/satoshi-bridge/src/btc_pending_info.rs +++ b/contracts/satoshi-bridge/src/btc_pending_info.rs @@ -456,6 +456,7 @@ mod tests { 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); diff --git a/contracts/satoshi-bridge/src/chain_signature.rs b/contracts/satoshi-bridge/src/chain_signature.rs index 1a0f253..19fdc66 100644 --- a/contracts/satoshi-bridge/src/chain_signature.rs +++ b/contracts/satoshi-bridge/src/chain_signature.rs @@ -79,12 +79,13 @@ impl Contract { sign_index: usize, key_version: u32, ) -> Promise { - let public_key = self.generate_btc_public_key( - &self - .internal_unwrap_btc_pending_info(&btc_pending_sign_id) - .vutxos[sign_index] - .get_path(), - ); + 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!( @@ -93,7 +94,7 @@ impl Contract { ); let payload = btc_pending_info .get_psbt() - .get_hash_to_sign(sign_index, &public_key); + .get_hash_to_sign(sign_index, &public_keys); let path = btc_pending_info.vutxos[sign_index].get_path(); self.sign_promise(SignRequest { payload, @@ -140,6 +141,7 @@ impl Contract { if let Some(result_bytes) = promise_result_as_success() { let signature = serde_json::from_slice::(&result_bytes) .expect("Invalid signature"); + let public_key = self .generate_btc_public_key( &self @@ -168,12 +170,27 @@ impl Contract { btc_pending_info.psbt_hex = psbt.serialize(); if btc_pending_info.is_all_signed() { 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(); diff --git a/contracts/satoshi-bridge/src/config.rs b/contracts/satoshi-bridge/src/config.rs index 440e62c..1614189 100644 --- a/contracts/satoshi-bridge/src/config.rs +++ b/contracts/satoshi-bridge/src/config.rs @@ -142,14 +142,18 @@ impl Config { } pub fn get_change_script_pubkey(&self) -> ScriptBuf { - self.string_to_script_pubkey(self.change_address.as_ref().unwrap()) + 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| panic!("{address_string}: {e}")) + .unwrap_or_else(|e| env::panic_str(&format!("{address_string}: {e}"))) .script_pubkey() .expect("Failed to get script pubkey") } @@ -187,11 +191,19 @@ impl Config { 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 { diff --git a/contracts/satoshi-bridge/src/event.rs b/contracts/satoshi-bridge/src/event.rs index 590b987..eeca095 100644 --- a/contracts/satoshi-bridge/src/event.rs +++ b/contracts/satoshi-bridge/src/event.rs @@ -53,7 +53,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/kdf.rs b/contracts/satoshi-bridge/src/kdf.rs index c4fe126..a1026c9 100644 --- a/contracts/satoshi-bridge/src/kdf.rs +++ b/contracts/satoshi-bridge/src/kdf.rs @@ -3,6 +3,16 @@ 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( diff --git a/contracts/satoshi-bridge/src/lib.rs b/contracts/satoshi-bridge/src/lib.rs index 149eb1b..05c8348 100644 --- a/contracts/satoshi-bridge/src/lib.rs +++ b/contracts/satoshi-bridge/src/lib.rs @@ -32,7 +32,7 @@ pub mod json_utils; pub mod kdf; pub mod legacy; pub mod nbtc; -mod network; +pub mod network; pub mod psbt; pub mod rbf; pub mod token_transfer; @@ -61,6 +61,8 @@ pub use crate::utxo::*; 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::*; @@ -68,6 +70,8 @@ pub use crate::zcash_utils::contract_methods::*; 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::*; diff --git a/contracts/satoshi-bridge/src/network.rs b/contracts/satoshi-bridge/src/network.rs index 7213f6b..bf00f17 100644 --- a/contracts/satoshi-bridge/src/network.rs +++ b/contracts/satoshi-bridge/src/network.rs @@ -8,6 +8,12 @@ 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 { @@ -202,6 +208,24 @@ impl Address { } } + /// 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(); diff --git a/contracts/satoshi-bridge/src/psbt.rs b/contracts/satoshi-bridge/src/psbt.rs index 8828eb7..6a782ea 100644 --- a/contracts/satoshi-bridge/src/psbt.rs +++ b/contracts/satoshi-bridge/src/psbt.rs @@ -7,7 +7,7 @@ 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: &PsbtWrapper, vutxos: &[VUTXO], @@ -22,7 +22,7 @@ impl Contract { 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, @@ -162,7 +162,7 @@ impl Contract { pub fn check_withdraw_psbt( &self, psbt: &PsbtWrapper, - target_address_script_pubkey: &ScriptBuf, + target_btc_address: String, withdraw_change_address_script_pubkey: &ScriptBuf, vutxos: &[VUTXO], amount: u128, @@ -175,30 +175,40 @@ impl Contract { let mut total_output_amount = 0; let mut actual_received_amounts = vec![]; let mut change_amounts = vec![]; - psbt.get_output().iter().for_each(|output| { - let output_value = u128::from(output.value.to_sat()); - 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(), - ); - } - }); + + 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." @@ -239,7 +249,7 @@ impl Contract { ) ); - self.check_psbt_chain_specific(psbt, gas_fee); + self.check_psbt_chain_specific(psbt, gas_fee, target_btc_address); (input_num, change_num, actual_received_amount, gas_fee) } } diff --git a/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs b/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs index e23db1b..0afa66d 100644 --- a/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs +++ b/contracts/satoshi-bridge/src/rbf/active_utxo_management.rs @@ -29,6 +29,7 @@ impl Contract { account_id: &AccountId, original_btc_pending_verify_id: String, 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); 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 6aae207..1d83b66 100644 --- a/contracts/satoshi-bridge/src/rbf/cancel_active_utxo_management.rs +++ b/contracts/satoshi-bridge/src/rbf/cancel_active_utxo_management.rs @@ -23,6 +23,7 @@ impl Contract { _account_id: &AccountId, original_btc_pending_verify_id: String, 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); diff --git a/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs b/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs index 2b108e3..a1d57b4 100644 --- a/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs +++ b/contracts/satoshi-bridge/src/rbf/cancel_withdraw.rs @@ -24,6 +24,7 @@ impl Contract { _account_id: &AccountId, original_btc_pending_verify_id: String, 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); @@ -55,7 +56,7 @@ impl Contract { .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/withdraw.rs b/contracts/satoshi-bridge/src/rbf/withdraw.rs index af9926d..36fdc9a 100644 --- a/contracts/satoshi-bridge/src/rbf/withdraw.rs +++ b/contracts/satoshi-bridge/src/rbf/withdraw.rs @@ -1,6 +1,6 @@ use crate::{ - init_rbf_btc_pending_info, psbt_wrapper::PsbtWrapper, require, AccountId, BTCPendingInfo, - Contract, PendingInfoStage, PendingInfoState, RbfState, + init_rbf_btc_pending_info, network::Address, psbt_wrapper::PsbtWrapper, require, AccountId, + BTCPendingInfo, Contract, PendingInfoStage, PendingInfoState, RbfState, }; impl Contract { @@ -13,20 +13,16 @@ impl Contract { 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; require!( 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, @@ -40,6 +36,7 @@ impl Contract { account_id: &AccountId, original_btc_pending_verify_id: String, 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); @@ -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/utxo.rs b/contracts/satoshi-bridge/src/utxo.rs index 447b003..ecefe7e 100644 --- a/contracts/satoshi-bridge/src/utxo.rs +++ b/contracts/satoshi-bridge/src/utxo.rs @@ -1,6 +1,7 @@ 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)] @@ -67,7 +68,9 @@ impl Contract { self.data_mut() .utxos .remove(&utxo_storage_key) - .unwrap_or_else(|| panic!("UTXO {utxo_storage_key} not exist")) + .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 index 6cf303b..64b0628 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs @@ -1,4 +1,5 @@ use crate::psbt_wrapper::PsbtWrapper; +use crate::zcash_utils::types::ChainSpecificData; use crate::*; use bitcoin::{OutPoint, TxOut}; use near_sdk::json_types::U128; @@ -15,11 +16,19 @@ macro_rules! define_rbf_callback { 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), + .$callback_name( + user_account_id, + original_btc_pending_verify_id, + output, + chain_specific_data, + predecessor_account_id, + ), ); } } @@ -32,9 +41,12 @@ macro_rules! define_rbf_callback { 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 = last_block_height + self.get_config().expiry_height_gap; + 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); @@ -42,11 +54,17 @@ macro_rules! define_rbf_callback { 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); + 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()); @@ -85,6 +103,7 @@ define_rbf_callback!( #[near] impl Contract { #[private] + #[allow(clippy::too_many_arguments)] pub fn ft_on_transfer_callback( &mut self, sender_id: AccountId, @@ -93,22 +112,29 @@ impl Contract { input: Vec, output: Vec, max_gas_fee: Option, + chain_specific_data: Option, #[callback_unwrap] last_block_height: u32, ) -> U128 { - let expiry_height = last_block_height + self.get_config().expiry_height_gap; - let mut psbt = PsbtWrapper::new(input, output, expiry_height, self.internal_config()); - self.create_btc_pending_info( - sender_id, - amount.0, - target_btc_address, - &mut psbt, - max_gas_fee, + 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); + U128(0) } #[private] + #[allow(clippy::too_many_arguments)] pub fn active_utxo_management_callback( &mut self, account_id: AccountId, @@ -118,14 +144,54 @@ impl Contract { ) { let expiry_height = last_block_height + self.get_config().expiry_height_gap; - let mut psbt = PsbtWrapper::new(input, output, expiry_height, self.internal_config()); + // 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, &mut psbt); + self.create_active_utxo_management_pending_info(account_id, psbt); } } impl Contract { - pub(crate) fn check_psbt_chain_specific(&self, psbt: &PsbtWrapper, gas_fee: u128) { + 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, @@ -135,6 +201,11 @@ impl Contract { 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( @@ -143,6 +214,7 @@ impl Contract { ) { } + #[allow(clippy::too_many_arguments)] pub(crate) fn ft_on_transfer_withdraw_chain_specific( &self, sender_id: AccountId, @@ -151,6 +223,7 @@ impl Contract { input: Vec, output: Vec, max_gas_fee: Option, + chain_specific_data: Option, ) -> PromiseOrValue { PromiseOrValue::Promise( self.get_last_block_height_promise().then( @@ -163,6 +236,7 @@ impl Contract { input, output, max_gas_fee, + chain_specific_data, ), ), ) @@ -185,13 +259,17 @@ impl Contract { &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 index 89ef353..ea6ecdf 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/mod.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/mod.rs @@ -1,3 +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 0000000..f85813f --- /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 index ba5b5df..f3f376a 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs @@ -1,14 +1,16 @@ +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 std::io; -use std::io::{Cursor, Read, Write}; - -use crate::zcash_utils::transaction::Transaction; use bitcoin::hashes::Hash; use bitcoin::{OutPoint, TxOut}; -use near_sdk::require; +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, TxVersion}; +use zcash_primitives::transaction::{TransactionData, TransactionDigest, TxVersion}; use zcash_protocol::consensus::{BlockHeight, BranchId}; use zcash_protocol::value::Zatoshis; use zcash_transparent::bundle::Authorized; @@ -22,17 +24,26 @@ pub struct PsbtWrapper { 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"); - require!(!output.is_empty(), "empty output"); + // 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 @@ -61,19 +72,41 @@ impl PsbtWrapper { 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(expiry_height, config), + 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() { @@ -89,12 +122,19 @@ impl PsbtWrapper { .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(expiry_height, config), + 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, } } @@ -114,6 +154,21 @@ impl PsbtWrapper { 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() @@ -127,6 +182,15 @@ impl PsbtWrapper { .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() @@ -140,7 +204,7 @@ impl PsbtWrapper { pub fn to_bytes(&self) -> Vec { let mut buf = Vec::::new(); - let version: u8 = 2; + let version: u8 = 3; buf.push(version); match self.branch_id { BranchId::Nu6 => buf.write_all(&[7u8; 1]).unwrap(), @@ -170,6 +234,32 @@ impl PsbtWrapper { 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 { @@ -177,60 +267,137 @@ impl PsbtWrapper { } pub fn deserialize(psbt_hex: &String) -> Self { - let bytes = hex::decode(&psbt_hex).unwrap(); + 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(); - let branch_id = if version == 2 { - let branch_id_u8 = read_u8(&mut rdr).unwrap(); + 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, - _ => unreachable!(), + _ => env::panic_str("ERR_INVALID_PSBT: unsupported branch_id"), } } else { BranchId::Nu6_1 }; - let expiry_height = read_u32_le(&mut rdr).unwrap(); + 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() as usize; + 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()); + 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() as usize; + 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()); + 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() as usize; + 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()); + 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() + 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 { + 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, @@ -239,29 +406,51 @@ impl PsbtWrapper { Some(transparent_bundle), None, None, - None, + self.orchard.map(|b| b.bundle), ) .freeze() - .unwrap(); + .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 { + 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_key: &bitcoin::PublicKey) -> [u8; 32] { + 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_key, + public_keys, self.branch_id, ); - let txid_parts = tx_data.digest(zcash_primitives::transaction::txid::TxIdDigester); + 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( @@ -273,9 +462,8 @@ impl PsbtWrapper { ), ); - zcash_primitives::transaction::sighash::signature_hash(&tx_data, &sig_input, &txid_parts) + *zcash_primitives::transaction::sighash::signature_hash(&tx_data, &sig_input, &txid_parts) .as_ref() - .clone() } pub fn save_signature( @@ -294,23 +482,32 @@ impl PsbtWrapper { 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, - 0, - 0, + 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(expiry_height: u32, config: &Config) -> BranchId { - let current_height = expiry_height - config.expiry_height_gap; - return config.chain.get_branch_id(current_height); +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 { @@ -325,6 +522,18 @@ fn read_u8(r: &mut R) -> io::Result { 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)?; diff --git a/contracts/satoshi-bridge/src/zcash_utils/transaction.rs b/contracts/satoshi-bridge/src/zcash_utils/transaction.rs index 021c2bb..94d2f49 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/transaction.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/transaction.rs @@ -2,12 +2,17 @@ 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, Unauthorized, -}; +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, @@ -52,17 +57,17 @@ impl Transaction { } pub fn get_transparent_builder( - vin: &Vec>, - vout: &Vec, - input: &Vec, - public_key: &bitcoin::PublicKey, + 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_key.inner, + public_keys[index].inner, vin[index].prevout.clone(), input[index].clone(), ) @@ -79,21 +84,21 @@ impl Transaction { } pub fn to_zcash_tx( - vin: &Vec>, - vout: &Vec, - input: &Vec, + vin: &[zcash_transparent::bundle::TxIn], + vout: &[zcash_transparent::bundle::TxOut], + input: &[zcash_transparent::bundle::TxOut], expiry_height: u32, - public_key: &bitcoin::PublicKey, + public_keys: &[bitcoin::PublicKey], branch_id: BranchId, - ) -> TransactionData { - let transparent_bundle = Self::get_transparent_builder(vin, vout, input, public_key) + ) -> 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); - let inner_tx = TransactionData::from_parts( + TransactionData::from_parts( TxVersion::V5, branch_id, lock_time, @@ -102,8 +107,6 @@ impl Transaction { None, None, None, - ); - - inner_tx + ) } } 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 0000000..aab7a40 --- /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/orchard_bundle_cache_100000.txt b/contracts/satoshi-bridge/tests/orchard_bundle_cache_100000.txt new file mode 100644 index 0000000..9935fd3 --- /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 0000000..7b130cb --- /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 0000000..a97f73f --- /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 0000000..451e846 --- /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 36d47bc..3c2054a 100644 --- a/contracts/satoshi-bridge/tests/setup/context.rs +++ b/contracts/satoshi-bridge/tests/setup/context.rs @@ -23,6 +23,11 @@ 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, @@ -38,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, @@ -56,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() @@ -162,10 +167,14 @@ impl Context { .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": "BitcoinMainnet", + "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(), @@ -201,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() @@ -1129,7 +1139,7 @@ impl UpgradeContext { "rbf_num_limit": 99, "max_btc_tx_pending_sec": 3600 * 24, "unhealthy_utxo_amount": 1000, - "expiry_height_gap": 100, + "expiry_height_gap": 1000, } })) .transact() @@ -1151,10 +1161,10 @@ impl UpgradeContext { .args_json(json!({ "controller": root.id(), "bridge_id": previous_satoshi_bridge_contract.id(), - "name": "Near WTC".to_string(), - "symbol": "NBTC".to_string(), - "decimals": 8, - "icon": Some(DATA_IMAGE_SVG_NEAR_ICON.to_string()), + "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 38dd94e..76d00fc 100644 --- a/contracts/satoshi-bridge/tests/setup/mod.rs +++ b/contracts/satoshi-bridge/tests/setup/mod.rs @@ -1,6 +1,16 @@ #![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}; diff --git a/contracts/satoshi-bridge/tests/setup/orchard.rs b/contracts/satoshi-bridge/tests/setup/orchard.rs new file mode 100644 index 0000000..f128ff6 --- /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 7a45574..cac6975 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 0000000..61c0874 --- /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").id().clone(), + 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").id().clone(), + 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").id().clone(), + 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").id().clone(), + 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").id().clone(), + 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").id().clone(), + 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 0000000..f8d726a --- /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").id().clone(), + 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").id().clone(), + 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").id().clone(), + 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").id().clone(), + 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 509f372..5502ebc 100644 --- a/contracts/satoshi-bridge/tests/test_satoshi_bridge.rs +++ b/contracts/satoshi-bridge/tests/test_satoshi_bridge.rs @@ -1,15 +1,32 @@ mod setup; -use std::str::FromStr; - -use bitcoin::{Address, Amount, OutPoint, TxOut}; +use bitcoin::{Amount, OutPoint, TxOut}; use near_sdk::{AccountId, Gas}; +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()] @@ -88,7 +105,7 @@ 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 @@ -126,7 +143,7 @@ async fn test_base() { )], vec![ (alice_btc_deposit_address.as_str(), 10000), - ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000) + (TARGET_ADDRESS, 90000) ], ), 0, @@ -173,7 +190,7 @@ async fn test_base() { None, ),], vec![ - ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (TARGET_ADDRESS, 90000), (alice_btc_deposit_address.as_str(), 50000), ], ), @@ -222,7 +239,7 @@ async fn test_base() { None, ),], vec![ - ("1MgiBKohM2poApYamQadp21vJrNyh5T19G", 90000), + (TARGET_ADDRESS, 90000), (alice_btc_deposit_address.as_str(), 50000), ], ), @@ -256,7 +273,7 @@ async fn test_base() { ),], vec![ (bob_btc_deposit_address.as_str(), 200000), - ("1F3HTDzfWnPPbBaUrxg99LJEjHQd4NsisC", 50000), + (TARGET_ADDRESS, 50000), ], ), 0, @@ -297,12 +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(), - max_gas_fee: None, + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![ OutPoint { txid: first_utxo[0].parse().unwrap(), @@ -314,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!( @@ -454,7 +472,7 @@ 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(); @@ -514,23 +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(), - max_gas_fee: None, + 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() @@ -593,7 +612,7 @@ 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(); @@ -653,23 +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(), - max_gas_fee: None, + 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() @@ -735,7 +755,7 @@ 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(); @@ -801,23 +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(), - max_gas_fee: None, + 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() @@ -858,7 +879,7 @@ 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; { @@ -1370,7 +1391,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 @@ -1452,7 +1473,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!( @@ -1461,8 +1482,7 @@ async fn test_utxo_passive_management() { "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), - max_gas_fee: None, + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: utxo500000[0].parse().unwrap(), vout: utxo500000[1].parse().unwrap(), @@ -1472,19 +1492,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" @@ -1497,8 +1522,7 @@ async fn test_utxo_passive_management() { "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), - max_gas_fee: None, + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![OutPoint { txid: utxo500000[0].parse().unwrap(), vout: utxo500000[1].parse().unwrap(), @@ -1508,26 +1532,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" @@ -1540,8 +1572,7 @@ async fn test_utxo_passive_management() { "bridge", withdraw_amount, TokenReceiverMessage::Withdraw { - target_btc_address: "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ".to_string(), - max_gas_fee: None, + target_btc_address: TARGET_ADDRESS.to_string(), input: vec![ OutPoint { txid: utxo500000[0].parse().unwrap(), @@ -1557,19 +1588,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" @@ -1579,7 +1615,7 @@ 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(); @@ -1640,23 +1676,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(), - max_gas_fee: None, + 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 @@ -1682,9 +1719,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" @@ -1698,23 +1736,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." @@ -1727,9 +1768,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()), ] ) ); @@ -1809,7 +1850,7 @@ 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(); @@ -1870,23 +1911,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(), - max_gas_fee: None, + 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 @@ -1910,7 +1952,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()), ] ) ); @@ -1992,7 +2034,7 @@ 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")); @@ -2088,8 +2130,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" @@ -2108,8 +2150,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" @@ -2128,8 +2170,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" @@ -2148,7 +2190,8 @@ async fn test_utxo_active_management() { ], vec![generate_tx_out( output_amount * 2, - "1PAGsaT5vDz6hjzvuenSw33hWzESTR3ZHQ" + TARGET_ADDRESS, + get_chain() ),] ), "Invalid output script_pubkey" @@ -2167,7 +2210,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" @@ -2192,7 +2236,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() ), ] ) @@ -2206,7 +2250,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." @@ -2215,8 +2260,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" @@ -2226,7 +2279,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" @@ -2235,7 +2289,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() ),] )); @@ -2258,7 +2313,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" @@ -2268,8 +2324,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." @@ -2280,11 +2340,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() ), ] ) @@ -2343,7 +2403,7 @@ 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")); @@ -2447,7 +2507,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() ), ] ) @@ -2460,7 +2520,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(); @@ -2484,11 +2545,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() ), ] ) diff --git a/contracts/satoshi-bridge/tests/test_upgrade.rs b/contracts/satoshi-bridge/tests/test_upgrade.rs index 90eb98c..541771e 100644 --- a/contracts/satoshi-bridge/tests/test_upgrade.rs +++ b/contracts/satoshi-bridge/tests/test_upgrade.rs @@ -6,12 +6,12 @@ async fn test_btc_bridge_upgrade() { let worker = near_workspaces::sandbox().await.unwrap(); let upgrade_context = UpgradeContext::new( &worker, - "../../res/satoshi_bridge.wasm", + "../../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()); } @@ -25,7 +25,7 @@ async fn test_btc_bridge_upgrade_from_v0_5_1() { ) .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()); } @@ -39,7 +39,7 @@ async fn test_zcash_bridge_upgrade_from_v0_6_0() { ) .await; check!(view upgrade_context.get_satoshi_bridge_version()); - check!(upgrade_context.upgrade_satoshi_bridge("../../res/zcash.wasm")); + check!(upgrade_context.upgrade_satoshi_bridge("../../res/zcash_bridge.wasm")); check!(view upgrade_context.get_satoshi_bridge_version()); } @@ -48,7 +48,7 @@ async fn test_nbtc_upgrade() { let worker = near_workspaces::sandbox().await.unwrap(); let upgrade_context = UpgradeContext::new( &worker, - "../../res/satoshi_bridge.wasm", + "../../res/bitcoin_bridge.wasm", "../../res/nbtc.wasm", ) .await; @@ -74,7 +74,7 @@ async fn test_nbtc_upgrade_from_v0_5_1() { #[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);